From 42eb5464fd8a65ed84b799de5d4dc225349449be Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Mon, 14 Sep 2015 15:35:58 +0100 Subject: [PATCH] Release React Native for Android This is an early release and there are several things that are known not to work if you're porting your iOS app to Android. See the Known Issues guide on the website. We will work with the community to reach platform parity with iOS. --- .gitignore | 6 + Examples/Movies/Movies/AppDelegate.m | 4 +- Examples/Movies/MoviesApp.android.js | 94 ++ Examples/Movies/SearchBar.android.js | 104 ++ Examples/Movies/android/app/build.gradle | 35 + .../Movies/android/app/proguard-rules.pro | 17 + .../android/app/src/main/AndroidManifest.xml | 21 + .../facebook/react/movies/MoviesActivity.java | 89 ++ .../res/drawable-hdpi/android_back_white.png | Bin 0 -> 237 bytes .../drawable-hdpi/android_search_white.png | Bin 0 -> 575 bytes .../res/drawable-mdpi/android_back_white.png | Bin 0 -> 190 bytes .../drawable-mdpi/android_search_white.png | Bin 0 -> 337 bytes .../res/drawable-xhdpi/android_back_white.png | Bin 0 -> 266 bytes .../drawable-xhdpi/android_search_white.png | Bin 0 -> 581 bytes .../drawable-xxhdpi/android_back_white.png | Bin 0 -> 337 bytes .../drawable-xxhdpi/android_search_white.png | Bin 0 -> 930 bytes .../res/drawable/rotten_tomatoes_icon.png | Bin 0 -> 58467 bytes .../app/src/main/res/layout/activity_main.xml | 12 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 8 + Examples/SampleApp/android/app/build.gradle | 35 + .../SampleApp/android/app/proguard-rules.pro | 17 + .../android/app/src/main/AndroidManifest.xml | 21 + .../facebook/react/sample/MainActivity.java | 88 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 8 + Examples/SampleApp/index.android.js | 52 + .../AccessibilityAndroidExample.android.js | 213 ++++ .../ProgressBarAndroidExample.android.js | 62 + .../UIExplorer/ScrollViewSimpleExample.js | 1 + .../SwitchAndroidExample.android.js | 80 ++ Examples/UIExplorer/TextExample.android.js | 349 ++++++ .../UIExplorer/TextInputExample.android.js | 316 +++++ .../UIExplorer/ToastAndroidExample.android.js | 68 + .../ToolbarAndroidExample.android.js | 119 ++ Examples/UIExplorer/UIExplorerApp.android.js | 53 +- Examples/UIExplorer/UIExplorerList.android.js | 99 ++ Examples/UIExplorer/XHRExample.android.js | 326 +++++ Examples/UIExplorer/android/app/build.gradle | 35 + .../UIExplorer/android/app/proguard-rules.pro | 17 + .../android/app/src/main/AndroidManifest.xml | 23 + .../app/src/main/java/UIExplorerActivity.java | 89 ++ .../res/drawable/ic_create_black_48dp.png | Bin 0 -> 406 bytes .../main/res/drawable/ic_menu_black_24dp.png | Bin 0 -> 179 bytes .../res/drawable/ic_settings_black_48dp.png | Bin 0 -> 1257 bytes .../src/main/res/drawable/launcher_icon.png | Bin 0 -> 9578 bytes .../res/drawable/uie_comment_highlighted.png | Bin 0 -> 403 bytes .../main/res/drawable/uie_comment_normal.png | Bin 0 -> 420 bytes .../main/res/drawable/uie_thumb_normal.png | Bin 0 -> 850 bytes .../main/res/drawable/uie_thumb_selected.png | Bin 0 -> 1110 bytes .../app/src/main/res/layout/activity_main.xml | 12 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 8 + Libraries/AppStateIOS/AppStateIOS.android.js | 4 +- .../ActivityIndicatorIOS.android.js | 22 + .../DatePicker/DatePickerIOS.android.js | 46 + .../DrawerLayoutAndroid.android.js | 224 ++++ .../Navigation/NavigatorIOS.android.js | 13 + ...orBreadcrumbNavigationBarStyles.android.js | 217 ++++ .../NavigatorNavigationBarStyles.android.js | 159 +++ .../ProgressBarAndroid.android.js | 92 ++ .../Components/SliderIOS/SliderIOS.android.js | 14 + .../StatusBar/StatusBarIOS.android.js | 14 + .../SwitchAndroid/SwitchAndroid.android.js | 80 ++ .../ToastAndroid/ToastAndroid.android.js | 38 + .../ToolbarAndroid/ToolbarAndroid.android.js | 174 +++ .../TouchableNativeFeedback.android.js | 219 ++++ Libraries/Image/Image.android.js | 169 +++ Libraries/Network/RCTNetworking.android.js | 45 + Libraries/Network/XMLHttpRequest.android.js | 66 + .../ReactIOS/renderApplication.android.js | 132 ++ Libraries/Storage/AsyncStorage.android.js | 233 ++++ Libraries/Utilities/BackAndroid.android.js | 63 + Libraries/Utilities/Platform.android.js | 22 + .../core/ExecutionEnvironment.android.js | 51 + README.md | 4 +- React.podspec | 2 +- ReactAndroid/DevExperience.md | 23 + ReactAndroid/README.md | 101 ++ ReactAndroid/build.gradle | 246 ++++ ReactAndroid/gradle.properties | 6 + ReactAndroid/libs/infer-annotations-1.5.jar | Bin 0 -> 11990 bytes ReactAndroid/release.gradle | 128 ++ ReactAndroid/src/main/AndroidManifest.xml | 7 + .../java/com/facebook/csslayout/CSSAlign.java | 20 + .../com/facebook/csslayout/CSSConstants.java | 21 + .../com/facebook/csslayout/CSSDirection.java | 18 + .../facebook/csslayout/CSSFlexDirection.java | 19 + .../com/facebook/csslayout/CSSJustify.java | 20 + .../com/facebook/csslayout/CSSLayout.java | 60 + .../facebook/csslayout/CSSLayoutContext.java | 23 + .../java/com/facebook/csslayout/CSSNode.java | 396 ++++++ .../facebook/csslayout/CSSPositionType.java | 17 + .../java/com/facebook/csslayout/CSSStyle.java | 46 + .../java/com/facebook/csslayout/CSSWrap.java | 17 + .../facebook/csslayout/CachedCSSLayout.java | 24 + .../com/facebook/csslayout/FloatUtil.java | 24 + .../com/facebook/csslayout/LayoutEngine.java | 1100 +++++++++++++++++ .../com/facebook/csslayout/MeasureOutput.java | 21 + .../main/java/com/facebook/csslayout/README | 12 + .../com/facebook/csslayout/README.facebook | 14 + .../java/com/facebook/csslayout/Spacing.java | 162 +++ .../com/facebook/csslayout/syncFromGithub.sh | 99 ++ .../main/java/com/facebook/jni/Countable.java | 40 + .../java/com/facebook/jni/CppException.java | 20 + .../facebook/jni/CppSystemErrorException.java | 27 + .../java/com/facebook/jni/HybridData.java | 50 + .../java/com/facebook/jni/Prerequisites.java | 65 + .../com/facebook/jni/UnknownCppException.java | 25 + .../proguard/annotations/DoNotStrip.java | 26 + .../annotations/KeepGettersAndSetters.java | 30 + .../annotations/proguard_annotations.pro | 15 + .../facebook/react/CompositeReactPackage.java | 88 ++ .../facebook/react/CoreModulesPackage.java | 86 ++ .../com/facebook/react/LifecycleState.java | 27 + .../facebook/react/ReactInstanceManager.java | 564 +++++++++ .../java/com/facebook/react/ReactPackage.java | 54 + .../com/facebook/react/ReactRootView.java | 374 ++++++ .../AbstractFloatPairPropertyUpdater.java | 64 + .../AbstractSingleFloatProperyUpdater.java | 54 + .../facebook/react/animation/Animation.java | 107 ++ .../react/animation/AnimationListener.java | 27 + .../animation/AnimationPropertyUpdater.java | 46 + .../react/animation/AnimationRegistry.java | 43 + .../react/animation/ImmediateAnimation.java | 28 + .../NoopAnimationPropertyUpdater.java | 30 + .../OpacityAnimationPropertyUpdater.java | 36 + .../PositionAnimationPairPropertyUpdater.java | 42 + .../RotationAnimationPropertyUpdater.java | 32 + .../ScaleXAnimationPropertyUpdater.java | 36 + .../ScaleXYAnimationPairPropertyUpdater.java | 42 + .../ScaleYAnimationPropertyUpdater.java | 36 + .../com/facebook/react/bridge/Arguments.java | 142 +++ .../react/bridge/AssertionException.java | 22 + .../facebook/react/bridge/BaseJavaModule.java | 181 +++ .../com/facebook/react/bridge/Callback.java | 25 + .../facebook/react/bridge/CallbackImpl.java | 29 + .../react/bridge/CatalystInstance.java | 419 +++++++ .../react/bridge/GuardedAsyncTask.java | 43 + .../bridge/InvalidIteratorException.java | 25 + .../JSApplicationCausedNativeException.java | 43 + ...JSApplicationIllegalArgumentException.java | 20 + .../facebook/react/bridge/JSBundleLoader.java | 69 ++ .../react/bridge/JSCJavaScriptExecutor.java | 28 + .../bridge/JSDebuggerWebSocketClient.java | 269 ++++ .../react/bridge/JavaScriptExecutor.java | 25 + .../react/bridge/JavaScriptModule.java | 29 + .../bridge/JavaScriptModuleRegistration.java | 96 ++ .../bridge/JavaScriptModuleRegistry.java | 76 ++ .../react/bridge/JavaScriptModulesConfig.java | 84 ++ .../react/bridge/JsonGeneratorHelper.java | 54 + .../react/bridge/LifecycleEventListener.java | 32 + .../bridge/NativeArgumentsParseException.java | 26 + .../facebook/react/bridge/NativeArray.java | 36 + .../com/facebook/react/bridge/NativeMap.java | 34 + .../facebook/react/bridge/NativeModule.java | 57 + .../NativeModuleCallExceptionHandler.java | 28 + .../react/bridge/NativeModuleRegistry.java | 200 +++ .../react/bridge/NoSuchKeyException.java | 24 + .../NotThreadSafeBridgeIdleDebugListener.java | 33 + .../ObjectAlreadyConsumedException.java | 26 + .../react/bridge/OnBatchCompleteListener.java | 18 + .../react/bridge/ProxyJavaScriptExecutor.java | 96 ++ .../react/bridge/ReactApplicationContext.java | 25 + .../facebook/react/bridge/ReactBridge.java | 71 ++ .../facebook/react/bridge/ReactCallback.java | 22 + .../facebook/react/bridge/ReactContext.java | 202 +++ .../bridge/ReactContextBaseJavaModule.java | 30 + .../facebook/react/bridge/ReactMethod.java | 26 + .../facebook/react/bridge/ReadableArray.java | 27 + .../facebook/react/bridge/ReadableMap.java | 28 + .../bridge/ReadableMapKeySeyIterator.java | 22 + .../react/bridge/ReadableNativeArray.java | 47 + .../react/bridge/ReadableNativeMap.java | 75 ++ .../facebook/react/bridge/ReadableType.java | 26 + .../facebook/react/bridge/SoftAssertions.java | 41 + .../facebook/react/bridge/UiThreadUtil.java | 56 + .../bridge/UnexpectedNativeTypeException.java | 25 + .../bridge/WebsocketJavaScriptExecutor.java | 183 +++ .../facebook/react/bridge/WritableArray.java | 24 + .../facebook/react/bridge/WritableMap.java | 26 + .../react/bridge/WritableNativeArray.java | 60 + .../react/bridge/WritableNativeMap.java | 68 + .../com/facebook/react/bridge/package_js.py | 14 + .../queue/CatalystQueueConfiguration.java | 90 ++ .../queue/CatalystQueueConfigurationSpec.java | 79 ++ .../bridge/queue/MessageQueueThread.java | 144 +++ .../queue/MessageQueueThreadHandler.java | 36 + .../bridge/queue/MessageQueueThreadSpec.java | 48 + .../react/bridge/queue/NativeRunnable.java | 29 + .../queue/QueueThreadExceptionHandler.java | 19 + .../com/facebook/react/common/LongArray.java | 78 ++ .../com/facebook/react/common/MapBuilder.java | 154 +++ .../facebook/react/common/ReactConstants.java | 15 + .../com/facebook/react/common/SetBuilder.java | 26 + .../facebook/react/common/ShakeDetector.java | 121 ++ .../facebook/react/common/SystemClock.java | 25 + .../common/annotations/VisibleForTesting.java | 17 + .../common/futures/SimpleSettableFuture.java | 62 + .../react/devsupport/AndroidManifest.xml | 10 + .../devsupport/DebugOverlayController.java | 54 + .../devsupport/DebugServerException.java | 74 ++ .../react/devsupport/DevInternalSettings.java | 70 ++ .../react/devsupport/DevOptionHandler.java | 25 + .../react/devsupport/DevServerHelper.java | 307 +++++ .../react/devsupport/DevSettingsActivity.java | 29 + .../react/devsupport/DevSupportManager.java | 629 ++++++++++ .../devsupport/ExceptionFormatterHelper.java | 75 ++ .../facebook/react/devsupport/FpsView.java | 102 ++ .../ReactInstanceDevCommandsHandler.java | 34 + .../react/devsupport/RedBoxDialog.java | 84 ++ .../modules/common/ModuleDataCleaner.java | 50 + .../core/DefaultHardwareBackBtnHandler.java | 25 + .../core/DeviceEventManagerModule.java | 67 + .../modules/core/ExceptionsManagerModule.java | 78 ++ .../react/modules/core/JSTimersExecution.java | 18 + .../modules/core/JavascriptException.java | 21 + .../facebook/react/modules/core/Timing.java | 204 +++ .../modules/debug/AnimationsDebugModule.java | 120 ++ .../modules/debug/DeveloperSettings.java | 26 + .../DidJSUpdateUiDuringFrameDetector.java | 175 +++ .../modules/debug/FpsDebugFrameCallback.java | 196 +++ .../react/modules/debug/SourceCodeModule.java | 54 + .../react/modules/fresco/FrescoModule.java | 76 ++ .../modules/network/NetworkingModule.java | 289 +++++ .../modules/network/OkHttpClientProvider.java | 36 + .../modules/network/RequestBodyUtil.java | 115 ++ .../storage/AsyncLocalStorageUtil.java | 147 +++ .../storage/AsyncStorageErrorUtil.java | 47 + .../modules/storage/AsyncStorageModule.java | 369 ++++++ .../storage/CatalystSQLiteOpenHelper.java | 53 + .../modules/systeminfo/AndroidInfoModule.java | 37 + .../react/modules/toast/ToastModule.java | 52 + .../react/shell/MainReactPackage.java | 73 ++ .../touch/CatalystInterceptingViewGroup.java | 34 + .../react/touch/JSResponderHandler.java | 77 ++ .../touch/OnInterceptTouchEventListener.java | 30 + .../react/uimanager/AccessibilityHelper.java | 103 ++ .../react/uimanager/AndroidManifest.xml | 6 + .../facebook/react/uimanager/AppRegistry.java | 20 + .../uimanager/BaseCSSPropertyApplicator.java | 146 +++ .../uimanager/BaseViewPropertyApplicator.java | 175 +++ .../react/uimanager/CSSColorUtil.java | 155 +++ .../uimanager/CatalystStylesDiffMap.java | 89 ++ .../react/uimanager/DisplayMetricsHolder.java | 29 + .../GuardedChoreographerFrameCallback.java | 43 + .../IllegalViewOperationException.java | 22 + .../uimanager/MeasureSpecAssertions.java | 29 + .../uimanager/NativeViewHierarchyManager.java | 607 +++++++++ .../NativeViewHierarchyOptimizer.java | 428 +++++++ .../uimanager/NoSuchNativeViewException.java | 21 + .../react/uimanager/OnLayoutEvent.java | 51 + .../facebook/react/uimanager/PixelUtil.java | 60 + .../react/uimanager/PointerEvents.java | 38 + .../react/uimanager/ReactChoreographer.java | 121 ++ .../react/uimanager/ReactCompoundView.java | 28 + .../ReactInvalidPropertyException.java | 18 + .../facebook/react/uimanager/ReactNative.java | 19 + .../uimanager/ReactPointerEventsView.java | 24 + .../react/uimanager/ReactShadowNode.java | 374 ++++++ .../facebook/react/uimanager/RootView.java | 24 + .../react/uimanager/RootViewManager.java | 30 + .../react/uimanager/RootViewUtil.java | 34 + .../react/uimanager/ShadowNodeRegistry.java | 72 ++ .../react/uimanager/SimpleViewManager.java | 36 + .../uimanager/SizeMonitoringFrameLayout.java | 56 + .../react/uimanager/ThemedReactContext.java | 50 + .../react/uimanager/TouchTargetHelper.java | 146 +++ .../react/uimanager/UIManagerModule.java | 837 +++++++++++++ .../uimanager/UIManagerModuleConstants.java | 157 +++ .../UIManagerModuleConstantsHelper.java | 100 ++ .../com/facebook/react/uimanager/UIProp.java | 52 + .../react/uimanager/UIViewOperationQueue.java | 631 ++++++++++ .../facebook/react/uimanager/ViewAtIndex.java | 33 + .../react/uimanager/ViewDefaults.java | 20 + .../react/uimanager/ViewGroupManager.java | 65 + .../facebook/react/uimanager/ViewManager.java | 216 ++++ .../react/uimanager/ViewManagerRegistry.java | 38 + .../facebook/react/uimanager/ViewProps.java | 112 ++ .../debug/DebugComponentOwnershipModule.java | 100 ++ .../NotThreadSafeUiManagerDebugListener.java | 32 + .../react/uimanager/events/Event.java | 83 ++ .../uimanager/events/EventDispatcher.java | 302 +++++ .../uimanager/events/NativeGestureUtil.java | 33 + .../uimanager/events/RCTEventEmitter.java | 24 + .../react/uimanager/events/TouchEvent.java | 98 ++ .../events/TouchEventCoalescingKeyHelper.java | 86 ++ .../uimanager/events/TouchEventType.java | 30 + .../react/uimanager/events/TouchesHelper.java | 99 ++ .../react/views/drawer/ReactDrawerLayout.java | 73 ++ .../drawer/ReactDrawerLayoutManager.java | 182 +++ .../drawer/events/DrawerClosedEvent.java | 40 + .../drawer/events/DrawerOpenedEvent.java | 40 + .../views/drawer/events/DrawerSlideEvent.java | 56 + .../events/DrawerStateChangedEvent.java | 53 + .../react/views/image/ImageResizeMode.java | 51 + .../react/views/image/ReactImageManager.java | 88 ++ .../react/views/image/ReactImageView.java | 259 ++++ .../progressbar/ProgressBarShadowNode.java | 86 ++ .../ReactProgressBarViewManager.java | 93 ++ .../views/scroll/OnScrollDispatchHelper.java | 44 + .../scroll/ReactHorizontalScrollView.java | 63 + .../ReactHorizontalScrollViewManager.java | 54 + .../react/views/scroll/ReactScrollView.java | 123 ++ .../scroll/ReactScrollViewCommandHelper.java | 68 + .../views/scroll/ReactScrollViewHelper.java | 41 + .../views/scroll/ReactScrollViewManager.java | 98 ++ .../react/views/scroll/ScrollEvent.java | 88 ++ .../react/views/switchview/ReactSwitch.java | 45 + .../views/switchview/ReactSwitchEvent.java | 57 + .../views/switchview/ReactSwitchManager.java | 119 ++ .../react/views/text/CustomStyleSpan.java | 114 ++ .../views/text/DefaultStyleValuesUtil.java | 65 + .../react/views/text/ReactRawTextManager.java | 48 + .../react/views/text/ReactTagSpan.java | 27 + .../react/views/text/ReactTextShadowNode.java | 394 ++++++ .../react/views/text/ReactTextView.java | 70 ++ .../views/text/ReactTextViewManager.java | 103 ++ .../text/ReactVirtualTextViewManager.java | 27 + .../react/views/textinput/ReactEditText.java | 275 +++++ .../textinput/ReactTextChangedEvent.java | 66 + .../textinput/ReactTextInputBlurEvent.java | 50 + .../ReactTextInputEndEditingEvent.java | 56 + .../views/textinput/ReactTextInputEvent.java | 72 ++ .../textinput/ReactTextInputFocusEvent.java | 50 + .../textinput/ReactTextInputManager.java | 445 +++++++ .../textinput/ReactTextInputShadowNode.java | 147 +++ .../ReactTextInputSubmitEditingEvent.java | 56 + .../views/textinput/ReactTextUpdate.java | 35 + .../views/toolbar/ReactToolbarManager.java | 234 ++++ .../toolbar/events/ToolbarClickEvent.java | 51 + .../facebook/react/views/view/ColorUtil.java | 55 + .../views/view/ReactClippingViewGroup.java | 63 + .../view/ReactClippingViewGroupHelper.java | 74 ++ .../react/views/view/ReactDrawableHelper.java | 94 ++ .../view/ReactViewBackgroundDrawable.java | 301 +++++ .../react/views/view/ReactViewGroup.java | 487 ++++++++ .../react/views/view/ReactViewManager.java | 222 ++++ .../com/facebook/soloader/ApkSoSource.java | 171 +++ .../facebook/soloader/DirectorySoSource.java | 76 ++ .../java/com/facebook/soloader/Elf32_Dyn.java | 14 + .../com/facebook/soloader/Elf32_Ehdr.java | 26 + .../com/facebook/soloader/Elf32_Phdr.java | 20 + .../com/facebook/soloader/Elf32_Shdr.java | 22 + .../java/com/facebook/soloader/Elf64_Dyn.java | 14 + .../com/facebook/soloader/Elf64_Ehdr.java | 26 + .../com/facebook/soloader/Elf64_Phdr.java | 20 + .../com/facebook/soloader/Elf64_Shdr.java | 22 + .../com/facebook/soloader/ExoSoSource.java | 177 +++ .../com/facebook/soloader/FileLocker.java | 48 + .../java/com/facebook/soloader/MinElf.java | 282 +++++ .../com/facebook/soloader/NativeLibrary.java | 93 ++ .../com/facebook/soloader/NoopSoSource.java | 28 + .../java/com/facebook/soloader/SoLoader.java | 237 ++++ .../java/com/facebook/soloader/SoSource.java | 57 + .../java/com/facebook/soloader/SysUtil.java | 205 +++ .../java/com/facebook/soloader/genstructs.sh | 35 + .../java/com/facebook/soloader/soloader.pro | 6 + .../java/com/facebook/systrace/Systrace.java | 31 + .../facebook/systrace/SystraceMessage.java | 69 ++ ReactAndroid/src/main/jni/Application.mk | 14 + .../src/main/jni/first-party/fb/Android.mk | 30 + .../src/main/jni/first-party/fb/Countable.h | 47 + .../main/jni/first-party/fb/ProgramLocation.h | 50 + .../src/main/jni/first-party/fb/RefPtr.h | 274 ++++ .../jni/first-party/fb/StaticInitialized.h | 40 + .../src/main/jni/first-party/fb/ThreadLocal.h | 118 ++ .../src/main/jni/first-party/fb/assert.cpp | 41 + .../jni/first-party/fb/include/fb/assert.h | 34 + .../main/jni/first-party/fb/include/fb/log.h | 361 ++++++ .../src/main/jni/first-party/fb/log.cpp | 100 ++ .../src/main/jni/first-party/fb/noncopyable.h | 21 + .../src/main/jni/first-party/fb/nonmovable.h | 21 + .../src/main/jni/first-party/jni/ALog.h | 83 ++ .../src/main/jni/first-party/jni/Android.mk | 35 + .../main/jni/first-party/jni/Countable.cpp | 69 ++ .../src/main/jni/first-party/jni/Countable.h | 33 + .../src/main/jni/first-party/jni/Doxyfile | 18 + .../main/jni/first-party/jni/Environment.cpp | 89 ++ .../main/jni/first-party/jni/Environment.h | 61 + .../jni/first-party/jni/GlobalReference.h | 91 ++ .../main/jni/first-party/jni/LocalReference.h | 37 + .../main/jni/first-party/jni/LocalString.cpp | 242 ++++ .../main/jni/first-party/jni/LocalString.h | 61 + .../src/main/jni/first-party/jni/OnLoad.cpp | 23 + .../main/jni/first-party/jni/Registration.h | 27 + .../jni/first-party/jni/WeakReference.cpp | 43 + .../main/jni/first-party/jni/WeakReference.h | 53 + .../src/main/jni/first-party/jni/fbjni.cpp | 203 +++ .../src/main/jni/first-party/jni/fbjni.h | 23 + .../main/jni/first-party/jni/fbjni/Common.h | 68 + .../first-party/jni/fbjni/CoreClasses-inl.h | 451 +++++++ .../jni/first-party/jni/fbjni/CoreClasses.h | 488 ++++++++ .../jni/first-party/jni/fbjni/Exceptions.cpp | 399 ++++++ .../jni/first-party/jni/fbjni/Exceptions.h | 130 ++ .../main/jni/first-party/jni/fbjni/Hybrid.cpp | 76 ++ .../main/jni/first-party/jni/fbjni/Hybrid.h | 251 ++++ .../main/jni/first-party/jni/fbjni/Meta-inl.h | 342 +++++ .../src/main/jni/first-party/jni/fbjni/Meta.h | 302 +++++ .../jni/fbjni/ReferenceAllocators-inl.h | 123 ++ .../jni/fbjni/ReferenceAllocators.h | 60 + .../first-party/jni/fbjni/References-inl.h | 370 ++++++ .../jni/first-party/jni/fbjni/References.cpp | 41 + .../jni/first-party/jni/fbjni/References.h | 506 ++++++++ .../first-party/jni/fbjni/Registration-inl.h | 206 +++ .../jni/first-party/jni/fbjni/Registration.h | 87 ++ .../jni/first-party/jni/fbjni/TypeTraits.h | 149 +++ .../main/jni/first-party/jni/jni_helpers.cpp | 195 +++ .../main/jni/first-party/jni/jni_helpers.h | 137 ++ ReactAndroid/src/main/jni/react/Android.mk | 32 + ReactAndroid/src/main/jni/react/Bridge.cpp | 109 ++ ReactAndroid/src/main/jni/react/Bridge.h | 50 + ReactAndroid/src/main/jni/react/Executor.h | 47 + .../src/main/jni/react/JSCExecutor.cpp | 154 +++ ReactAndroid/src/main/jni/react/JSCExecutor.h | 41 + .../src/main/jni/react/JSCHelpers.cpp | 22 + ReactAndroid/src/main/jni/react/JSCHelpers.h | 16 + .../src/main/jni/react/JSCLegacyProfiler.cpp | 63 + .../src/main/jni/react/JSCLegacyProfiler.h | 15 + .../src/main/jni/react/JSCPerfLogging.cpp | 212 ++++ .../src/main/jni/react/JSCPerfLogging.h | 11 + .../src/main/jni/react/JSCTracing.cpp | 327 +++++ ReactAndroid/src/main/jni/react/JSCTracing.h | 11 + .../src/main/jni/react/MethodCall.cpp | 61 + ReactAndroid/src/main/jni/react/MethodCall.h | 27 + ReactAndroid/src/main/jni/react/Value.cpp | 53 + ReactAndroid/src/main/jni/react/Value.h | 138 +++ .../src/main/jni/react/jni/Android.mk | 30 + .../src/main/jni/react/jni/JSLoader.cpp | 60 + .../src/main/jni/react/jni/JSLoader.h | 21 + .../src/main/jni/react/jni/NativeArray.cpp | 41 + .../src/main/jni/react/jni/NativeArray.h | 36 + .../src/main/jni/react/jni/OnLoad.cpp | 731 +++++++++++ .../src/main/jni/react/jni/ProxyExecutor.cpp | 64 + .../src/main/jni/react/jni/ProxyExecutor.h | 47 + .../src/main/jni/react/perftests/OnLoad.cpp | 65 + .../src/main/jni/react/test/.gitignore | 2 + .../src/main/jni/react/test/Android.mk | 27 + .../src/main/jni/react/test/jni/Android.mk | 1 + .../main/jni/react/test/jni/Application.mk | 6 + .../src/main/jni/react/test/jscexecutor.cpp | 223 ++++ .../src/main/jni/react/test/jsclogging.cpp | 44 + .../src/main/jni/react/test/methodcall.cpp | 115 ++ ReactAndroid/src/main/jni/react/test/run | 19 + .../src/main/jni/react/test/value.cpp | 38 + .../src/main/jni/third-party/boost/Android.mk | 11 + .../third-party/double-conversion/Android.mk | 23 + .../src/main/jni/third-party/folly/Android.mk | 41 + .../src/main/jni/third-party/glog/Android.mk | 32 + .../src/main/jni/third-party/glog/config.h | 179 +++ .../src/main/jni/third-party/jsc/Android.mk | 6 + .../devsupport/anim/catalyst_push_up_in.xml | 13 + .../devsupport/anim/catalyst_push_up_out.xml | 13 + .../main/res/devsupport/layout/fps_view.xml | 18 + .../res/devsupport/layout/redbox_view.xml | 54 + .../main/res/devsupport/values-cs/strings.xml | 16 + .../main/res/devsupport/values-da/strings.xml | 16 + .../main/res/devsupport/values-de/strings.xml | 16 + .../main/res/devsupport/values-el/strings.xml | 16 + .../res/devsupport/values-en-rGB/strings.xml | 16 + .../res/devsupport/values-es-rES/strings.xml | 16 + .../main/res/devsupport/values-es/strings.xml | 16 + .../res/devsupport/values-fb-rLL/strings.xml | 16 + .../main/res/devsupport/values-fb/strings.xml | 16 + .../main/res/devsupport/values-fi/strings.xml | 16 + .../main/res/devsupport/values-fr/strings.xml | 16 + .../main/res/devsupport/values-hu/strings.xml | 16 + .../main/res/devsupport/values-in/strings.xml | 16 + .../main/res/devsupport/values-it/strings.xml | 16 + .../main/res/devsupport/values-ja/strings.xml | 16 + .../main/res/devsupport/values-ko/strings.xml | 16 + .../main/res/devsupport/values-nb/strings.xml | 16 + .../main/res/devsupport/values-nl/strings.xml | 16 + .../main/res/devsupport/values-pl/strings.xml | 16 + .../res/devsupport/values-pt-rPT/strings.xml | 16 + .../main/res/devsupport/values-pt/strings.xml | 16 + .../main/res/devsupport/values-ro/strings.xml | 16 + .../main/res/devsupport/values-ru/strings.xml | 16 + .../main/res/devsupport/values-sv/strings.xml | 16 + .../main/res/devsupport/values-th/strings.xml | 16 + .../main/res/devsupport/values-tr/strings.xml | 16 + .../main/res/devsupport/values-vi/strings.xml | 16 + .../res/devsupport/values-zh-rCN/strings.xml | 16 + .../res/devsupport/values-zh-rHK/strings.xml | 16 + .../res/devsupport/values-zh-rTW/strings.xml | 16 + .../src/main/res/devsupport/values/colors.xml | 4 + .../main/res/devsupport/values/strings.xml | 16 + .../src/main/res/devsupport/values/styles.xml | 16 + .../main/res/devsupport/xml/preferences.xml | 40 + .../src/main/res/shell/values/styles.xml | 13 + build.gradle | 22 + docs/Accessibility.md | 113 +- docs/Animations.md | 2 +- docs/Debugging.md | 24 +- docs/DevelopmentSetupAndroid.md | 37 + docs/GettingStarted.md | 54 +- docs/Image.md | 22 +- docs/JavaScriptEnvironment.md | 2 +- docs/KnownIssues.md | 64 + docs/NativeComponentsAndroid.md | 179 +++ docs/NativeComponentsIOS.md | 2 +- docs/NativeModulesAndroid.md | 247 ++++ docs/NativeModulesIOS.md | 2 +- docs/NavigatorComparison.md | 2 +- docs/RunningOnDeviceAndroid.md | 38 + ...nningOnDevice.md => RunningOnDeviceIOS.md} | 2 +- docs/Testing.md | 12 +- docs/Tutorial.md | 32 +- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 52141 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 +++ gradlew.bat | 90 ++ local-cli/__tests__/generator-android-test.js | 77 ++ local-cli/__tests__/generator-test.js | 24 +- local-cli/cli.js | 23 +- local-cli/generate-android.js | 24 + local-cli/generator-android/index.js | 64 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../bin/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 52266 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + .../generator-android/templates/bin/gradlew | 164 +++ .../templates/bin/gradlew.bat | 90 ++ .../templates/package/MainActivity.java | 69 ++ .../templates/src/app/build.gradle | 29 + .../templates/src/app/proguard-rules.pro | 17 + .../src/app/src/main/AndroidManifest.xml | 22 + .../src/app/src/main/res/values/strings.xml | 3 + .../src/app/src/main/res/values/styles.xml | 8 + .../templates/src/build.gradle | 20 + .../templates/src/gradle.properties | 20 + .../templates/src/settings.gradle | 3 + local-cli/generator-ios/index.js | 2 +- local-cli/generator/index.js | 43 +- .../generator/templates/index.android.js | 52 + local-cli/init.js | 5 +- local-cli/run-android.js | 73 ++ local-cli/run-packager.js | 18 + package.json | 2 +- react-native-cli/package.json | 3 + react-native-gradle/.gitignore | 5 + .../.idea/codeStyleSettings.xml | 110 ++ react-native-gradle/README.md | 68 + react-native-gradle/build.gradle | 18 + react-native-gradle/settings.gradle | 1 + .../facebook/react/AbstractPackageJsTask.java | 232 ++++ .../facebook/react/PackageDebugJsTask.java | 20 + .../facebook/react/PackageReleaseJsTask.java | 20 + .../com/facebook/react/PackagerParams.java | 96 ++ .../facebook/react/ReactGradleExtension.java | 95 ++ .../com/facebook/react/ReactGradlePlugin.java | 39 + .../com.facebook.react.properties | 1 + .../facebook/react/PackageJSTasksTest.java | 85 ++ .../facebook/react/ReactGradlePluginTest.java | 26 + settings.gradle | 3 + website/publish-android.sh | 36 + website/server/extractDocs.js | 40 +- website/src/react-native/img/AndroidSDK1.png | Bin 0 -> 389672 bytes website/src/react-native/img/AndroidSDK2.png | Bin 0 -> 374852 bytes website/src/react-native/img/CreateAVD.png | Bin 0 -> 86956 bytes .../src/react-native/img/TutorialFinal2.png | Bin 0 -> 42030 bytes .../src/react-native/img/TutorialMock2.png | Bin 0 -> 8606 bytes .../img/TutorialSingleFetched2.png | Bin 0 -> 9399 bytes .../react-native/img/TutorialStyledMock2.png | Bin 0 -> 8797 bytes website/src/react-native/index.js | 148 ++- 571 files changed, 44550 insertions(+), 116 deletions(-) create mode 100644 Examples/Movies/MoviesApp.android.js create mode 100644 Examples/Movies/SearchBar.android.js create mode 100644 Examples/Movies/android/app/build.gradle create mode 100644 Examples/Movies/android/app/proguard-rules.pro create mode 100644 Examples/Movies/android/app/src/main/AndroidManifest.xml create mode 100644 Examples/Movies/android/app/src/main/java/com/facebook/react/movies/MoviesActivity.java create mode 100644 Examples/Movies/android/app/src/main/res/drawable-hdpi/android_back_white.png create mode 100755 Examples/Movies/android/app/src/main/res/drawable-hdpi/android_search_white.png create mode 100644 Examples/Movies/android/app/src/main/res/drawable-mdpi/android_back_white.png create mode 100755 Examples/Movies/android/app/src/main/res/drawable-mdpi/android_search_white.png create mode 100644 Examples/Movies/android/app/src/main/res/drawable-xhdpi/android_back_white.png create mode 100755 Examples/Movies/android/app/src/main/res/drawable-xhdpi/android_search_white.png create mode 100644 Examples/Movies/android/app/src/main/res/drawable-xxhdpi/android_back_white.png create mode 100755 Examples/Movies/android/app/src/main/res/drawable-xxhdpi/android_search_white.png create mode 100644 Examples/Movies/android/app/src/main/res/drawable/rotten_tomatoes_icon.png create mode 100644 Examples/Movies/android/app/src/main/res/layout/activity_main.xml create mode 100644 Examples/Movies/android/app/src/main/res/values/strings.xml create mode 100644 Examples/Movies/android/app/src/main/res/values/styles.xml create mode 100644 Examples/SampleApp/android/app/build.gradle create mode 100644 Examples/SampleApp/android/app/proguard-rules.pro create mode 100644 Examples/SampleApp/android/app/src/main/AndroidManifest.xml create mode 100644 Examples/SampleApp/android/app/src/main/java/com/facebook/react/sample/MainActivity.java create mode 100644 Examples/SampleApp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 Examples/SampleApp/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 Examples/SampleApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 Examples/SampleApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 Examples/SampleApp/android/app/src/main/res/values/strings.xml create mode 100644 Examples/SampleApp/android/app/src/main/res/values/styles.xml create mode 100644 Examples/SampleApp/index.android.js create mode 100644 Examples/UIExplorer/AccessibilityAndroidExample.android.js create mode 100644 Examples/UIExplorer/ProgressBarAndroidExample.android.js create mode 100644 Examples/UIExplorer/SwitchAndroidExample.android.js create mode 100644 Examples/UIExplorer/TextExample.android.js create mode 100644 Examples/UIExplorer/TextInputExample.android.js create mode 100644 Examples/UIExplorer/ToastAndroidExample.android.js create mode 100644 Examples/UIExplorer/ToolbarAndroidExample.android.js create mode 100644 Examples/UIExplorer/UIExplorerList.android.js create mode 100644 Examples/UIExplorer/XHRExample.android.js create mode 100644 Examples/UIExplorer/android/app/build.gradle create mode 100644 Examples/UIExplorer/android/app/proguard-rules.pro create mode 100644 Examples/UIExplorer/android/app/src/main/AndroidManifest.xml create mode 100644 Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/ic_create_black_48dp.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/ic_menu_black_24dp.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/ic_settings_black_48dp.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/launcher_icon.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/uie_comment_highlighted.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/uie_comment_normal.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/uie_thumb_normal.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/uie_thumb_selected.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/layout/activity_main.xml create mode 100644 Examples/UIExplorer/android/app/src/main/res/values/strings.xml create mode 100644 Examples/UIExplorer/android/app/src/main/res/values/styles.xml create mode 100644 Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.android.js create mode 100644 Libraries/Components/DatePicker/DatePickerIOS.android.js create mode 100644 Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js create mode 100644 Libraries/Components/Navigation/NavigatorIOS.android.js create mode 100644 Libraries/Components/Navigator/NavigatorBreadcrumbNavigationBarStyles.android.js create mode 100644 Libraries/Components/Navigator/NavigatorNavigationBarStyles.android.js create mode 100644 Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js create mode 100644 Libraries/Components/SliderIOS/SliderIOS.android.js create mode 100644 Libraries/Components/StatusBar/StatusBarIOS.android.js create mode 100644 Libraries/Components/SwitchAndroid/SwitchAndroid.android.js create mode 100644 Libraries/Components/ToastAndroid/ToastAndroid.android.js create mode 100644 Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js create mode 100644 Libraries/Components/Touchable/TouchableNativeFeedback.android.js create mode 100644 Libraries/Image/Image.android.js create mode 100644 Libraries/Network/RCTNetworking.android.js create mode 100644 Libraries/Network/XMLHttpRequest.android.js create mode 100644 Libraries/ReactIOS/renderApplication.android.js create mode 100644 Libraries/Storage/AsyncStorage.android.js create mode 100644 Libraries/Utilities/BackAndroid.android.js create mode 100644 Libraries/Utilities/Platform.android.js create mode 100644 Libraries/vendor/react/vendor/core/ExecutionEnvironment.android.js create mode 100644 ReactAndroid/DevExperience.md create mode 100644 ReactAndroid/README.md create mode 100644 ReactAndroid/build.gradle create mode 100644 ReactAndroid/gradle.properties create mode 100644 ReactAndroid/libs/infer-annotations-1.5.jar create mode 100644 ReactAndroid/release.gradle create mode 100644 ReactAndroid/src/main/AndroidManifest.xml create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSAlign.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSConstants.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSDirection.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSFlexDirection.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSJustify.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayout.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayoutContext.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSPositionType.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSStyle.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSWrap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CachedCSSLayout.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/FloatUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/MeasureOutput.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/README create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/Spacing.java create mode 100755 ReactAndroid/src/main/java/com/facebook/csslayout/syncFromGithub.sh create mode 100644 ReactAndroid/src/main/java/com/facebook/jni/Countable.java create mode 100644 ReactAndroid/src/main/java/com/facebook/jni/CppException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/jni/CppSystemErrorException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/jni/HybridData.java create mode 100644 ReactAndroid/src/main/java/com/facebook/jni/Prerequisites.java create mode 100644 ReactAndroid/src/main/java/com/facebook/jni/UnknownCppException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/proguard/annotations/DoNotStrip.java create mode 100644 ReactAndroid/src/main/java/com/facebook/proguard/annotations/KeepGettersAndSetters.java create mode 100644 ReactAndroid/src/main/java/com/facebook/proguard/annotations/proguard_annotations.pro create mode 100644 ReactAndroid/src/main/java/com/facebook/react/CompositeReactPackage.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/LifecycleState.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/ReactPackage.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/AbstractFloatPairPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/AbstractSingleFloatProperyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/Animation.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/AnimationListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/AnimationPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/AnimationRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/ImmediateAnimation.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/NoopAnimationPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/OpacityAnimationPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/PositionAnimationPairPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/RotationAnimationPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/ScaleXAnimationPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/ScaleXYAnimationPairPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/ScaleYAnimationPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/AssertionException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/Callback.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/CallbackImpl.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/GuardedAsyncTask.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/InvalidIteratorException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationCausedNativeException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationIllegalArgumentException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JSDebuggerWebSocketClient.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistration.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModulesConfig.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JsonGeneratorHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/LifecycleEventListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArgumentsParseException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleCallExceptionHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NoSuchKeyException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NotThreadSafeBridgeIdleDebugListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ObjectAlreadyConsumedException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/OnBatchCompleteListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReactApplicationContext.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReactCallback.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContextBaseJavaModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReactMethod.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableArray.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMapKeySeyIterator.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableType.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/SoftAssertions.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/UiThreadUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/UnexpectedNativeTypeException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/WebsocketJavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/WritableArray.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/package_js.py create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfiguration.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfigurationSpec.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadSpec.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/NativeRunnable.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/QueueThreadExceptionHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/LongArray.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/MapBuilder.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/ReactConstants.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/SetBuilder.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/ShakeDetector.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/SystemClock.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/annotations/VisibleForTesting.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/AndroidManifest.xml create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugOverlayController.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugServerException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DevOptionHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSettingsActivity.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/ExceptionFormatterHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/FpsView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevCommandsHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/common/ModuleDataCleaner.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/DefaultHardwareBackBtnHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/JavascriptException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/debug/AnimationsDebugModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/debug/DeveloperSettings.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/debug/FpsDebugFrameCallback.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageErrorUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/CatalystSQLiteOpenHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/toast/ToastModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/touch/CatalystInterceptingViewGroup.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/touch/JSResponderHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/touch/OnInterceptTouchEventListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/AndroidManifest.xml create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/AppRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseCSSPropertyApplicator.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewPropertyApplicator.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/CSSColorUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/CatalystStylesDiffMap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/GuardedChoreographerFrameCallback.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/MeasureSpecAssertions.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/NoSuchNativeViewException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/OnLayoutEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/PointerEvents.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactCompoundView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactInvalidPropertyException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactNative.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactPointerEventsView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ShadowNodeRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/SimpleViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/SizeMonitoringFrameLayout.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIProp.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewAtIndex.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/DebugComponentOwnershipModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/NotThreadSafeUiManagerDebugListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/NativeGestureUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTEventEmitter.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventType.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerClosedEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerOpenedEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerSlideEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerStateChangedEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarShadowNode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/OnScrollDispatchHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/DefaultStyleValuesUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTagSpan.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEndEditingEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputSubmitEditingEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextUpdate.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/toolbar/events/ToolbarClickEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroup.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroupHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/ApkSoSource.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/DirectorySoSource.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Dyn.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Ehdr.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Phdr.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Shdr.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Dyn.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Ehdr.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Phdr.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Shdr.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/ExoSoSource.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/FileLocker.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/MinElf.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/NativeLibrary.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/NoopSoSource.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/SoLoader.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/SoSource.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/SysUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/genstructs.sh create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/soloader.pro create mode 100644 ReactAndroid/src/main/java/com/facebook/systrace/Systrace.java create mode 100644 ReactAndroid/src/main/java/com/facebook/systrace/SystraceMessage.java create mode 100644 ReactAndroid/src/main/jni/Application.mk create mode 100644 ReactAndroid/src/main/jni/first-party/fb/Android.mk create mode 100644 ReactAndroid/src/main/jni/first-party/fb/Countable.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/ProgramLocation.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/RefPtr.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/StaticInitialized.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/ThreadLocal.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/assert.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/fb/include/fb/assert.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/include/fb/log.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/log.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/fb/noncopyable.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/nonmovable.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/ALog.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Android.mk create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Countable.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Countable.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Doxyfile create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Environment.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Environment.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/GlobalReference.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/LocalReference.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/LocalString.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/LocalString.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/OnLoad.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Registration.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/WeakReference.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/WeakReference.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Common.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses-inl.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta-inl.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators-inl.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/References-inl.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/References.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/References.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration-inl.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/TypeTraits.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/jni_helpers.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/jni_helpers.h create mode 100644 ReactAndroid/src/main/jni/react/Android.mk create mode 100644 ReactAndroid/src/main/jni/react/Bridge.cpp create mode 100644 ReactAndroid/src/main/jni/react/Bridge.h create mode 100644 ReactAndroid/src/main/jni/react/Executor.h create mode 100644 ReactAndroid/src/main/jni/react/JSCExecutor.cpp create mode 100644 ReactAndroid/src/main/jni/react/JSCExecutor.h create mode 100644 ReactAndroid/src/main/jni/react/JSCHelpers.cpp create mode 100644 ReactAndroid/src/main/jni/react/JSCHelpers.h create mode 100644 ReactAndroid/src/main/jni/react/JSCLegacyProfiler.cpp create mode 100644 ReactAndroid/src/main/jni/react/JSCLegacyProfiler.h create mode 100644 ReactAndroid/src/main/jni/react/JSCPerfLogging.cpp create mode 100644 ReactAndroid/src/main/jni/react/JSCPerfLogging.h create mode 100644 ReactAndroid/src/main/jni/react/JSCTracing.cpp create mode 100644 ReactAndroid/src/main/jni/react/JSCTracing.h create mode 100644 ReactAndroid/src/main/jni/react/MethodCall.cpp create mode 100644 ReactAndroid/src/main/jni/react/MethodCall.h create mode 100644 ReactAndroid/src/main/jni/react/Value.cpp create mode 100644 ReactAndroid/src/main/jni/react/Value.h create mode 100644 ReactAndroid/src/main/jni/react/jni/Android.mk create mode 100644 ReactAndroid/src/main/jni/react/jni/JSLoader.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/JSLoader.h create mode 100644 ReactAndroid/src/main/jni/react/jni/NativeArray.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/NativeArray.h create mode 100644 ReactAndroid/src/main/jni/react/jni/OnLoad.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/ProxyExecutor.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/ProxyExecutor.h create mode 100644 ReactAndroid/src/main/jni/react/perftests/OnLoad.cpp create mode 100644 ReactAndroid/src/main/jni/react/test/.gitignore create mode 100644 ReactAndroid/src/main/jni/react/test/Android.mk create mode 100644 ReactAndroid/src/main/jni/react/test/jni/Android.mk create mode 100644 ReactAndroid/src/main/jni/react/test/jni/Application.mk create mode 100644 ReactAndroid/src/main/jni/react/test/jscexecutor.cpp create mode 100644 ReactAndroid/src/main/jni/react/test/jsclogging.cpp create mode 100644 ReactAndroid/src/main/jni/react/test/methodcall.cpp create mode 100755 ReactAndroid/src/main/jni/react/test/run create mode 100644 ReactAndroid/src/main/jni/react/test/value.cpp create mode 100644 ReactAndroid/src/main/jni/third-party/boost/Android.mk create mode 100644 ReactAndroid/src/main/jni/third-party/double-conversion/Android.mk create mode 100644 ReactAndroid/src/main/jni/third-party/folly/Android.mk create mode 100644 ReactAndroid/src/main/jni/third-party/glog/Android.mk create mode 100644 ReactAndroid/src/main/jni/third-party/glog/config.h create mode 100644 ReactAndroid/src/main/jni/third-party/jsc/Android.mk create mode 100644 ReactAndroid/src/main/res/devsupport/anim/catalyst_push_up_in.xml create mode 100644 ReactAndroid/src/main/res/devsupport/anim/catalyst_push_up_out.xml create mode 100644 ReactAndroid/src/main/res/devsupport/layout/fps_view.xml create mode 100644 ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-cs/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-da/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-de/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-el/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-en-rGB/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-es-rES/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-es/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-fb-rLL/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-fb/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-fi/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-fr/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-hu/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-in/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-it/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-ja/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-ko/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-nb/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-nl/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-pl/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-pt-rPT/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-pt/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-ro/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-ru/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-sv/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-th/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-tr/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-vi/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-zh-rCN/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-zh-rHK/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-zh-rTW/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values/colors.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values/styles.xml create mode 100644 ReactAndroid/src/main/res/devsupport/xml/preferences.xml create mode 100644 ReactAndroid/src/main/res/shell/values/styles.xml create mode 100644 build.gradle create mode 100644 docs/DevelopmentSetupAndroid.md create mode 100644 docs/KnownIssues.md create mode 100644 docs/NativeComponentsAndroid.md create mode 100644 docs/NativeModulesAndroid.md create mode 100644 docs/RunningOnDeviceAndroid.md rename docs/{RunningOnDevice.md => RunningOnDeviceIOS.md} (98%) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 local-cli/__tests__/generator-android-test.js create mode 100644 local-cli/generate-android.js create mode 100644 local-cli/generator-android/index.js create mode 100644 local-cli/generator-android/templates/bin/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 local-cli/generator-android/templates/bin/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 local-cli/generator-android/templates/bin/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 local-cli/generator-android/templates/bin/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 local-cli/generator-android/templates/bin/gradle/wrapper/gradle-wrapper.jar create mode 100644 local-cli/generator-android/templates/bin/gradle/wrapper/gradle-wrapper.properties create mode 100755 local-cli/generator-android/templates/bin/gradlew create mode 100644 local-cli/generator-android/templates/bin/gradlew.bat create mode 100644 local-cli/generator-android/templates/package/MainActivity.java create mode 100644 local-cli/generator-android/templates/src/app/build.gradle create mode 100644 local-cli/generator-android/templates/src/app/proguard-rules.pro create mode 100644 local-cli/generator-android/templates/src/app/src/main/AndroidManifest.xml create mode 100644 local-cli/generator-android/templates/src/app/src/main/res/values/strings.xml create mode 100644 local-cli/generator-android/templates/src/app/src/main/res/values/styles.xml create mode 100644 local-cli/generator-android/templates/src/build.gradle create mode 100644 local-cli/generator-android/templates/src/gradle.properties create mode 100644 local-cli/generator-android/templates/src/settings.gradle create mode 100644 local-cli/generator/templates/index.android.js create mode 100644 local-cli/run-android.js create mode 100644 local-cli/run-packager.js create mode 100644 react-native-gradle/.gitignore create mode 100644 react-native-gradle/.idea/codeStyleSettings.xml create mode 100644 react-native-gradle/README.md create mode 100644 react-native-gradle/build.gradle create mode 100644 react-native-gradle/settings.gradle create mode 100644 react-native-gradle/src/main/java/com/facebook/react/AbstractPackageJsTask.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/PackageDebugJsTask.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/PackageReleaseJsTask.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/PackagerParams.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/ReactGradleExtension.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/ReactGradlePlugin.java create mode 100644 react-native-gradle/src/main/resources/META-INF/gradle-plugins/com.facebook.react.properties create mode 100644 react-native-gradle/src/test/java/com/facebook/react/PackageJSTasksTest.java create mode 100644 react-native-gradle/src/test/java/com/facebook/react/ReactGradlePluginTest.java create mode 100644 settings.gradle create mode 100755 website/publish-android.sh create mode 100644 website/src/react-native/img/AndroidSDK1.png create mode 100644 website/src/react-native/img/AndroidSDK2.png create mode 100644 website/src/react-native/img/CreateAVD.png create mode 100644 website/src/react-native/img/TutorialFinal2.png create mode 100644 website/src/react-native/img/TutorialMock2.png create mode 100644 website/src/react-native/img/TutorialSingleFetched2.png create mode 100644 website/src/react-native/img/TutorialStyledMock2.png diff --git a/.gitignore b/.gitignore index 5326cd9cab..60dbe841d0 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,12 @@ project.xcworkspace # OS X .DS_Store +# Android/IJ +.idea +.gradle +local.properties +*.iml + # Node node_modules *.log diff --git a/Examples/Movies/Movies/AppDelegate.m b/Examples/Movies/Movies/AppDelegate.m index 4d322023f0..680d5eba4c 100644 --- a/Examples/Movies/Movies/AppDelegate.m +++ b/Examples/Movies/Movies/AppDelegate.m @@ -37,14 +37,14 @@ * on the same Wi-Fi network. */ - jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/Movies/MoviesApp.ios.bundle?platform=ios&dev=true"]; + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/Movies/MoviesApp.ios.includeRequire.runModule.bundle"]; /** * OPTION 2 * Load from pre-bundled file on disk. To re-generate the static bundle, `cd` * to your Xcode project folder in the terminal, and run * - * $ curl 'http://localhost:8081/Examples/Movies/MoviesApp.includeRequire.runModule.bundle' -o main.jsbundle + * $ curl 'http://localhost:8081/Examples/Movies/MoviesApp.ios.includeRequire.runModule.bundle' -o main.jsbundle * * then add the `main.jsbundle` file to your project and uncomment this line: */ diff --git a/Examples/Movies/MoviesApp.android.js b/Examples/Movies/MoviesApp.android.js new file mode 100644 index 0000000000..5dfb57fd2f --- /dev/null +++ b/Examples/Movies/MoviesApp.android.js @@ -0,0 +1,94 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule MoviesApp + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + AppRegistry, + BackAndroid, + Navigator, + StyleSheet, + ToolbarAndroid, + View, +} = React; + +var MovieScreen = require('./MovieScreen'); +var SearchScreen = require('./SearchScreen'); + +var _navigator; +BackAndroid.addEventListener('hardwareBackPress', () => { + if (_navigator && _navigator.getCurrentRoutes().length > 1) { + _navigator.pop(); + return true; + } + return false; +}); + +var RouteMapper = function(route, navigationOperations, onComponentRef) { + _navigator = navigationOperations; + if (route.name === 'search') { + return ( + + ); + } else if (route.name === 'movie') { + return ( + + + + + ); + } +}; + +var MoviesApp = React.createClass({ + render: function() { + var initialRoute = {name: 'search'}; + return ( + Navigator.SceneConfigs.FadeAndroid} + renderScene={RouteMapper} + /> + ); + } +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: 'white', + }, + toolbar: { + backgroundColor: '#a9a9a9', + height: 56, + }, +}); + +AppRegistry.registerComponent('MoviesApp', () => MoviesApp); + +module.exports = MoviesApp; diff --git a/Examples/Movies/SearchBar.android.js b/Examples/Movies/SearchBar.android.js new file mode 100644 index 0000000000..2e12ff4880 --- /dev/null +++ b/Examples/Movies/SearchBar.android.js @@ -0,0 +1,104 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule SearchBar + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Image, + Platform, + ProgressBarAndroid, + TextInput, + StyleSheet, + TouchableNativeFeedback, + View, +} = React; + +var IS_RIPPLE_EFFECT_SUPPORTED = Platform.Version >= 21; + +var SearchBar = React.createClass({ + render: function() { + var loadingView; + if (this.props.isLoading) { + loadingView = ( + + ); + } else { + loadingView = ; + } + var background = IS_RIPPLE_EFFECT_SUPPORTED ? + TouchableNativeFeedback.SelectableBackgroundBorderless() : + TouchableNativeFeedback.SelectableBackground(); + return ( + + this.refs.input && this.refs.input.focus()}> + + + + + + {loadingView} + + ); + } +}); + +var styles = StyleSheet.create({ + searchBar: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#a9a9a9', + height: 56, + }, + searchBarInput: { + flex: 1, + fontSize: 20, + fontWeight: 'bold', + color: 'white', + height: 50, + padding: 0, + backgroundColor: 'transparent' + }, + spinner: { + width: 30, + height: 30, + }, + icon: { + width: 24, + height: 24, + marginHorizontal: 8, + }, +}); + +module.exports = SearchBar; diff --git a/Examples/Movies/android/app/build.gradle b/Examples/Movies/android/app/build.gradle new file mode 100644 index 0000000000..3a8c184e62 --- /dev/null +++ b/Examples/Movies/android/app/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 22 + buildToolsVersion "23.0.1" + + defaultConfig { + applicationId "com.facebook.react.movies" + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + ndk { + abiFilters "armeabi-v7a", "x86" + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:22.2.0' + + // Depend on pre-built React Native + compile 'com.facebook.react:react-native:0.11.+' + + // Depend on React Native source. + // This is useful for testing your changes when working on React Native. + // compile project(':ReactAndroid') +} diff --git a/Examples/Movies/android/app/proguard-rules.pro b/Examples/Movies/android/app/proguard-rules.pro new file mode 100644 index 0000000000..a92fa177ee --- /dev/null +++ b/Examples/Movies/android/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/Examples/Movies/android/app/src/main/AndroidManifest.xml b/Examples/Movies/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2123d078fd --- /dev/null +++ b/Examples/Movies/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/Examples/Movies/android/app/src/main/java/com/facebook/react/movies/MoviesActivity.java b/Examples/Movies/android/app/src/main/java/com/facebook/react/movies/MoviesActivity.java new file mode 100644 index 0000000000..6499cea1f9 --- /dev/null +++ b/Examples/Movies/android/app/src/main/java/com/facebook/react/movies/MoviesActivity.java @@ -0,0 +1,89 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.facebook.react.movies; + +import android.app.Activity; +import android.os.Bundle; +import android.view.KeyEvent; + +import com.facebook.react.LifecycleState; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactRootView; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.shell.MainReactPackage; + +public class MoviesActivity extends Activity implements DefaultHardwareBackBtnHandler { + + private ReactInstanceManager mReactInstanceManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mReactInstanceManager = ReactInstanceManager.builder() + .setApplication(getApplication()) + .setBundleAssetName("MoviesApp.android.bundle") + .setJSMainModuleName("Examples/Movies/MoviesApp.android") + .addPackage(new MainReactPackage()) + .setUseDeveloperSupport(true) + .setInitialLifecycleState(LifecycleState.RESUMED) + .build(); + + ((ReactRootView) findViewById(R.id.react_root_view)) + .startReactApplication(mReactInstanceManager, "MoviesApp", null); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) { + mReactInstanceManager.showDevOptionsDialog(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onPause() { + super.onPause(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onPause(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onResume(this); + } + } + + @Override + public void onBackPressed() { + if (mReactInstanceManager != null) { + mReactInstanceManager.onBackPressed(); + } else { + super.onBackPressed(); + } + } + + @Override + public void invokeDefaultOnBackPressed() { + super.onBackPressed(); + } +} diff --git a/Examples/Movies/android/app/src/main/res/drawable-hdpi/android_back_white.png b/Examples/Movies/android/app/src/main/res/drawable-hdpi/android_back_white.png new file mode 100644 index 0000000000000000000000000000000000000000..a34f0dbb8fd520edb1d996294478110d3c085504 GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;V2Nu)NpOBzNqJ&XDnmhDdU8=| zafU;w=VzegbWaz@5Rc<;uX=Me81S%MXj(8`?2Xs?$Io-}j)w+p%w4vuspIUwi&N4b zJvcSx_rh{VxqU287#)^dut@|)E||Mg*IX#=(oB&lM~^5bruB6?-t>{!b+APtahl9C zF3!!1+QYTEI1kTzzA!nvdFj^A$>!H2cDgRLPhnfy%i;Ud**nOmCqP6zhvlf-+obMW kbF)sbS^O^k=KU74e(?n{ab}NOfKFuaboFyt=akR{0O5RHwEzGB literal 0 HcmV?d00001 diff --git a/Examples/Movies/android/app/src/main/res/drawable-hdpi/android_search_white.png b/Examples/Movies/android/app/src/main/res/drawable-hdpi/android_search_white.png new file mode 100755 index 0000000000000000000000000000000000000000..861b40db0d4ddb6d7b7b6d68d174072fb691008c GIT binary patch literal 575 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB*pj^6U4S$Y{B+)352QE?JR*yM zvQ&rUPlPeufHm>3#+V##5dyjv*0;-%hjjmI)MTyC0df$#L?bC7tn^ zn{tC#!w)8LW=uVF%U(h7NZg}oo?EYmdCCW|n}}tySkFG&6tzZ7Ni)V#mxrs)M(o_7 zLZig=wD<3#&P2=VJ-7XC`TgF^>ZKtoqb7^1OIE05I$heZWX+OYOL9G>=RLn@n5DqK zhG}}sYyJ%!U$#mwVb@^3?sD1g!?vlG|97VzaQ3uU@tyfl+w=9LRg=!C@Oo6Kzid5e zIsMDM&wJw6g)f(R@z2^oXwr1XvR{J-1!SYp|?6C~CW*;M& z76c31mokRrFLk~f`;b}OVYZqc!^{;&YkX(zkh=VPJ;V9^v!!NrKCETPk$xX@+M(`Y z|FR5OiM|)VQ)=7~2!}iDJupvmANQWTO-A#X!_V#!mM}i4@!`5p0b|YA|HkJV-4AFd zh~3~ia7Ad&UXOsu2MUiYcXzf{`^T9Q>-_Z3p|d3{ItA6gFNbN!=kRDSiarpqc>c|C z8&Ay!c8evA-yDuMoL?588N$AxE0&q7@zpyn)&*VPnbbm8`MqRgud`g}emFT07{v^p Lu6{1-oD!M(}6TtKSPDj(q%x-9Zwg>5Rc<;rx@}zIf%H}s}yibcP=+DRGW8r z^#R>4JkqvpALW8F(z6@SZkFs(e6^{2s@2(+)PHA;ZXSKpAHiUJfYE({pml=T6$|ET zdyY$;NMa5%+Q}elz*tncw6q~KM|bVdFRf|61cbt-Td`@_?h3LJmnyiF@bIGOrMXVb zQoPUPV)bGlfA+CHpZ@WLvH7bP)$DJMc%P5?T_+zE_(F3QJL`!B?`?0e&DLIU-uGSy z{{aiL7a!wGt_qgr>Ke(5J-T_|hq==wkNeh>R92qNNlg2Cy!`(YZ@c)5-ygG`lV9OA U_x_2kKz}lLy85}Sb4q9e0DP-{=l}o! literal 0 HcmV?d00001 diff --git a/Examples/Movies/android/app/src/main/res/drawable-xhdpi/android_back_white.png b/Examples/Movies/android/app/src/main/res/drawable-xhdpi/android_back_white.png new file mode 100644 index 0000000000000000000000000000000000000000..500892e105e98c5a84da959a6cf4eadc317582ee GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}t!4lVqlHmNblJdl&REC1Q^yH$_ z;tYpU&(A=~EuJopArXh)Ui9W-G8AZexN~ZS!NJ}c%H7!;V|X16Ub=@ac3L()^IyNG zS(9`jp@j(pZmp{DhwiC`&HNBCryC*{_Ks~F231r@tm7p|>*@R#tWV$vf|**%Orp6eAaXjpG& z5PiT=abDn`_xok}OV@npKA_=VuvTz`gw>xrEtz1G|xihP`RxLbau3P+%DdNO~jS0VAm|o;zELg|3@0?JaV~qCt;`-7J-o@sn= zed@>P&zu{Ui?3n!-@D;^SJgS6jO^9FZXEo-$oAIGm>{MrA+;|V9ICGr?OY=GVUb+< z)yDMD%6%eV=V)dH1Z=iroEN(HiNc0&6`wc@*1ytaXSntFocaNWtJZuBTaI67TprsP zm-6}^!e1{rR{;nxH*8kP$*}M$`YA3Yjr?)ZA=l*&%X1jy@4R$k~zgEG_fedy| zKCMbew!DaUo>IHyPuL_CSwYJ!rc>wYPQEhv#gw_rIgZ@tkrx#e{kr`N%5L>1~hVoEMWS}zasE7OO}g^nTmCv;K@nJPMe+z?9J$BIXlPu2^XV8+%D->wN_oS{l)WjiTfAb*PiEJlyiiz fYC$|<_?r2`s>aPtPE0w#U|{fc^>bP0l+XkKNSc6; literal 0 HcmV?d00001 diff --git a/Examples/Movies/android/app/src/main/res/drawable-xxhdpi/android_search_white.png b/Examples/Movies/android/app/src/main/res/drawable-xxhdpi/android_search_white.png new file mode 100755 index 0000000000000000000000000000000000000000..d9f75bc794e76f1e7057b35124cefe6c2fa9c5c8 GIT binary patch literal 930 zcmV;T16}-yP)mb5SFhNPR4CL}$ObYHF6EskADbz{t~4^dg< z45oOj_JAiqf6AEs(e-;N9=Ac?4w9N#NNC~(Fq|{?aP<2)7JNI1YoLyOwmJo#0_R$M z0w#(Rm#3Ud0AMZVKRaJ=uKAc;Yrb7{D|ja1u_|=i65UC0k6XD`+`zJ>(0yRYQO=<# zem9nVJ83U;1ZY|copYRf&XTuDYeVcGVAfJ(1IJqdT3eZhCI77J-Y=!RZpt#gVvIR< zSzr`@C1YKMkyg;8W&D-v0u%B~W{Q%7w1UPho>mq?GlA2u`Fvi)jv6TJ}I<$;mcUfGDJV&k*rxkQy8BaRyC<CLJnElPT9j5+D0q+HP3ck|u&1=M_dNRrfxuRAt?#VSj3^?lF@@C@h!E?4?a zibNIph1n_{Q+dMm)sPf5lJ^_MCa{EMg&axfxg=j-xgJSsNcySPk;O+zZ;dgHn0Pfw z?pr`9k3B| zCtd7Eteq~Bf}Bp;Bn3G(B1tvhbp!%|Kp+qZ1OgX}|BrB-B9PU+=Kufz07*qoM6N<$ Eg1Z%=^Z)<= literal 0 HcmV?d00001 diff --git a/Examples/Movies/android/app/src/main/res/drawable/rotten_tomatoes_icon.png b/Examples/Movies/android/app/src/main/res/drawable/rotten_tomatoes_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..395d7043130e5e1712052cf5722608b7f4315b4a GIT binary patch literal 58467 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 + + + + diff --git a/Examples/Movies/android/app/src/main/res/values/strings.xml b/Examples/Movies/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..7c4632e0d0 --- /dev/null +++ b/Examples/Movies/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + MoviesApp + diff --git a/Examples/Movies/android/app/src/main/res/values/styles.xml b/Examples/Movies/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..319eb0ca10 --- /dev/null +++ b/Examples/Movies/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/Examples/SampleApp/android/app/build.gradle b/Examples/SampleApp/android/app/build.gradle new file mode 100644 index 0000000000..aafb4a48a3 --- /dev/null +++ b/Examples/SampleApp/android/app/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + applicationId "com.facebook.react.sample" + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + ndk { + abiFilters "armeabi-v7a", "x86" + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:23.0.0' + + // Depend on pre-built React Native + compile 'com.facebook.react:react-native:0.11.+' + + // Depend on React Native source. + // This is useful for testing your changes when working on React Native. + // compile project(':ReactAndroid') +} diff --git a/Examples/SampleApp/android/app/proguard-rules.pro b/Examples/SampleApp/android/app/proguard-rules.pro new file mode 100644 index 0000000000..a92fa177ee --- /dev/null +++ b/Examples/SampleApp/android/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/Examples/SampleApp/android/app/src/main/AndroidManifest.xml b/Examples/SampleApp/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..bf3de9949b --- /dev/null +++ b/Examples/SampleApp/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/Examples/SampleApp/android/app/src/main/java/com/facebook/react/sample/MainActivity.java b/Examples/SampleApp/android/app/src/main/java/com/facebook/react/sample/MainActivity.java new file mode 100644 index 0000000000..eb2e4f8b18 --- /dev/null +++ b/Examples/SampleApp/android/app/src/main/java/com/facebook/react/sample/MainActivity.java @@ -0,0 +1,88 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.facebook.react.sample; + +import android.app.Activity; +import android.os.Bundle; +import android.view.KeyEvent; + +import com.facebook.react.LifecycleState; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactRootView; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.shell.MainReactPackage; +import com.facebook.soloader.SoLoader; + +public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler { + + private ReactInstanceManager mReactInstanceManager; + private ReactRootView mReactRootView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + SoLoader.init(this, false); + mReactRootView = new ReactRootView(this); + + mReactInstanceManager = ReactInstanceManager.builder() + .setApplication(getApplication()) + .setBundleAssetName("index.android.bundle") + .setJSMainModuleName("Examples/SampleApp/index.android") + .addPackage(new MainReactPackage()) + .setUseDeveloperSupport(BuildConfig.DEBUG) + .setInitialLifecycleState(LifecycleState.RESUMED) + .build(); + + mReactRootView.startReactApplication( + mReactInstanceManager, + "SampleApp", + null); + + setContentView(mReactRootView); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) { + mReactInstanceManager.showDevOptionsDialog(); + return true; + } + return super.onKeyUp(keyCode, event); + + } + + @Override + public void invokeDefaultOnBackPressed() { + super.onBackPressed(); + } + + @Override + protected void onPause() { + super.onPause(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onPause(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onResume(this); + } + } +} diff --git a/Examples/SampleApp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Examples/SampleApp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cde69bcccec65160d92116f20ffce4fce0b5245c GIT binary patch literal 3418 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 literal 0 HcmV?d00001 diff --git a/Examples/SampleApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Examples/SampleApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa42f0e7b91d006d22352c9ff2f134e504e3c1d GIT binary patch literal 4842 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 literal 0 HcmV?d00001 diff --git a/Examples/SampleApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Examples/SampleApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..324e72cdd7480cb983fa1bcc7ce686e51ef87fe7 GIT binary patch literal 7718 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 literal 0 HcmV?d00001 diff --git a/Examples/SampleApp/android/app/src/main/res/values/strings.xml b/Examples/SampleApp/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..18f4708813 --- /dev/null +++ b/Examples/SampleApp/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + SampleApp + diff --git a/Examples/SampleApp/android/app/src/main/res/values/styles.xml b/Examples/SampleApp/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..319eb0ca10 --- /dev/null +++ b/Examples/SampleApp/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/Examples/SampleApp/index.android.js b/Examples/SampleApp/index.android.js new file mode 100644 index 0000000000..47371fa593 --- /dev/null +++ b/Examples/SampleApp/index.android.js @@ -0,0 +1,52 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + */ +'use strict'; + +var React = require('react-native'); +var { + AppRegistry, + StyleSheet, + Text, + View, +} = React; + +var SampleApp = React.createClass({ + render: function() { + return ( + + + Welcome to React Native! + + + To get started, edit index.android.js + + + Shake or press menu button for dev menu + + + ); + } +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#F5FCFF', + }, + welcome: { + fontSize: 20, + textAlign: 'center', + margin: 10, + }, + instructions: { + textAlign: 'center', + color: '#333333', + marginBottom: 5, + }, +}); + +AppRegistry.registerComponent('SampleApp', () => SampleApp); diff --git a/Examples/UIExplorer/AccessibilityAndroidExample.android.js b/Examples/UIExplorer/AccessibilityAndroidExample.android.js new file mode 100644 index 0000000000..3df94c6031 --- /dev/null +++ b/Examples/UIExplorer/AccessibilityAndroidExample.android.js @@ -0,0 +1,213 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + Text, + View, + ToastAndroid, + TouchableWithoutFeedback, +} = React; + +var UIExplorerBlock = require('./UIExplorerBlock'); +var UIExplorerPage = require('./UIExplorerPage'); + +var importantForAccessibilityValues = ['auto', 'yes', 'no', 'no-hide-descendants']; + +var AccessibilityAndroidExample = React.createClass({ + + statics: { + title: 'Accessibility', + description: 'Examples of using Accessibility API.', + }, + + getInitialState: function() { + return { + count: 0, + backgroundImportantForAcc: 0, + forgroundImportantForAcc: 0, + }; + }, + + _addOne: function() { + this.setState({ + count: ++this.state.count, + }); + }, + + _changeBackgroundImportantForAcc: function() { + this.setState({ + backgroundImportantForAcc: (this.state.backgroundImportantForAcc + 1) % 4, + }); + }, + + _changeForgroundImportantForAcc: function() { + this.setState({ + forgroundImportantForAcc: (this.state.forgroundImportantForAcc + 1) % 4, + }); + }, + + render: function() { + return ( + + + + + + This is + + + nontouchable normal view. + + + + + + + + This is + + + nontouchable accessible view without label. + + + + + + + + This is + + + nontouchable accessible view with label. + + + + + + ToastAndroid.show('Toasts work by default', ToastAndroid.SHORT)} + accessibilityComponentType="button"> + + Click me + Or not + + + + + + + + Click me + + + + Clicked {this.state.count} times + + + + + + + + + Hello + + + + + + + world + + + + + + + + Change importantForAccessibility for background layout. + + + + + + Background layout importantForAccessibility + + + {importantForAccessibilityValues[this.state.backgroundImportantForAcc]} + + + + + + Change importantForAccessibility for forground layout. + + + + + + Forground layout importantForAccessibility + + + {importantForAccessibilityValues[this.state.forgroundImportantForAcc]} + + + + + + ); + }, +}); + +var styles = StyleSheet.create({ + embedded: { + backgroundColor: 'yellow', + padding:10, + }, + container: { + flex: 1, + backgroundColor: 'white', + padding: 10, + height:150, + }, +}); + +module.exports = AccessibilityAndroidExample; diff --git a/Examples/UIExplorer/ProgressBarAndroidExample.android.js b/Examples/UIExplorer/ProgressBarAndroidExample.android.js new file mode 100644 index 0000000000..040ed00fdf --- /dev/null +++ b/Examples/UIExplorer/ProgressBarAndroidExample.android.js @@ -0,0 +1,62 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var ProgressBar = require('ProgressBarAndroid'); +var React = require('React'); +var UIExplorerBlock = require('UIExplorerBlock'); +var UIExplorerPage = require('UIExplorerPage'); + +var ProgressBarAndroidExample = React.createClass({ + + statics: { + title: '', + description: 'Visual indicator of progress of some operation. ' + + 'Shows either a cyclic animation or a horizontal bar.', + }, + + render: function() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); + }, +}); + +module.exports = ProgressBarAndroidExample; diff --git a/Examples/UIExplorer/ScrollViewSimpleExample.js b/Examples/UIExplorer/ScrollViewSimpleExample.js index c9bbe74072..af7d6863bc 100644 --- a/Examples/UIExplorer/ScrollViewSimpleExample.js +++ b/Examples/UIExplorer/ScrollViewSimpleExample.js @@ -30,6 +30,7 @@ var ScrollViewSimpleExample = React.createClass({ title: '', description: 'Component that enables scrolling through child components.' }, + makeItems: function(nItems: number, styles): Array { var items = []; for (var i = 0; i < nItems; i++) { diff --git a/Examples/UIExplorer/SwitchAndroidExample.android.js b/Examples/UIExplorer/SwitchAndroidExample.android.js new file mode 100644 index 0000000000..ef58c5a368 --- /dev/null +++ b/Examples/UIExplorer/SwitchAndroidExample.android.js @@ -0,0 +1,80 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +'use strict'; + +var React = require('React'); + +var SwitchAndroid = require('SwitchAndroid'); +var Text = require('Text'); +var UIExplorerBlock = require('UIExplorerBlock'); +var UIExplorerPage = require('UIExplorerPage'); + +var SwitchAndroidExample = React.createClass({ + statics: { + title: '', + description: 'Standard Android two-state toggle component' + }, + + getInitialState : function() { + return { + trueSwitchIsOn: true, + falseSwitchIsOn: false, + colorTrueSwitchIsOn: true, + colorFalseSwitchIsOn: false, + eventSwitchIsOn: false, + }; + }, + + render: function() { + return ( + + + this.setState({falseSwitchIsOn: value})} + style={{marginBottom: 10}} + value={this.state.falseSwitchIsOn} /> + this.setState({trueSwitchIsOn: value})} + value={this.state.trueSwitchIsOn} /> + + + + + + + this.setState({eventSwitchIsOn: value})} + style={{marginBottom: 10}} + value={this.state.eventSwitchIsOn} /> + this.setState({eventSwitchIsOn: value})} + style={{marginBottom: 10}} + value={this.state.eventSwitchIsOn} /> + {this.state.eventSwitchIsOn ? "On" : "Off"} + + + + + + + ); + } +}); + +module.exports = SwitchAndroidExample; diff --git a/Examples/UIExplorer/TextExample.android.js b/Examples/UIExplorer/TextExample.android.js new file mode 100644 index 0000000000..4159d0c18f --- /dev/null +++ b/Examples/UIExplorer/TextExample.android.js @@ -0,0 +1,349 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + Text, + View, +} = React; +var UIExplorerBlock = require('./UIExplorerBlock'); +var UIExplorerPage = require('./UIExplorerPage'); + +var Entity = React.createClass({ + render: function() { + return ( + + {this.props.children} + + ); + } +}); + +var AttributeToggler = React.createClass({ + getInitialState: function() { + return {fontWeight: 'bold', fontSize: 15}; + }, + toggleWeight: function() { + this.setState({ + fontWeight: this.state.fontWeight === 'bold' ? 'normal' : 'bold' + }); + }, + increaseSize: function() { + this.setState({ + fontSize: this.state.fontSize + 1 + }); + }, + render: function() { + var curStyle = {fontWeight: this.state.fontWeight, fontSize: this.state.fontSize}; + return ( + + + Tap the controls below to change attributes. + + + See how it will even work on this nested text + + + Toggle Weight + {' (with highlight onPress)'} + + + Increase Size (suppressHighlighting true) + + + ); + } +}); + +var TextExample = React.createClass({ + statics: { + title: '', + description: 'Base component for rendering styled text.', + }, + render: function() { + return ( + + + + The text should wrap if it goes on multiple lines. + See, this is going to the next line. + + + + + This text is indented by 10px padding on all sides. + + + + + Sans-Serif + + + Sans-Serif Bold + + + Serif + + + Serif Bold + + + Monospace + + + Monospace Bold (After 5.0) + + + + + + + Roboto Regular + + + Roboto Italic + + + Roboto Bold + + + Roboto Bold Italic + + + Roboto Light + + + Roboto Light Italic + + + Roboto Thin (After 4.2) + + + Roboto Thin Italic (After 4.2) + + + Roboto Condensed + + + Roboto Condensed Italic + + + Roboto Condensed Bold + + + Roboto Condensed Bold Italic + + + Roboto Medium (After 5.0) + + + Roboto Medium Italic (After 5.0) + + + + + + + Size 23 + + + Size 8 + + + + + Red color + + + Blue color + + + + + Move fast and be bold + + + Move fast and be bold + + + + + Move fast and be bold + + + Move fast and be bold + + + + + Move fast and be bold + + + + console.log('1st')}> + (Normal text, + console.log('2nd')}> + (and bold + console.log('3rd')}> + (and tiny bold italic blue + console.log('4th')}> + (and tiny normal blue) + + ) + + ) + + ) + + console.log('1st')}> + (Serif + console.log('2nd')}> + (Serif Bold Italic + console.log('3rd')}> + (Monospace Normal + console.log('4th')}> + (Sans-Serif Bold + console.log('5th')}> + (and Sans-Serif Normal) + + ) + + ) + + ) + + ) + + + Entity Name + + + + + auto (default) - english LTR + + + أحب اللغة العربية auto (default) - arabic RTL + + + left left left left left left left left left left left left left left left + + + center center center center center center center center center center center + + + right right right right right right right right right right right right right + + + + + + + 星际争霸是世界上最好的游戏。 + + + + + 星际争霸是世界上最好的游戏。 + + + + + 星际争霸是世界上最好的游戏。 + + + + + 星际争霸是世界上最好的游戏。星际争霸是世界上最好的游戏。星际争霸是世界上最好的游戏。星际争霸是世界上最好的游戏。 + + + + + + + A {'generated'} {' '} {'string'} and some     spaces + + + + + Holisticly formulate inexpensive ideas before best-of-breed benefits. Continually expedite magnetic potentialities rather than client-focused interfaces. + + + + + + + + + + + Red background, + + {' '}blue background, + + {' '}inherited blue background, + + {' '}nested green background. + + + + + + + + + + + + Default containerBackgroundColor (inherited) + backgroundColor wash + + + {"containerBackgroundColor: 'transparent' + backgroundColor wash"} + + + + + Maximum of one line no matter now much I write here. If I keep writing it{"'"}ll just truncate after one line + + + Maximum of two lines no matter now much I write here. If I keep writing it{"'"}ll just truncate after two lines + + + No maximum lines specified no matter now much I write here. If I keep writing it{"'"}ll just keep going and going + + + + ); + } +}); + +var styles = StyleSheet.create({ + backgroundColorText: { + left: 5, + backgroundColor: 'rgba(100, 100, 100, 0.3)' + }, +}); + +module.exports = TextExample; diff --git a/Examples/UIExplorer/TextInputExample.android.js b/Examples/UIExplorer/TextInputExample.android.js new file mode 100644 index 0000000000..9659e9180f --- /dev/null +++ b/Examples/UIExplorer/TextInputExample.android.js @@ -0,0 +1,316 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Text, + TextInput, + View, + StyleSheet, +} = React; + +var TextEventsExample = React.createClass({ + getInitialState: function() { + return { + curText: '', + prevText: '', + prev2Text: '', + }; + }, + + updateText: function(text) { + this.setState((state) => { + return { + curText: text, + prevText: state.curText, + prev2Text: state.prevText, + }; + }); + }, + + render: function() { + return ( + + this.updateText('onFocus')} + onBlur={() => this.updateText('onBlur')} + onChange={(event) => this.updateText( + 'onChange text: ' + event.nativeEvent.text + )} + onEndEditing={(event) => this.updateText( + 'onEndEditing text: ' + event.nativeEvent.text + )} + onSubmitEditing={(event) => this.updateText( + 'onSubmitEditing text: ' + event.nativeEvent.text + )} + style={styles.singleLine} + /> + + {this.state.curText}{'\n'} + (prev: {this.state.prevText}){'\n'} + (prev2: {this.state.prev2Text}) + + + ); + } +}); + +class RewriteExample extends React.Component { + constructor(props) { + super(props); + this.state = {text: ''}; + } + render() { + return ( + { + text = text.replace(/ /g, '_'); + this.setState({text}); + }} + style={styles.singleLine} + value={this.state.text} + /> + ); + } +} + +var styles = StyleSheet.create({ + multiline: { + height: 60, + fontSize: 16, + padding: 4, + marginBottom: 10, + }, + eventLabel: { + margin: 3, + fontSize: 12, + }, + singleLine: { + fontSize: 16, + padding: 4, + }, + singleLineWithHeightTextInput: { + height: 30, + }, +}); + +exports.title = ''; +exports.description = 'Single and multi-line text inputs.'; +exports.examples = [ + { + title: 'Auto-focus', + render: function() { + return ; + } + }, + { + title: "Live Re-Write ( -> '_')", + render: function() { + return ; + } + }, + { + title: 'Auto-capitalize', + render: function() { + var autoCapitalizeTypes = [ + 'none', + 'sentences', + 'words', + 'characters', + ]; + var examples = autoCapitalizeTypes.map((type) => { + return ( + + ); + }); + return {examples}; + } + }, + { + title: 'Auto-correct', + render: function() { + return ( + + + + + ); + } + }, + { + title: 'Keyboard types', + render: function() { + var keyboardTypes = [ + 'default', + 'email-address', + 'numeric', + ]; + var examples = keyboardTypes.map((type) => { + return ( + + ); + }); + return {examples}; + } + }, + { + title: 'Event handling', + render: function(): ReactElement { return ; }, + }, + { + title: 'Colors and text inputs', + render: function() { + return ( + + + + + + + + + ); + } + }, + { + title: 'Text input, themes and heights', + render: function() { + return ( + + ); + } + }, + { + title: 'Passwords', + render: function() { + return ( + + ); + } + }, + { + title: 'Editable', + render: function() { + return ( + + ); + } + }, + { + title: 'Multiline', + render: function() { + return ( + + + + + multiline with children, aligned bottom-right + + + ); + } + }, + { + title: 'Fixed number of lines', + platform: 'android', + render: function() { + return ( + + + + + ); + } + }, +]; diff --git a/Examples/UIExplorer/ToastAndroidExample.android.js b/Examples/UIExplorer/ToastAndroidExample.android.js new file mode 100644 index 0000000000..0e99647665 --- /dev/null +++ b/Examples/UIExplorer/ToastAndroidExample.android.js @@ -0,0 +1,68 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + Text, + ToastAndroid, + TouchableWithoutFeedback +} = React; + +var UIExplorerBlock = require('UIExplorerBlock'); +var UIExplorerPage = require('UIExplorerPage'); + +var ToastExample = React.createClass({ + + statics: { + title: 'Toast Example', + description: 'Toast Example', + }, + + getInitialState: function() { + return {}; + }, + + render: function() { + return ( + + + + ToastAndroid.show('This is a toast with short duration', ToastAndroid.SHORT)}> + Click me. + + + + + ToastAndroid.show('This is a toast with long duration', ToastAndroid.LONG)}> + Click me too. + + + + ); + }, +}); + +var styles = StyleSheet.create({ + text: { + color: 'black', + }, +}); + +module.exports = ToastExample; diff --git a/Examples/UIExplorer/ToolbarAndroidExample.android.js b/Examples/UIExplorer/ToolbarAndroidExample.android.js new file mode 100644 index 0000000000..359ba087e8 --- /dev/null +++ b/Examples/UIExplorer/ToolbarAndroidExample.android.js @@ -0,0 +1,119 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + Text, + View, +} = React; +var UIExplorerBlock = require('./UIExplorerBlock'); +var UIExplorerPage = require('./UIExplorerPage'); + +var SwitchAndroid = require('SwitchAndroid'); +var ToolbarAndroid = require('ToolbarAndroid'); + +var ToolbarAndroidExample = React.createClass({ + statics: { + title: '', + description: 'Examples of using the Android toolbar.' + }, + getInitialState: function() { + return { + actionText: 'Example app with toolbar component', + toolbarSwitch: false, + colorProps: { + titleColor: '#3b5998', + subtitleColor: '#6a7180', + }, + }; + }, + render: function() { + return ( + + + this.setState({actionText: 'Icon clicked'})} + style={styles.toolbar} + subtitle={this.state.actionText} + title="Toolbar" /> + {this.state.actionText} + + + + + this.setState({'toolbarSwitch': value})} /> + {'\'Tis but a switch'} + + + + + + + + + + + this.setState({colorProps: {}})} + title="Wow, such toolbar" + style={styles.toolbar} + subtitle="Much native" + {...this.state.colorProps} /> + + Touch the icon to reset the custom colors to the default (theme-provided) ones. + + + + ); + }, + _onActionSelected: function(position) { + this.setState({ + actionText: 'Selected ' + toolbarActions[position].title, + }); + }, +}); + +var toolbarActions = [ + {title: 'Create', icon: require('image!ic_create_black_48dp'), show: 'always'}, + {title: 'Filter'}, + {title: 'Settings', icon: require('image!ic_settings_black_48dp'), show: 'always'}, +]; + +var styles = StyleSheet.create({ + toolbar: { + backgroundColor: '#e9eaed', + height: 56, + }, +}); + +module.exports = ToolbarAndroidExample; diff --git a/Examples/UIExplorer/UIExplorerApp.android.js b/Examples/UIExplorer/UIExplorerApp.android.js index 154796cef7..c9bd2418fe 100644 --- a/Examples/UIExplorer/UIExplorerApp.android.js +++ b/Examples/UIExplorer/UIExplorerApp.android.js @@ -18,34 +18,42 @@ var React = require('react-native'); var { + AppRegistry, + BackAndroid, Dimensions, + DrawerLayoutAndroid, StyleSheet, + ToolbarAndroid, View, } = React; -var UIExplorerList = require('./UIExplorerList'); - -// TODO: these should be exposed by the 'react-native' module. -var DrawerLayoutAndroid = require('DrawerLayoutAndroid'); -var ToolbarAndroid = require('ToolbarAndroid'); +var UIExplorerList = require('./UIExplorerList.android'); var DRAWER_WIDTH_LEFT = 56; var UIExplorerApp = React.createClass({ - getInitialState: function() { return { - example: { - title: 'UIExplorer', - component: this._renderHome(), - }, + example: this._getUIExplorerHome(), }; }, + _getUIExplorerHome: function() { + return { + title: 'UIExplorer', + component: this._renderHome(), + }; + }, + + componentWillMount: function() { + BackAndroid.addEventListener('hardwareBackPress', this._handleBackButtonPress); + }, + render: function() { return ( { this.drawer = drawer; }} renderNavigationView={this._renderNavigationView}> {this._renderNavigation()} @@ -64,14 +72,11 @@ var UIExplorerApp = React.createClass({ onSelectExample: function(example) { this.drawer.closeDrawer(); - if (example.title === 'UIExplorer') { - example.component = this._renderHome(); + if (example.title === this._getUIExplorerHome().title) { + example = this._getUIExplorerHome(); } this.setState({ - example: { - title: example.title, - component: example.component, - }, + example: example, }); }, @@ -105,16 +110,16 @@ var UIExplorerApp = React.createClass({ ); }, + _handleBackButtonPress: function() { + if (this.state.example.title !== this._getUIExplorerHome().title) { + this.onSelectExample(this._getUIExplorerHome()); + return true; + } + return false; + }, }); var styles = StyleSheet.create({ - messageText: { - fontSize: 17, - fontWeight: '500', - padding: 15, - marginTop: 50, - marginLeft: 15, - }, container: { flex: 1, }, @@ -124,4 +129,6 @@ var styles = StyleSheet.create({ }, }); +AppRegistry.registerComponent('UIExplorerApp', () => UIExplorerApp); + module.exports = UIExplorerApp; diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js new file mode 100644 index 0000000000..d102d3de84 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -0,0 +1,99 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + View, +} = React; +var UIExplorerListBase = require('./UIExplorerListBase'); + +var COMPONENTS = [ + require('./ImageExample'), + require('./ProgressBarAndroidExample'), + require('./ScrollViewSimpleExample'), + require('./SwitchAndroidExample'), + require('./TextExample.android'), + require('./TextInputExample.android'), + require('./ToolbarAndroidExample'), + require('./TouchableExample'), + require('./ViewExample'), +]; + +var APIS = [ + require('./AccessibilityAndroidExample.android'), + require('./BorderExample'), + require('./LayoutEventsExample'), + require('./LayoutExample'), + require('./PanResponderExample'), + require('./PointerEventsExample'), + require('./TimerExample'), + require('./ToastAndroidExample.android'), + require('./XHRExample'), +]; + +type Props = { + onSelectExample: Function, + isInDrawer: bool, +}; + +class UIExplorerList extends React.Component { + props: Props; + + render() { + return ( + + ); + } + + renderAdditionalView(renderRow, renderTextInput): React.Component { + if (this.props.isInDrawer) { + var homePage = renderRow({ + title: 'UIExplorer', + description: 'List of examples', + }, -1); + return ( + + {homePage} + + ); + } + return renderTextInput(styles.searchTextInput); + } + + onPressRow(example: any) { + var Component = UIExplorerListBase.makeRenderable(example); + this.props.onSelectExample({ + title: Component.title, + component: Component, + }); + } +} + +var styles = StyleSheet.create({ + searchTextInput: { + padding: 2, + }, +}); + +module.exports = UIExplorerList; diff --git a/Examples/UIExplorer/XHRExample.android.js b/Examples/UIExplorer/XHRExample.android.js new file mode 100644 index 0000000000..b0069794e7 --- /dev/null +++ b/Examples/UIExplorer/XHRExample.android.js @@ -0,0 +1,326 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + PixelRatio, + StyleSheet, + Text, + TextInput, + TouchableHighlight, + View, +} = React; + +// TODO t7093728 This is a simlified XHRExample.ios.js. +// Once we have Camera roll, Toast, Intent (for opening URLs) +// we should make this consistent with iOS. + +class Downloader extends React.Component { + + xhr: XMLHttpRequest; + cancelled: boolean; + + constructor(props) { + super(props); + this.cancelled = false; + this.state = { + status: '', + contentSize: 1, + downloaded: 0, + }; + } + + download() { + this.xhr && this.xhr.abort(); + + var xhr = this.xhr || new XMLHttpRequest(); + xhr.onreadystatechange = () => { + if (xhr.readyState === xhr.HEADERS_RECEIVED) { + var contentSize = parseInt(xhr.getResponseHeader('Content-Length'), 10); + this.setState({ + contentSize: contentSize, + downloaded: 0, + }); + } else if (xhr.readyState === xhr.LOADING) { + this.setState({ + downloaded: xhr.responseText.length, + }); + console.log(xhr.responseText.length); + } else if (xhr.readyState === xhr.DONE) { + if (this.cancelled) { + this.cancelled = false; + return; + } + if (xhr.status === 200) { + this.setState({ + status: 'Download complete!', + }); + } else if (xhr.status !== 0) { + this.setState({ + status: 'Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText, + }); + } else { + this.setState({ + status: 'Error: ' + xhr.responseText, + }); + } + } + }; + xhr.open('GET', 'http://www.gutenberg.org/cache/epub/100/pg100.txt'); + xhr.send(); + this.xhr = xhr; + + this.setState({status: 'Downloading...'}); + } + + componentWillUnmount() { + this.cancelled = true; + this.xhr && this.xhr.abort(); + } + + render() { + var button = this.state.status === 'Downloading...' ? ( + + + ... + + + ) : ( + + + Download 5MB Text File + + + ); + + return ( + + {button} + {this.state.status} + + ); + } +} + +class FormUploader extends React.Component { + + _isMounted: boolean; + _addTextParam: () => void; + _upload: () => void; + + constructor(props) { + super(props); + this.state = { + isUploading: false, + uploadProgress: null, + textParams: [], + }; + this._isMounted = true; + this._addTextParam = this._addTextParam.bind(this); + this._upload = this._upload.bind(this); + } + + _addTextParam() { + var textParams = this.state.textParams; + textParams.push({name: '', value: ''}); + this.setState({textParams}); + } + + componentWillUnmount() { + this._isMounted = false; + } + + _onTextParamNameChange(index, text) { + var textParams = this.state.textParams; + textParams[index].name = text; + this.setState({textParams}); + } + + _onTextParamValueChange(index, text) { + var textParams = this.state.textParams; + textParams[index].value = text; + this.setState({textParams}); + } + + _upload() { + var xhr = new XMLHttpRequest(); + xhr.open('POST', 'http://posttestserver.com/post.php'); + xhr.onload = () => { + this.setState({isUploading: false}); + if (xhr.status !== 200) { + console.log( + 'Upload failed', + 'Expected HTTP 200 OK response, got ' + xhr.status + ); + return; + } + if (!xhr.responseText) { + console.log( + 'Upload failed', + 'No response payload.' + ); + return; + } + var index = xhr.responseText.indexOf('http://www.posttestserver.com/'); + if (index === -1) { + console.log( + 'Upload failed', + 'Invalid response payload.' + ); + return; + } + var url = xhr.responseText.slice(index).split('\n')[0]; + console.log('Upload successful: ' + url); + }; + var formdata = new FormData(); + this.state.textParams.forEach( + (param) => formdata.append(param.name, param.value) + ); + if (xhr.upload) { + xhr.upload.onprogress = (event) => { + console.log('upload onprogress', event); + if (event.lengthComputable) { + this.setState({uploadProgress: event.loaded / event.total}); + } + }; + } + xhr.send(formdata); + this.setState({isUploading: true}); + } + + render() { + var textItems = this.state.textParams.map((item, index) => ( + + + = + + + )); + var uploadButtonLabel = this.state.isUploading ? 'Uploading...' : 'Upload'; + var uploadProgress = this.state.uploadProgress; + if (uploadProgress !== null) { + uploadButtonLabel += ' ' + Math.round(uploadProgress * 100) + '%'; + } + var uploadButton = ( + + {uploadButtonLabel} + + ); + if (!this.state.isUploading) { + uploadButton = ( + + {uploadButton} + + ); + } + return ( + + {textItems} + + + Add a text param + + + + {uploadButton} + + + ); + } +} + + +exports.framework = 'React'; +exports.title = 'XMLHttpRequest'; +exports.description = 'XMLHttpRequest'; +exports.examples = [{ + title: 'File Download', + render() { + return ; + } +}, { + title: 'multipart/form-data Upload', + render() { + return ; + } +}]; + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + backgroundColor: '#eeeeee', + padding: 8, + }, + paramRow: { + flexDirection: 'row', + paddingVertical: 8, + alignItems: 'center', + borderBottomWidth: 1 / PixelRatio.get(), + borderBottomColor: 'grey', + }, + textButton: { + color: 'blue', + }, + addTextParamButton: { + marginTop: 8, + }, + textInput: { + flex: 1, + borderRadius: 3, + borderColor: 'grey', + borderWidth: 1, + height: 30, + paddingLeft: 8, + }, + equalSign: { + paddingHorizontal: 4, + }, + uploadButton: { + marginTop: 16, + }, + uploadButtonBox: { + flex: 1, + paddingVertical: 12, + alignItems: 'center', + backgroundColor: 'blue', + borderRadius: 4, + }, + uploadButtonLabel: { + color: 'white', + fontSize: 16, + fontWeight: '500', + }, +}); diff --git a/Examples/UIExplorer/android/app/build.gradle b/Examples/UIExplorer/android/app/build.gradle new file mode 100644 index 0000000000..e5fa61d8c7 --- /dev/null +++ b/Examples/UIExplorer/android/app/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + applicationId "com.facebook.react.uiapp" + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + ndk { + abiFilters "armeabi-v7a", "x86" + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:23.0.0' + + // Depend on pre-built React Native + compile 'com.facebook.react:react-native:0.11.+' + + // Depend on React Native source. + // This is useful for testing your changes when working on React Native. + // compile project(':ReactAndroid') +} diff --git a/Examples/UIExplorer/android/app/proguard-rules.pro b/Examples/UIExplorer/android/app/proguard-rules.pro new file mode 100644 index 0000000000..a92fa177ee --- /dev/null +++ b/Examples/UIExplorer/android/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml b/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..b69776f9a5 --- /dev/null +++ b/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java b/Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java new file mode 100644 index 0000000000..04c8e258c6 --- /dev/null +++ b/Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java @@ -0,0 +1,89 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.facebook.react.uiapp; + +import android.app.Activity; +import android.os.Bundle; +import android.view.KeyEvent; + +import com.facebook.react.LifecycleState; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactRootView; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.shell.MainReactPackage; + +public class UIExplorerActivity extends Activity implements DefaultHardwareBackBtnHandler { + + private ReactInstanceManager mReactInstanceManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mReactInstanceManager = ReactInstanceManager.builder() + .setApplication(getApplication()) + .setBundleAssetName("UIExplorerApp.android.bundle") + .setJSMainModuleName("Examples/UIExplorer/UIExplorerApp.android") + .addPackage(new MainReactPackage()) + .setUseDeveloperSupport(true) + .setInitialLifecycleState(LifecycleState.RESUMED) + .build(); + + ((ReactRootView) findViewById(R.id.react_root_view)) + .startReactApplication(mReactInstanceManager, "UIExplorerApp", null); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) { + mReactInstanceManager.showDevOptionsDialog(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onPause() { + super.onPause(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onPause(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onResume(this); + } + } + + @Override + public void onBackPressed() { + if (mReactInstanceManager != null) { + mReactInstanceManager.onBackPressed(); + } else { + super.onBackPressed(); + } + } + + @Override + public void invokeDefaultOnBackPressed() { + super.onBackPressed(); + } +} diff --git a/Examples/UIExplorer/android/app/src/main/res/drawable/ic_create_black_48dp.png b/Examples/UIExplorer/android/app/src/main/res/drawable/ic_create_black_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..d3c4ccef2f5b4ff6ac5096459cef49f30fdc0812 GIT binary patch literal 406 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcg6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g&? zz`)4o>EaktaqI1s-MohkBwQcfZggpuXmriE(J;%iLe%k$tN(Ep1w&?0ArTLe6P_~W zKi3zn6aV4&BqFKD@R&sM-~6?!&)ab30lWW-wkZZh{uaj+&vyys74G{H`?;wleF=B)q=L@U`@7ZGEc(0du8^5v z{Omt1xmS)G=_~5k>nobL)I zeLB$2FK<`#86zx41X%4cpkd{J%TJX>IJF?hQAxvXHYbqTr( S^A7^GF?hQAxvX=P)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00d!4L_t(|+U;9eY)w%VU2WA&h-eKpHvJ>1ImX5mL4=SX zHAGF3_#`nmln{RK;X@=y1Rph~l&_){5o#u_QZcr~JQJNK!5-NRH2?BRAUleqRJ32Fj2387e@I%EyU=Q-;O0e6diOLNMjoq zrqjS2wt@LN4QyZ=*rd}yHrv1~agZ{o-(0JS{qN5+-4nDAQHHI+ zQJMm=NXHu|`Js4?$;32<7%QS~xJhmX_IM2a%vn5#X`hSyel+i^>O8EMatzKZkY9z1 zw(sqkD&&VE!$y8nJW*qy9L?kg)~k^pij7kFUGPPffv-rG891m)ekhJebQt0mdU$g8LXiXeWuAeaD6<^k7#bVdn;_Scy^QR$3-o*|`Mln=0b&Em+PUFx57p?v6XmLAHyzcd~b$ZZ0KF3*@l& z{jOr>6xMs;2%5WA#W7fA{JHb2m`xi+C6@cug1lfDYLOM|5(Dd5|5{s*3_}gHVjXN? zz2~2S&xWBA{!2UftzYtEjZ3Y*6>kIM8VC$bWag?UqL$kkrI0`;;~IwauiXq$VD?sl4a~$Ns;JW=A1>*m&+U^p3d6j>;?Ri zvM-)mmfg8jESkUllh#E&bDCapXnYh?vFQEflsK@T=NTw+YCcs`v6%9YjdPb{W9vH4 zz&syXi}_Tp45nGzfT9g04NGYbySElIxbNO8&O_DlSy`G&1ESN)Y<2R5=f$0jH9+l# z#fkk|RR%2XOf-qOcu{v@d24058Us^odvfPg$j`U02l^``er^D$tz2ep4!5uW&3 zD*c7FQE!8O!vTz>DK?RL;od8rV>~g9qT9URqPA8N;D}`rJHkq`RgFaKcapyst^cS( zGV#!ten*Y|S!sAS=3p#kN|8-kyTx7*r70YTaX22%SxaPf*uQi5YgAfip`*t$!;HP7 znwfo?XCAPQ&V~xzmai1EL3h|CD{}oh%U&me0X(0Q56(_X? z{-1!dbQ+k=HZVh{fgx-I{dF4P{}r({l;SzF$Eg+*7+3PkDIp9HAV7cs0er#_=WN)d Tv8ld300000NkvXXu0mjfLCH4e literal 0 HcmV?d00001 diff --git a/Examples/UIExplorer/android/app/src/main/res/drawable/launcher_icon.png b/Examples/UIExplorer/android/app/src/main/res/drawable/launcher_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a4aa3bd6d9acdc977bc945565d7722d84e0797c8 GIT binary patch literal 9578 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

72ATT)w1$($I%NoWL(@b}2Wz(}4&;la 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 EKl*B@YybcN literal 0 HcmV?d00001 diff --git a/Examples/UIExplorer/android/app/src/main/res/drawable/uie_comment_highlighted.png b/Examples/UIExplorer/android/app/src/main/res/drawable/uie_comment_highlighted.png new file mode 100644 index 0000000000000000000000000000000000000000..b33726757ed46498e7da512385e6708961b815e6 GIT binary patch literal 403 zcmeAS@N?(olHy`uVBq!ia0vp^N3j#jV-?mT=R~x z?qG_S5?46a-)zXcG1-6R+i#oC& zzvJE7cl6ywiMQb_vU3WSbIt9ztM)A8M5}1dx3CTO&WkBXEz_K2dYbpTst&{ou;2CIke`}<=m|S8gDPI6x5@=C);^Q3R#*ykgOCne9>D-nJd7pV!zCT)dC(t9N{Y0^mVs6FiMWuKAPnVn5 z?Roq5^S-ybKUY7x%Gdom?!Pgg^PVLYGoSvP^7X90$F9F6)BVq1GT+}VV6SOECsk~d s;zPy!yeaoLEZjKh?$rl^Fz^0*#@%wFGV)>p%W{yAr>mdKI;Vst00SMV4*&oF literal 0 HcmV?d00001 diff --git a/Examples/UIExplorer/android/app/src/main/res/drawable/uie_comment_normal.png b/Examples/UIExplorer/android/app/src/main/res/drawable/uie_comment_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..6491689fbbc88bfd4289d4ffee2e4262097ddaae GIT binary patch literal 420 zcmeAS@N?(olHy`uVBq!ia0vp^N(}fc;mYo{*7tr)BX@!Dy91&-83Z-YtleIPcr8Uv_SS zB@dn6-gzW;tJ>(;OP33+-dm?0)!n+i*HW^mOg^<&ep#>llC!ZBc;qTixW|2(-FD`% z#q@)-rkzySWbjEtzOw$4%N`9qmHThj_V1ZH{hH<1O;v%1mfn!N(pR)u+V0bY{ykPl zOgU$+y&U8%mm1114>INQdNxH4M-Hcd$sKb$@_I5oq#qxdxk2h;-=4R&J?FYl>bI)Q zSA6zo<^;P>o8C>Ge)Xa0`SR{7FPXRA}Dqm^*J1K@i9DFtHPqf+7+UU0N#S%Cew$ zlW*c89TGx1CB$I+e7@UztbK`-;1u)}G!*m*J^*|JB#=y$@Q@-cGv{4Y6&FAy05^!;5Ah}#_ z*>ZeQvr%58is{&LS7rx-nb(~_@bDlK^Fsk(-w|-uAgMazqPh1nQe*c};Ag{lzCLvj ztz%NrjzHkB)dn3!?~d0LTQ98pQ!UmwM6p<0Qy{zheGtPi?kZ22I7kcvi!oiPIo0mb z^moMe8pnk~A+2CFrG^>aI1ribQh5Uk^UMu)B}hA}lR69DDNyMvN@>kw&ehK#mmHKu zN}G=$o*2&_LCc<6&@ot`-5O~N-aHW1nrq0w*g3Z~t;o4_-0%`-enA#WnWtN&hi2XT zYJqq7jaIY^3dG}k0^0c$zVCp+bpWbUsWA|YE%pucW^?oLdff2hR-KX5@hlWjdS}Nh zfN7l|1P1Lx9BD_x1@2VOc1#}z zkE;&bHX@vx(bcBG$$vg_^`=T*I6wtizj0ijC)CA8Otv|TO|#d^*mW#Pqz z!ByMYZll*BgCPEHz{N`tPPDx!7BzY4ZgpCM-3E^6c^Ls-djCy7@BnX=T zNOYede1EAYG&Tj0aR2B>v(^3>gooQf5Zsv<2>lyCAOu1n1VSJLLLdY}AOu1n1VSJL c@{fc30h_?_Xm?+0KL7v#07*qoM6N<$g3xSt9RL6T literal 0 HcmV?d00001 diff --git a/Examples/UIExplorer/android/app/src/main/res/drawable/uie_thumb_selected.png b/Examples/UIExplorer/android/app/src/main/res/drawable/uie_thumb_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..79eb69cf92b3638954dd11b045006803abe6d904 GIT binary patch literal 1110 zcmV-c1gZOpP)5AvWIw6 zQBjmADu|kc9`b`XF-IW;B%ml>LOcnIkb^hLO-R6>gLn=i8YGK{TnssgQ9OlU#O%5| zU7v^Up6QvL*`0BdWe-(Q%rI1SJ>UENRWo#VY=#QqcxNEjK%WSbv`wX0sr;`>|KDM_>N% zYmiUF{koUsnakgO{N69$f3{qM`j!N%@{Y^G3!*xb?zAjBy7$@RCr8B1tp)l0hqGNt zu#mw*4q1R@#gTiTI&52=zHVj+S%IvWLC6Z2*=-L$dF-xRp1wmF!$uKVK$K7-LUTK3 zCXJ8(1=4~L06uf%%x)5p0EvJLE}>k$c6BJFZyv}HgaB~Q{SUn@0U{w15aHtAmo9#B ztyxRbhHA{3f)_Rcp#$$Pz8H$)ElH3FsXC%$ym0y3Qx3p}vd3F>#!1EoCXDj=h|TQ+ zMgV&CopVogW_N#1$t(nz2_Qf^Sa#2~s;x$V5J1@+?e0FROlyo4y-Yjw?)k@yUGra3 zHZRK&vJPY!9FEvsLAkPgqbyfeD`Q*R9VO`J|N8CX(QiIIc%@b*y1R!VOKl8DY@YVo ziN)uO9k-taIV%NNR=`3AP=LoOMvOba0CfP^4`0$ZMGSrw}_0cJT=L$K1j zU1w_#k0`-##8Ti&ywK{<1By5Rngv*vg9^R2>9IuYt~MYrGgua278<8d(F_1MFWo4w z@2?f|QN6~^gb*@hS&onw)o18Q?e6X-!o;d>80z{l$*fZw@qtd5y>H#hI=HF!h!ZZ3 zSu=$kPDDB)Vj1Xh__Cz6AgC;6WqD(Ldrj|mPBBG@_y<@yaO%x_eqQ$)&@`@5vTnR) z{Yg52I4gxi8#XopT63?cC} z;?-jlDAJd`Nlo|R7!hQGq^a^YR!Z;t@yee|8(tNwcCG>Wt61!NCHV92_%6e ckgXa20ii_@% literal 0 HcmV?d00001 diff --git a/Examples/UIExplorer/android/app/src/main/res/layout/activity_main.xml b/Examples/UIExplorer/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..db6e0a8b81 --- /dev/null +++ b/Examples/UIExplorer/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/Examples/UIExplorer/android/app/src/main/res/values/strings.xml b/Examples/UIExplorer/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..0b8cb6bc3c --- /dev/null +++ b/Examples/UIExplorer/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + UIExplorer App + diff --git a/Examples/UIExplorer/android/app/src/main/res/values/styles.xml b/Examples/UIExplorer/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..319eb0ca10 --- /dev/null +++ b/Examples/UIExplorer/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/Libraries/AppStateIOS/AppStateIOS.android.js b/Libraries/AppStateIOS/AppStateIOS.android.js index b8c3e8def8..0f59cbea06 100644 --- a/Libraries/AppStateIOS/AppStateIOS.android.js +++ b/Libraries/AppStateIOS/AppStateIOS.android.js @@ -16,11 +16,11 @@ var warning = require('warning'); class AppStateIOS { static addEventListener(type, handler) { - warning('Cannot listen to AppStateIOS events on Android.'); + warning(false, 'Cannot listen to AppStateIOS events on Android.'); } static removeEventListener(type, handler) { - warning('Cannot remove AppStateIOS listener on Android.'); + warning(false, 'Cannot remove AppStateIOS listener on Android.'); } } diff --git a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.android.js b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.android.js new file mode 100644 index 0000000000..36ad97e51f --- /dev/null +++ b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.android.js @@ -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. + * + * @providesModule ActivityIndicatorIOS + */ +'use strict'; + +var React = require('React'); +var View = require('View'); + +var ActivityIndicatorIOS = React.createClass({ + render(): ReactElement { + return ; + } +}); + +module.exports = ActivityIndicatorIOS; diff --git a/Libraries/Components/DatePicker/DatePickerIOS.android.js b/Libraries/Components/DatePicker/DatePickerIOS.android.js new file mode 100644 index 0000000000..1faf93a05c --- /dev/null +++ b/Libraries/Components/DatePicker/DatePickerIOS.android.js @@ -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. + * + * @providesModule DatePickerIOS + */ + +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var View = require('View'); + +var DummyDatePickerIOS = React.createClass({ + render: function() { + return ( + + DatePickerIOS is not supported on this platform! + + ); + }, +}); + +var styles = StyleSheet.create({ + dummyDatePickerIOS: { + height: 100, + width: 300, + backgroundColor: '#ffbcbc', + borderWidth: 1, + borderColor: 'red', + alignItems: 'center', + justifyContent: 'center', + margin: 10, + }, + datePickerText: { + color: '#333333', + margin: 20, + } +}); + +module.exports = DummyDatePickerIOS; diff --git a/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js b/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js new file mode 100644 index 0000000000..1510de4e8a --- /dev/null +++ b/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule DrawerLayoutAndroid + */ +'use strict'; + +var DrawerConsts = require('NativeModules').UIManager.AndroidDrawerLayout.Constants; +var NativeMethodsMixin = require('NativeMethodsMixin'); +var React = require('React'); +var ReactPropTypes = require('ReactPropTypes'); +var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); +var RCTUIManager = require('NativeModules').UIManager; +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var createReactNativeComponentClass = require('createReactNativeComponentClass'); +var dismissKeyboard = require('dismissKeyboard'); +var merge = require('merge'); + +var RK_DRAWER_REF = 'drawerlayout'; +var INNERVIEW_REF = 'innerView'; + +var DrawerLayoutValidAttributes = { + drawerWidth: true, + drawerPosition: true, +}; + +var DRAWER_STATES = [ + 'Idle', + 'Dragging', + 'Settling', +]; + +/** + * React component that wraps the platform `DrawerLayout` (Android only). The + * Drawer (typically used for navigation) is rendered with `renderNavigationView` + * and direct children are the main view (where your content goes). The navigation + * view is initially not visible on the screen, but can be pulled in from the + * side of the window specified by the `drawerPosition` prop and its width can + * be set by the `drawerWidth` prop. + * + * Example: + * + * ``` + * render: function() { + * var navigationView = ( + * I'm in the Drawer! + * ); + * return ( + * navigationView}> + * Hello + * World! + * + * ); + * }, + * ``` + */ +var DrawerLayoutAndroid = React.createClass({ + statics: { + positions: DrawerConsts.DrawerPosition, + }, + + propTypes: { + /** + * Determines whether the keyboard gets dismissed in response to a drag. + * - 'none' (the default), drags do not dismiss the keyboard. + * - 'on-drag', the keyboard is dismissed when a drag begins. + */ + keyboardDismissMode: ReactPropTypes.oneOf([ + 'none', // default + 'on-drag', + ]), + /** + * Specifies the side of the screen from which the drawer will slide in. + */ + drawerPosition: ReactPropTypes.oneOf([ + DrawerConsts.DrawerPosition.Left, + DrawerConsts.DrawerPosition.Right + ]), + /** + * Specifies the width of the drawer, more precisely the width of the view that be pulled in + * from the edge of the window. + */ + drawerWidth: ReactPropTypes.number, + /** + * Function called whenever there is an interaction with the navigation view. + */ + onDrawerSlide: ReactPropTypes.func, + /** + * Function called when the drawer state has changed. The drawer can be in 3 states: + * - idle, meaning there is no interaction with the navigation view happening at the time + * - dragging, meaning there is currently an interation with the navigation view + * - settling, meaning that there was an interaction with the navigation view, and the + * navigation view is now finishing it's closing or opening animation + */ + onDrawerStateChanged: ReactPropTypes.func, + /** + * Function called whenever the navigation view has been opened. + */ + onDrawerOpen: ReactPropTypes.func, + /** + * Function called whenever the navigation view has been closed. + */ + onDrawerClose: ReactPropTypes.func, + /** + * The navigation view that will be rendered to the side of the screen and can be pulled in. + */ + renderNavigationView: ReactPropTypes.func.isRequired, + }, + + mixins: [NativeMethodsMixin], + + getInnerViewNode: function() { + return this.refs[INNERVIEW_REF].getInnerViewNode(); + }, + + render: function() { + var drawerViewWrapper = + + {this.props.renderNavigationView()} + ; + var childrenWrapper = + + {this.props.children} + ; + return ( + + {childrenWrapper} + {drawerViewWrapper} + + ); + }, + + _onDrawerSlide: function(event) { + if (this.props.onDrawerSlide) { + this.props.onDrawerSlide(event); + } + if (this.props.keyboardDismissMode === 'on-drag') { + dismissKeyboard(); + } + }, + + _onDrawerOpen: function() { + if (this.props.onDrawerOpen) { + this.props.onDrawerOpen(); + } + }, + + _onDrawerClose: function() { + if (this.props.onDrawerClose) { + this.props.onDrawerClose(); + } + }, + + _onDrawerStateChanged: function(event) { + if (this.props.onDrawerStateChanged) { + this.props.onDrawerStateChanged(DRAWER_STATES[event.nativeEvent.drawerState]); + } + }, + + openDrawer: function() { + RCTUIManager.dispatchViewManagerCommand( + this._getDrawerLayoutHandle(), + RCTUIManager.AndroidDrawerLayout.Commands.openDrawer, + null + ); + }, + + closeDrawer: function() { + RCTUIManager.dispatchViewManagerCommand( + this._getDrawerLayoutHandle(), + RCTUIManager.AndroidDrawerLayout.Commands.closeDrawer, + null + ); + }, + + _getDrawerLayoutHandle: function() { + return React.findNodeHandle(this.refs[RK_DRAWER_REF]); + }, +}); + +var styles = StyleSheet.create({ + base: { + flex: 1, + }, + mainSubview: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + drawerSubview: { + position: 'absolute', + top: 0, + bottom: 0, + }, +}); + +// The View that contains both the actual drawer and the main view +var AndroidDrawerLayout = createReactNativeComponentClass({ + validAttributes: merge(ReactNativeViewAttributes.UIView, DrawerLayoutValidAttributes), + uiViewClassName: 'AndroidDrawerLayout', +}); + +module.exports = DrawerLayoutAndroid; diff --git a/Libraries/Components/Navigation/NavigatorIOS.android.js b/Libraries/Components/Navigation/NavigatorIOS.android.js new file mode 100644 index 0000000000..12699e09c6 --- /dev/null +++ b/Libraries/Components/Navigation/NavigatorIOS.android.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule NavigatorIOS + */ +'use strict'; + +module.exports = require('UnimplementedView'); diff --git a/Libraries/Components/Navigator/NavigatorBreadcrumbNavigationBarStyles.android.js b/Libraries/Components/Navigator/NavigatorBreadcrumbNavigationBarStyles.android.js new file mode 100644 index 0000000000..359e63c326 --- /dev/null +++ b/Libraries/Components/Navigator/NavigatorBreadcrumbNavigationBarStyles.android.js @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule NavigatorBreadcrumbNavigationBarStyles + */ +'use strict'; + +var Dimensions = require('Dimensions'); +var NavigatorNavigationBarStyles = require('NavigatorNavigationBarStyles'); + +var buildStyleInterpolator = require('buildStyleInterpolator'); +var merge = require('merge'); + +var SCREEN_WIDTH = Dimensions.get('window').width; +var NAV_BAR_HEIGHT = NavigatorNavigationBarStyles.General.NavBarHeight; + +var SPACING = 8; +var ICON_WIDTH = 40; +var SEPARATOR_WIDTH = 9; +var CRUMB_WIDTH = ICON_WIDTH + SEPARATOR_WIDTH; +var NAV_ELEMENT_HEIGHT = NAV_BAR_HEIGHT; + +var OPACITY_RATIO = 100; +var ICON_INACTIVE_OPACITY = 0.6; +var MAX_BREADCRUMBS = 10; + +var CRUMB_BASE = { + position: 'absolute', + flexDirection: 'row', + top: 0, + width: CRUMB_WIDTH, + height: NAV_ELEMENT_HEIGHT, + backgroundColor: 'transparent', +}; + +var ICON_BASE = { + width: ICON_WIDTH, + height: NAV_ELEMENT_HEIGHT, +}; + +var SEPARATOR_BASE = { + width: SEPARATOR_WIDTH, + height: NAV_ELEMENT_HEIGHT, +}; + +var TITLE_BASE = { + position: 'absolute', + top: 0, + height: NAV_ELEMENT_HEIGHT, + backgroundColor: 'transparent', + alignItems: 'flex-start', +}; + +var FIRST_TITLE_BASE = merge(TITLE_BASE, { + left: 0, + right: 0, +}); + +var RIGHT_BUTTON_BASE = { + position: 'absolute', + top: 0, + right: 0, + overflow: 'hidden', + opacity: 1, + height: NAV_ELEMENT_HEIGHT, + backgroundColor: 'transparent', +}; + +/** + * Precompute crumb styles so that they don't need to be recomputed on every + * interaction. + */ +var LEFT = []; +var CENTER = []; +var RIGHT = []; +for (var i = 0; i < MAX_BREADCRUMBS; i++) { + var crumbLeft = CRUMB_WIDTH * i + SPACING; + LEFT[i] = { + Crumb: merge(CRUMB_BASE, { left: crumbLeft }), + Icon: merge(ICON_BASE, { opacity: ICON_INACTIVE_OPACITY }), + Separator: merge(SEPARATOR_BASE, { opacity: 1 }), + Title: merge(TITLE_BASE, { left: crumbLeft, opacity: 0 }), + RightItem: merge(RIGHT_BUTTON_BASE, { opacity: 0 }), + }; + CENTER[i] = { + Crumb: merge(CRUMB_BASE, { left: crumbLeft }), + Icon: merge(ICON_BASE, { opacity: 1 }), + Separator: merge(SEPARATOR_BASE, { opacity: 0 }), + Title: merge(TITLE_BASE, { + left: crumbLeft + ICON_WIDTH, + opacity: 1, + }), + RightItem: merge(RIGHT_BUTTON_BASE, { opacity: 1 }), + }; + var crumbRight = crumbLeft + 50; + RIGHT[i] = { + Crumb: merge(CRUMB_BASE, { left: crumbRight}), + Icon: merge(ICON_BASE, { opacity: 0 }), + Separator: merge(SEPARATOR_BASE, { opacity: 0 }), + Title: merge(TITLE_BASE, { + left: crumbRight + ICON_WIDTH, + opacity: 0, + }), + RightItem: merge(RIGHT_BUTTON_BASE, { opacity: 0 }), + }; +} + +// Special case the CENTER state of the first scene. +CENTER[0] = { + Crumb: merge(CRUMB_BASE, {left: SPACING + CRUMB_WIDTH}), + Icon: merge(ICON_BASE, {opacity: 0}), + Separator: merge(SEPARATOR_BASE, {opacity: 0}), + Title: merge(FIRST_TITLE_BASE, {opacity: 1}), + RightItem: CENTER[0].RightItem, +}; +LEFT[0].Title = merge(FIRST_TITLE_BASE, {opacity: 0}); +RIGHT[0].Title = merge(FIRST_TITLE_BASE, {opacity: 0}); + + +var buildIndexSceneInterpolator = function(startStyles, endStyles) { + return { + Crumb: buildStyleInterpolator({ + translateX: { + type: 'linear', + from: 0, + to: endStyles.Crumb.left - startStyles.Crumb.left, + min: 0, + max: 1, + extrapolate: true, + }, + left: { + value: startStyles.Crumb.left, + type: 'constant' + }, + }), + Icon: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.Icon.opacity, + to: endStyles.Icon.opacity, + min: 0, + max: 1, + }, + }), + Separator: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.Separator.opacity, + to: endStyles.Separator.opacity, + min: 0, + max: 1, + }, + }), + Title: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.Title.opacity, + to: endStyles.Title.opacity, + min: 0, + max: 1, + }, + translateX: { + type: 'linear', + from: 0, + to: endStyles.Title.left - startStyles.Title.left, + min: 0, + max: 1, + extrapolate: true, + }, + left: { + value: startStyles.Title.left, + type: 'constant' + }, + }), + RightItem: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.RightItem.opacity, + to: endStyles.RightItem.opacity, + min: 0, + max: 1, + round: OPACITY_RATIO, + }, + }), + }; +}; + +var Interpolators = CENTER.map(function(_, ii) { + return { + // Animating *into* the center stage from the right + RightToCenter: buildIndexSceneInterpolator(RIGHT[ii], CENTER[ii]), + // Animating out of the center stage, to the left + CenterToLeft: buildIndexSceneInterpolator(CENTER[ii], LEFT[ii]), + // Both stages (animating *past* the center stage) + RightToLeft: buildIndexSceneInterpolator(RIGHT[ii], LEFT[ii]), + }; +}); + +/** + * Contains constants that are used in constructing both `StyleSheet`s and + * inline styles during transitions. + */ +module.exports = { + Interpolators, + Left: LEFT, + Center: CENTER, + Right: RIGHT, + IconWidth: ICON_WIDTH, + IconHeight: NAV_BAR_HEIGHT, + SeparatorWidth: SEPARATOR_WIDTH, + SeparatorHeight: NAV_BAR_HEIGHT, +}; diff --git a/Libraries/Components/Navigator/NavigatorNavigationBarStyles.android.js b/Libraries/Components/Navigator/NavigatorNavigationBarStyles.android.js new file mode 100644 index 0000000000..b3cf275808 --- /dev/null +++ b/Libraries/Components/Navigator/NavigatorNavigationBarStyles.android.js @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule NavigatorNavigationBarStyles + */ +'use strict'; + +var buildStyleInterpolator = require('buildStyleInterpolator'); +var merge = require('merge'); + +// Android Material Design +var NAV_BAR_HEIGHT = 56; +var TITLE_LEFT = 72; +var BUTTON_SIZE = 24; +var TOUCH_TARGT_SIZE = 48; +var BUTTON_HORIZONTAL_MARGIN = 16; + +var BUTTON_EFFECTIVE_MARGIN = BUTTON_HORIZONTAL_MARGIN - (TOUCH_TARGT_SIZE - BUTTON_SIZE) / 2; +var NAV_ELEMENT_HEIGHT = NAV_BAR_HEIGHT; + +var BASE_STYLES = { + Title: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + alignItems: 'flex-start', + height: NAV_ELEMENT_HEIGHT, + backgroundColor: 'transparent', + marginLeft: TITLE_LEFT, + }, + LeftButton: { + position: 'absolute', + top: 0, + left: BUTTON_EFFECTIVE_MARGIN, + overflow: 'hidden', + height: NAV_ELEMENT_HEIGHT, + backgroundColor: 'transparent', + }, + RightButton: { + position: 'absolute', + top: 0, + right: BUTTON_EFFECTIVE_MARGIN, + overflow: 'hidden', + alignItems: 'flex-end', + height: NAV_ELEMENT_HEIGHT, + backgroundColor: 'transparent', + }, +}; + +// There are 3 stages: left, center, right. All previous navigation +// items are in the left stage. The current navigation item is in the +// center stage. All upcoming navigation items are in the right stage. +// Another way to think of the stages is in terms of transitions. When +// we move forward in the navigation stack, we perform a +// right-to-center transition on the new navigation item and a +// center-to-left transition on the current navigation item. +var Stages = { + Left: { + Title: merge(BASE_STYLES.Title, { opacity: 0 }), + LeftButton: merge(BASE_STYLES.LeftButton, { opacity: 0 }), + RightButton: merge(BASE_STYLES.RightButton, { opacity: 0 }), + }, + Center: { + Title: merge(BASE_STYLES.Title, { opacity: 1 }), + LeftButton: merge(BASE_STYLES.LeftButton, { opacity: 1 }), + RightButton: merge(BASE_STYLES.RightButton, { opacity: 1 }), + }, + Right: { + Title: merge(BASE_STYLES.Title, { opacity: 0 }), + LeftButton: merge(BASE_STYLES.LeftButton, { opacity: 0 }), + RightButton: merge(BASE_STYLES.RightButton, { opacity: 0 }), + }, +}; + + +var opacityRatio = 100; + +function buildSceneInterpolators(startStyles, endStyles) { + return { + Title: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.Title.opacity, + to: endStyles.Title.opacity, + min: 0, + max: 1, + }, + left: { + type: 'linear', + from: startStyles.Title.left, + to: endStyles.Title.left, + min: 0, + max: 1, + extrapolate: true, + }, + }), + LeftButton: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.LeftButton.opacity, + to: endStyles.LeftButton.opacity, + min: 0, + max: 1, + round: opacityRatio, + }, + left: { + type: 'linear', + from: startStyles.LeftButton.left, + to: endStyles.LeftButton.left, + min: 0, + max: 1, + }, + }), + RightButton: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.RightButton.opacity, + to: endStyles.RightButton.opacity, + min: 0, + max: 1, + round: opacityRatio, + }, + left: { + type: 'linear', + from: startStyles.RightButton.left, + to: endStyles.RightButton.left, + min: 0, + max: 1, + extrapolate: true, + }, + }), + }; +} + +var Interpolators = { + // Animating *into* the center stage from the right + RightToCenter: buildSceneInterpolators(Stages.Right, Stages.Center), + // Animating out of the center stage, to the left + CenterToLeft: buildSceneInterpolators(Stages.Center, Stages.Left), + // Both stages (animating *past* the center stage) + RightToLeft: buildSceneInterpolators(Stages.Right, Stages.Left), +}; + + +module.exports = { + General: { + NavBarHeight: NAV_BAR_HEIGHT, + StatusBarHeight: 0, + TotalNavHeight: NAV_BAR_HEIGHT, + }, + Interpolators, + Stages, +}; diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js new file mode 100644 index 0000000000..307a2bad41 --- /dev/null +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ProgressBarAndroid + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var React = require('React'); +var ReactPropTypes = require('ReactPropTypes'); +var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); + +var createReactNativeComponentClass = require('createReactNativeComponentClass'); + +var STYLE_ATTRIBUTES = [ + 'Horizontal', + 'Small', + 'Large', + 'Inverse', + 'SmallInverse', + 'LargeInverse' +]; + +/** + * React component that wraps the Android-only `ProgressBar`. This component is used to indicate + * that the app is loading or there is some activity in the app. + * + * Example: + * + * ``` + * render: function() { + * var progressBar = + * + * + * ; + + * return ( + * + * ); + * }, + * ``` + */ +var ProgressBarAndroid = React.createClass({ + propTypes: { + /** + * Style of the ProgressBar. One of: + * + * - Horizontal + * - Small + * - Large + * - Inverse + * - SmallInverse + * - LargeInverse + */ + styleAttr: ReactPropTypes.oneOf(STYLE_ATTRIBUTES), + /** + * Used to locate this view in end-to-end tests. + */ + testID: ReactPropTypes.string, + }, + + getDefaultProps: function() { + return { + styleAttr: 'Large', + }; + }, + + mixins: [NativeMethodsMixin], + + render: function() { + return ; + }, +}); + +var AndroidProgressBar = createReactNativeComponentClass({ + validAttributes: { + ...ReactNativeViewAttributes.UIView, + styleAttr: true, + }, + uiViewClassName: 'AndroidProgressBar', +}); + +module.exports = ProgressBarAndroid; diff --git a/Libraries/Components/SliderIOS/SliderIOS.android.js b/Libraries/Components/SliderIOS/SliderIOS.android.js new file mode 100644 index 0000000000..714972f988 --- /dev/null +++ b/Libraries/Components/SliderIOS/SliderIOS.android.js @@ -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. + * + * @providesModule SliderIOS + */ + +'use strict'; + +module.exports = require('UnimplementedView'); diff --git a/Libraries/Components/StatusBar/StatusBarIOS.android.js b/Libraries/Components/StatusBar/StatusBarIOS.android.js new file mode 100644 index 0000000000..d4ffd63f08 --- /dev/null +++ b/Libraries/Components/StatusBar/StatusBarIOS.android.js @@ -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. + * + * @providesModule StatusBarIOS + * @flow + */ +'use strict'; + +module.exports = null; diff --git a/Libraries/Components/SwitchAndroid/SwitchAndroid.android.js b/Libraries/Components/SwitchAndroid/SwitchAndroid.android.js new file mode 100644 index 0000000000..1d4d604952 --- /dev/null +++ b/Libraries/Components/SwitchAndroid/SwitchAndroid.android.js @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule SwitchAndroid + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var PropTypes = require('ReactPropTypes'); +var React = require('React'); + +var requireNativeComponent = require('requireNativeComponent'); + +var SWITCH = 'switch'; + +/** + * Standard Android two-state toggle component + */ +var SwitchAndroid = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + /** + * Boolean value of the switch. + */ + value: PropTypes.bool, + /** + * If `true`, this component can't be interacted with. + */ + disabled: PropTypes.bool, + /** + * Invoked with the new value when the value chages. + */ + onValueChange: PropTypes.func, + /** + * Used to locate this view in end-to-end tests. + */ + testID: PropTypes.string, + }, + + getDefaultProps: function() { + return { + value: false, + disabled: false, + }; + }, + + _onChange: function(event) { + this.props.onChange && this.props.onChange(event); + this.props.onValueChange && this.props.onValueChange(event.nativeEvent.value); + + // The underlying switch might have changed, but we're controlled, + // and so want to ensure it represents our value. + this.refs[SWITCH].setNativeProps({on: this.props.value}); + }, + + render: function() { + return ( + true} + onResponderTerminationRequest={() => false} + /> + ); + } +}); + +var RKSwitch = requireNativeComponent('AndroidSwitch', null); + +module.exports = SwitchAndroid; diff --git a/Libraries/Components/ToastAndroid/ToastAndroid.android.js b/Libraries/Components/ToastAndroid/ToastAndroid.android.js new file mode 100644 index 0000000000..ae06b3f039 --- /dev/null +++ b/Libraries/Components/ToastAndroid/ToastAndroid.android.js @@ -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. + * + * @providesModule ToastAndroid + */ + +'use strict'; + +var RCTToastAndroid = require('NativeModules').ToastAndroid; + +/** + * This exposes the native ToastAndroid module as a JS module. This has a function 'showText' + * which takes the following parameters: + * + * 1. String message: A string with the text to toast + * 2. int duration: The duration of the toast. May be ToastAndroid.SHORT or ToastAndroid.LONG + */ + +var ToastAndroid = { + + SHORT: RCTToastAndroid.SHORT, + LONG: RCTToastAndroid.LONG, + + show: function ( + message: string, + duration: number + ): void { + RCTToastAndroid.show(message, duration); + }, + +}; + +module.exports = ToastAndroid; diff --git a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js new file mode 100644 index 0000000000..961b59ac46 --- /dev/null +++ b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js @@ -0,0 +1,174 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ToolbarAndroid + */ + +'use strict'; + +var Image = require('Image'); +var NativeMethodsMixin = require('NativeMethodsMixin'); +var React = require('React'); +var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); +var ReactPropTypes = require('ReactPropTypes'); + +var createReactNativeComponentClass = require('createReactNativeComponentClass'); + +/** + * React component that wraps the Android-only [`Toolbar` widget][0]. A Toolbar can display a logo, + * navigation icon (e.g. hamburger menu), a title & subtitle and a list of actions. The title and + * subtitle are expanded so the logo and navigation icons are displayed on the left, title and + * subtitle in the middle and the actions on the right. + * + * If the toolbar has an only child, it will be displayed between the title and actions. + * + * Example: + * + * ``` + * render: function() { + * return ( + * + * ) + * }, + * onActionSelected: function(position) { + * if (position === 0) { // index of 'Settings' + * showSettings(); + * } + * } + * ``` + * + * [0]: https://developer.android.com/reference/android/support/v7/widget/Toolbar.html + */ +var ToolbarAndroid = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + /** + * Sets possible actions on the toolbar as part of the action menu. These are displayed as icons + * or text on the right side of the widget. If they don't fit they are placed in an 'overflow' + * menu. + * + * This property takes an array of objects, where each object has the following keys: + * + * * `title`: **required**, the title of this action + * * `icon`: the icon for this action, e.g. `require('image!some_icon')` + * * `show`: when to show this action as an icon or hide it in the overflow menu: `always`, + * `ifRoom` or `never` + * * `showWithText`: boolean, whether to show text alongside the icon or not + */ + actions: ReactPropTypes.arrayOf(ReactPropTypes.shape({ + title: ReactPropTypes.string.isRequired, + icon: Image.propTypes.source, + show: ReactPropTypes.oneOf(['always', 'ifRoom', 'never']), + showWithText: ReactPropTypes.bool + })), + /** + * Sets the toolbar logo. + */ + logo: Image.propTypes.source, + /** + * Sets the navigation icon. + */ + navIcon: Image.propTypes.source, + /** + * Callback that is called when an action is selected. The only argument that is passeed to the + * callback is the position of the action in the actions array. + */ + onActionSelected: ReactPropTypes.func, + /** + * Callback called when the icon is selected. + */ + onIconClicked: ReactPropTypes.func, + /** + * Sets the toolbar subtitle. + */ + subtitle: ReactPropTypes.string, + /** + * Sets the toolbar subtitle color. + */ + subtitleColor: ReactPropTypes.string, + /** + * Sets the toolbar title. + */ + title: ReactPropTypes.string, + /** + * Sets the toolbar title color. + */ + titleColor: ReactPropTypes.string, + /** + * Used to locate this view in end-to-end tests. + */ + testID: ReactPropTypes.string, + }, + + render: function() { + var nativeProps = { + ...this.props, + }; + if (this.props.logo) { + if (!this.props.logo.isStatic) { + throw 'logo prop should be a static image (obtained via ix)'; + } + nativeProps.logo = this.props.logo.uri; + } + if (this.props.navIcon) { + if (!this.props.navIcon.isStatic) { + throw 'navIcon prop should be static image (obtained via ix)'; + } + nativeProps.navIcon = this.props.navIcon.uri; + } + if (this.props.actions) { + nativeProps.actions = []; + for (var i = 0; i < this.props.actions.length; i++) { + var action = { + ...this.props.actions[i], + }; + if (action.icon) { + if (!action.icon.isStatic) { + throw 'action icons should be static images (obtained via ix)'; + } + action.icon = action.icon.uri; + } + nativeProps.actions.push(action); + } + } + + return ; + }, + + _onSelect: function(event) { + var position = event.nativeEvent.position; + if (position === -1) { + this.props.onIconClicked && this.props.onIconClicked(); + } else { + this.props.onActionSelected && this.props.onActionSelected(position); + } + }, +}); + +var toolbarAttributes = { + ...ReactNativeViewAttributes.UIView, + actions: true, + logo: true, + navIcon: true, + subtitle: true, + subtitleColor: true, + title: true, + titleColor: true, +}; + +var NativeToolbar = createReactNativeComponentClass({ + validAttributes: toolbarAttributes, + uiViewClassName: 'ToolbarAndroid', +}); + +module.exports = ToolbarAndroid; diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js new file mode 100644 index 0000000000..98b59c3c30 --- /dev/null +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule TouchableNativeFeedback + */ +'use strict'; + +var PropTypes = require('ReactPropTypes'); +var RCTUIManager = require('NativeModules').UIManager; +var React = require('React'); +var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); +var Touchable = require('Touchable'); +var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); + +var createReactNativeComponentClass = require('createReactNativeComponentClass'); +var createStrictShapeTypeChecker = require('createStrictShapeTypeChecker'); +var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); +var onlyChild = require('onlyChild'); + +var rippleBackgroundPropType = createStrictShapeTypeChecker({ + type: React.PropTypes.oneOf(['RippleAndroid']), + color: PropTypes.string, + borderless: PropTypes.bool, +}); + +var themeAttributeBackgroundPropType = createStrictShapeTypeChecker({ + type: React.PropTypes.oneOf(['ThemeAttrAndroid']), + attribute: PropTypes.string.isRequired, +}); + +var backgroundPropType = PropTypes.oneOfType([ + rippleBackgroundPropType, + themeAttributeBackgroundPropType, +]); + +var TouchableView = createReactNativeComponentClass({ + validAttributes: { + ...ReactNativeViewAttributes.UIView, + nativeBackgroundAndroid: backgroundPropType, + }, + uiViewClassName: 'RCTView', +}); + +var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; + +/** + * A wrapper for making views respond properly to touches (Android only). + * On Android this component uses native state drawable to display touch + * feedback. At the moment it only supports having a single View instance as a + * child node, as it's implemented by replacing that View with another instance + * of RCTView node with some additional properties set. + * + * Background drawable of native feedback touchable can be customized with + * `background` property. + * + * Example: + * + * ``` + * renderButton: function() { + * return ( + * + * + * Button + * + * + * ); + * }, + * ``` + */ + +var TouchableNativeFeedback = React.createClass({ + propTypes: { + ...TouchableWithoutFeedback.propTypes, + + /** + * Determines the type of background drawable that's going to be used to + * display feedback. It takes an object with `type` property and extra data + * depending on the `type`. It's recommended to use one of the following + * static methods to generate that dictionary: + * + * 1) TouchableNativeFeedback.SelectableBackground() - will create object + * that represents android theme's default background for selectable + * elements (?android:attr/selectableItemBackground) + * + * 2) TouchableNativeFeedback.SelectableBackgroundBorderless() - will create + * object that represent android theme's default background for borderless + * selectable elements (?android:attr/selectableItemBackgroundBorderless). + * Available on android API level 21+ + * + * 3) TouchableNativeFeedback.RippleAndroid(color, borderless) - will create + * object that represents ripple drawable with specified color (as a + * string). If property `borderless` evaluates to true the ripple will + * render outside of the view bounds (see native actionbar buttons as an + * example of that behavior). This background type is available on Android + * API level 21+ + */ + background: backgroundPropType, + }, + + statics: { + SelectableBackground: function() { + return {type: 'ThemeAttrAndroid', attribute: 'selectableItemBackground'}; + }, + SelectableBackgroundBorderless: function() { + return {type: 'ThemeAttrAndroid', attribute: 'selectableItemBackgroundBorderless'}; + }, + Ripple: function(color, borderless) { + return {type: 'RippleAndroid', color: color, borderless: borderless}; + }, + }, + + mixins: [Touchable.Mixin], + + getDefaultProps: function() { + return { + background: this.SelectableBackground(), + }; + }, + + getInitialState: function() { + return this.touchableGetInitialState(); + }, + + componentDidMount: function() { + ensurePositiveDelayProps(this.props); + }, + + componentWillReceiveProps: function(nextProps) { + ensurePositiveDelayProps(nextProps); + }, + + /** + * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are + * defined on your component. + */ + touchableHandleActivePressIn: function() { + this.props.onPressIn && this.props.onPressIn(); + this._dispatchPressedStateChange(true); + this._dispatchHotspotUpdate(this.pressInLocation.pageX, this.pressInLocation.pageY); + }, + + touchableHandleActivePressOut: function() { + this.props.onPressOut && this.props.onPressOut(); + this._dispatchPressedStateChange(false); + }, + + touchableHandlePress: function() { + this.props.onPress && this.props.onPress(); + }, + + touchableHandleLongPress: function() { + this.props.onLongPress && this.props.onLongPress(); + }, + + touchableGetPressRectOffset: function() { + return PRESS_RECT_OFFSET; // Always make sure to predeclare a constant! + }, + + touchableGetHighlightDelayMS: function() { + return this.props.delayPressIn; + }, + + touchableGetLongPressDelayMS: function() { + return this.props.delayLongPress; + }, + + touchableGetPressOutDelayMS: function() { + return this.props.delayPressOut; + }, + + _handleResponderMove: function(e) { + this.touchableHandleResponderMove(e); + this._dispatchHotspotUpdate(e.nativeEvent.pageX, e.nativeEvent.pageY); + }, + + _dispatchHotspotUpdate: function(destX, destY) { + RCTUIManager.dispatchViewManagerCommand( + React.findNodeHandle(this), + RCTUIManager.RCTView.Commands.hotspotUpdate, + [destX || 0, destY || 0] + ); + }, + + _dispatchPressedStateChange: function(pressed) { + RCTUIManager.dispatchViewManagerCommand( + React.findNodeHandle(this), + RCTUIManager.RCTView.Commands.setPressed, + [pressed] + ); + }, + + render: function() { + var childProps = { + ...onlyChild(this.props.children).props, + nativeBackgroundAndroid: this.props.background, + accessible: this.props.accessible !== false, + accessibilityComponentType: this.props.accessibilityComponentType, + accessibilityTraits: this.props.accessibilityTraits, + testID: this.props.testID, + onLayout: this.props.onLayout, + onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, + onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, + onResponderGrant: this.touchableHandleResponderGrant, + onResponderMove: this._handleResponderMove, + onResponderRelease: this.touchableHandleResponderRelease, + onResponderTerminate: this.touchableHandleResponderTerminate, + }; + return ; + } +}); + +module.exports = TouchableNativeFeedback; diff --git a/Libraries/Image/Image.android.js b/Libraries/Image/Image.android.js new file mode 100644 index 0000000000..7b055780f0 --- /dev/null +++ b/Libraries/Image/Image.android.js @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule Image + * @flow + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var NativeModules = require('NativeModules'); +var ImageResizeMode = require('ImageResizeMode'); +var ImageStylePropTypes = require('ImageStylePropTypes'); +var PropTypes = require('ReactPropTypes'); +var React = require('React'); +var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); +var StyleSheet = require('StyleSheet'); +var StyleSheetPropType = require('StyleSheetPropType'); +var View = require('View'); + +var createReactNativeComponentClass = require('createReactNativeComponentClass'); +var flattenStyle = require('flattenStyle'); +var invariant = require('invariant'); +var merge = require('merge'); +var resolveAssetSource = require('resolveAssetSource'); + +/** + * - A react component for displaying different types of images, + * including network images, static resources, temporary local images, and + * images from local disk, such as the camera roll. Example usage: + * + * renderImages: function() { + * return ( + * + * + * + * + * ); + * }, + * + * More example code in ImageExample.js + */ + +var ImageViewAttributes = merge(ReactNativeViewAttributes.UIView, { + src: true, + resizeMode: true, +}); + +var Image = React.createClass({ + propTypes: { + source: PropTypes.shape({ + /** + * A string representing the resource identifier for the image, which + * could be an http address, a local file path, or the name of a static image + * resource (which should be wrapped in the `ix` function). + */ + uri: PropTypes.string, + }).isRequired, + style: StyleSheetPropType(ImageStylePropTypes), + /** + * Used to locate this view in end-to-end tests. + */ + testID: PropTypes.string, + }, + + statics: { + resizeMode: ImageResizeMode, + }, + + mixins: [NativeMethodsMixin], + + /** + * `NativeMethodsMixin` will look for this when invoking `setNativeProps`. We + * make `this` look like an actual native component class. Since it can render + * as 3 different native components we need to update viewConfig accordingly + */ + viewConfig: { + uiViewClassName: 'RCTView', + validAttributes: ReactNativeViewAttributes.RKView + }, + + _updateViewConfig: function(props) { + if (props.children) { + this.viewConfig = { + uiViewClassName: 'RCTView', + validAttributes: ReactNativeViewAttributes.RKView, + }; + } else { + this.viewConfig = { + uiViewClassName: 'RCTImageView', + validAttributes: ImageViewAttributes, + }; + } + }, + + componentWillMount: function() { + this._updateViewConfig(this.props); + }, + + componentWillReceiveProps: function(nextProps) { + this._updateViewConfig(nextProps); + }, + + render: function() { + var source = resolveAssetSource(this.props.source); + if (source && source.uri) { + var isNetwork = source.uri.match(/^https?:/); + invariant( + !(isNetwork && source.isStatic), + 'Static image URIs cannot start with "http": "' + source.uri + '"' + ); + + var {width, height} = source; + var style = flattenStyle([{width, height}, styles.base, this.props.style]); + + var nativeProps = merge(this.props, { + style, + src: source.uri, + }); + + if (nativeProps.children) { + // TODO(6033040): Consider implementing this as a separate native component + var imageProps = merge(nativeProps, { + style: styles.absoluteImage, + children: undefined, + }); + return ( + + + {this.props.children} + + ); + } else { + return ; + } + } + return null; + } +}); + +var styles = StyleSheet.create({ + base: { + overflow: 'hidden', + }, + absoluteImage: { + left: 0, + right: 0, + top: 0, + bottom: 0, + position: 'absolute' + } +}); + +var RKImage = createReactNativeComponentClass({ + validAttributes: ImageViewAttributes, + uiViewClassName: 'RCTImageView', +}); + +module.exports = Image; diff --git a/Libraries/Network/RCTNetworking.android.js b/Libraries/Network/RCTNetworking.android.js new file mode 100644 index 0000000000..8d21d81335 --- /dev/null +++ b/Libraries/Network/RCTNetworking.android.js @@ -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. + * + * @providesModule RCTNetworking + */ +'use strict'; + +// Do not require the native RCTNetworking module directly! Use this wrapper module instead. +// It will add the necessary requestId, so that you don't have to generate it yourself. +var RCTNetworkingNative = require('NativeModules').Networking; + +var _requestId = 1; +var generateRequestId = function() { + return _requestId++; +}; + +/** + * This class is a wrapper around the native RCTNetworking module. It adds a necessary unique + * requestId to each network request that can be used to abort that request later on. + */ +class RCTNetworking { + + static sendRequest(method, url, headers, data, callback) { + var requestId = generateRequestId(); + RCTNetworkingNative.sendRequest( + method, + url, + requestId, + headers, + data, + callback); + return requestId; + } + + static abortRequest(requestId) { + RCTNetworkingNative.abortRequest(requestId); + } +} + +module.exports = RCTNetworking; diff --git a/Libraries/Network/XMLHttpRequest.android.js b/Libraries/Network/XMLHttpRequest.android.js new file mode 100644 index 0000000000..ba7a84ad96 --- /dev/null +++ b/Libraries/Network/XMLHttpRequest.android.js @@ -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. + * + * @providesModule XMLHttpRequest + * @flow + */ +'use strict'; + +var FormData = require('FormData'); +var RCTNetworking = require('RCTNetworking'); +var XMLHttpRequestBase = require('XMLHttpRequestBase'); + +type Header = [string, string]; + +function convertHeadersMapToArray(headers: Object): Array

{ + var headerArray = []; + for (var name in headers) { + headerArray.push([name, headers[name]]); + } + return headerArray; +} + +class XMLHttpRequest extends XMLHttpRequestBase { + + _requestId: ?number; + + constructor() { + super(); + this._requestId = null; + } + + sendImpl(method: ?string, url: ?string, headers: Object, data: any): void { + var body; + if (typeof data === 'string') { + body = {string: data}; + } else if (data instanceof FormData) { + body = { + formData: data.getParts().map((part) => { + part.headers = convertHeadersMapToArray(part.headers); + return part; + }), + }; + } else { + body = data; + } + + this._requestId = RCTNetworking.sendRequest( + method, + url, + convertHeadersMapToArray(headers), + body, + this.callback.bind(this) + ); + } + + abortImpl(): void { + this._requestId && RCTNetworking.abortRequest(this._requestId); + } +} + +module.exports = XMLHttpRequest; diff --git a/Libraries/ReactIOS/renderApplication.android.js b/Libraries/ReactIOS/renderApplication.android.js new file mode 100644 index 0000000000..d299c2eb38 --- /dev/null +++ b/Libraries/ReactIOS/renderApplication.android.js @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule renderApplication + */ +'use strict'; + +var Inspector = require('Inspector'); +var Portal = require('Portal'); +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Subscribable = require('Subscribable'); +var View = require('View'); + +var invariant = require('invariant'); + +// require BackAndroid so it sets the default handler that exits the app if no listeners respond +require('BackAndroid'); + +var AppContainer = React.createClass({ + mixins: [Subscribable.Mixin], + + getInitialState: function() { + return { + enabled: __DEV__, + inspectorVisible: false, + rootNodeHandle: null, + rootImportanceForAccessibility: 'auto', + }; + }, + + toggleElementInspector: function() { + this.setState({ + inspectorVisible: !this.state.inspectorVisible, + rootNodeHandle: React.findNodeHandle(this.refs.main), + }); + }, + + componentDidMount: function() { + this.addListenerOn( + RCTDeviceEventEmitter, + 'toggleElementInspector', + this.toggleElementInspector + ); + + this._unmounted = false; + }, + + renderInspector: function() { + return this.state.inspectorVisible ? + : + null; + }, + + componentWillUnmount: function() { + this._unmounted = true; + }, + + setRootAccessibility: function(modalVisible) { + if (this._unmounted) { + return; + } + + this.setState({ + rootImportanceForAccessibility: modalVisible ? 'no-hide-descendants' : 'auto', + }); + }, + + render: function() { + var RootComponent = this.props.rootComponent; + var appView = + + + + ; + + return this.state.enabled ? + + {appView} + {this.renderInspector()} + : + appView; + } +}); + +function renderApplication( + RootComponent: ReactClass, + initialProps: P, + rootTag: any +) { + invariant( + rootTag, + 'Expect to have a valid rootTag, instead got ', rootTag + ); + React.render( + , + rootTag + ); +} + +var styles = StyleSheet.create({ + // This is needed so the application covers the whole screen + // and therefore the contents of the Portal are not clipped. + appContainer: { + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + }, +}); + +module.exports = renderApplication; diff --git a/Libraries/Storage/AsyncStorage.android.js b/Libraries/Storage/AsyncStorage.android.js new file mode 100644 index 0000000000..8b2fa312ad --- /dev/null +++ b/Libraries/Storage/AsyncStorage.android.js @@ -0,0 +1,233 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AsyncStorage + * @flow + */ +'use strict'; + +var RCTAsyncStorage = require('NativeModules').AsyncSQLiteDBStorage; + +/** + * AsyncStorage is a simple, asynchronous, persistent, global, key-value storage system. + * + * It is recommended that you use an abstraction on top of AsyncStorage instead of AsyncStorage + * directly for anything more than light usage since it operates globally. + * + * This JS code is a simple facade over the native android implementation to provide a clear + * JS API, real Error objects, and simple non-multi functions. + */ +var AsyncStorage = { + /** + * Fetches `key` and passes the result to `callback`, along with an `Error` if + * there is any. Returns a `Promise` object. + */ + getItem: function( + key: string, + callback?: ?(error: ?Error, result: ?string) => void + ) { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiGet([key], function(error, result) { + var value = (result && result[0] && result[0][1]) ? result[0][1] : null; + callback && callback((error && convertError(error)) || null, value); + if (error) { + reject(convertError(error)); + } else { + resolve(value); + } + }); + }); + }, + /** + * Sets `value` for `key` and calls `callback` on completion, along with an + * `Error` if there is any. Returns a `Promise` object. + */ + setItem: function( + key: string, + value: string, + callback?: ?(error: ?Error) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiSet([[key,value]], function(error) { + callback && callback((error && convertError(error)) || null); + if (error) { + reject(convertError(error)); + } else { + resolve(null); + } + }); + }); + }, + /** + * Returns a `Promise` object. + */ + removeItem: function( + key: string, + callback?: ?(error: ?Error) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiRemove([key], function(error) { + callback && callback((error && convertError(error)) || null); + if (error) { + reject(convertError(error)); + } else { + resolve(null); + } + }); + }); + }, + /** + * Merges existing value with input value, assuming they are stringified json. + * Returns a `Promise` object. + */ + mergeItem: function( + key: string, + value: string, + callback?: ?(error: ?Error) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiMerge([[key,value]], function(error) { + callback && callback((error && convertError(error)) || null); + if (error) { + reject(convertError(error)); + } else { + resolve(null); + } + }); + }); + }, + /** + * Erases *all* AsyncStorage for all clients, libraries, etc. You probably + * don't want to call this - use removeItem or multiRemove to clear only your + * own keys instead. Returns a `Promise` object. + */ + clear: function(callback?: ?(error: ?Error) => void): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.clear(function(error) { + callback && callback(convertError(error) || null); + if (error) { + reject(convertError(error)); + } else { + resolve(null); + } + }); + }); + }, + /** + * Gets *all* keys known to the app, for all callers, libraries, etc. Returns a `Promise` object. + */ + getAllKeys: function(callback?: ?(error: ?Error, keys: ?Array) => void): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.getAllKeys(function(error, keys) { + callback && callback((error && convertError(error)) || null, keys); + if (error) { + reject(convertError(error)); + } else { + resolve(keys); + } + }); + }); + }, + /** + * The following batched functions are useful for executing a lot of + * operations at once, allowing for native optimizations and provide the + * convenience of a single callback after all operations are complete. + * + * In case of errors, these functions return the first encountered error and abort. + */ + + /** + * multiGet invokes callback with an array of key-value pair arrays that + * matches the input format of multiSet. Returns a `Promise` object. + * + * multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']]) + */ + multiGet: function( + keys: Array, + callback?: ?(errors: ?Array, result: ?Array>) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiGet(keys, function(error, result) { + callback && callback((error && convertError(error)) || null, result); + if (error) { + reject(convertError(error)); + } else { + resolve(result); + } + }); + }); + }, + /** + * multiSet and multiMerge take arrays of key-value array pairs that match + * the output of multiGet, e.g. Returns a `Promise` object. + * + * multiSet([['k1', 'val1'], ['k2', 'val2']], cb); + */ + multiSet: function( + keyValuePairs: Array>, + callback?: ?(errors: ?Array) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiSet(keyValuePairs, function(error) { + callback && callback((error && convertError(error)) || null); + if (error) { + reject(convertError(error)); + } else { + resolve(null); + } + }); + }); + }, + /** + * Delete all the keys in the `keys` array. Returns a `Promise` object. + */ + multiRemove: function( + keys: Array, + callback?: ?(errors: ?Array) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiRemove(keys, function(error) { + callback && callback((error && convertError(error)) || null); + if (error) { + reject(convertError(error)); + } else { + resolve(null); + } + }); + }); + }, + /** + * Merges existing values with input values, assuming they are stringified + * json. Returns a `Promise` object. + */ + multiMerge: function( + keyValuePairs: Array>, + callback?: ?(errors: ?Array) => void + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiMerge(keyValuePairs, function(error) { + callback && callback((error && convertError(error)) || null); + if (error) { + reject(convertError(error)); + } else { + resolve(null); + } + }); + }); + }, +}; + +function convertError(error) { + if (!error) { + return null; + } + var out = new Error(error.message); + return [out]; +} + +module.exports = AsyncStorage; diff --git a/Libraries/Utilities/BackAndroid.android.js b/Libraries/Utilities/BackAndroid.android.js new file mode 100644 index 0000000000..d615b01cb4 --- /dev/null +++ b/Libraries/Utilities/BackAndroid.android.js @@ -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. + * + * Detect hardware back button presses, and programatically invoke the default back button + * functionality to exit the app if there are no listeners or if none of the listeners return true. + * + * @providesModule BackAndroid + */ + +'use strict'; + +var Set = require('Set'); +var DeviceEventManager = require('NativeModules').DeviceEventManager; +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); + +var DEVICE_BACK_EVENT = 'hardwareBackPress'; + +type BackPressEventName = $Enum<{ + backPress: string; +}>; + +var _backPressSubscriptions = new Set(); + +RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function() { + var invokeDefault = true; + _backPressSubscriptions.forEach((subscription) => { + if (subscription()) { + invokeDefault = false; + } + }); + if (invokeDefault) { + BackAndroid.exitApp(); + } +}); + +var BackAndroid = { + + exitApp: function() { + DeviceEventManager.invokeDefaultBackPressHandler(); + }, + + addEventListener: function ( + eventName: BackPressEventName, + handler: Function + ): void { + _backPressSubscriptions.add(handler); + }, + + removeEventListener: function( + eventName: BackPressEventName, + handler: Function + ): void { + _backPressSubscriptions.delete(handler); + }, + +}; + +module.exports = BackAndroid; diff --git a/Libraries/Utilities/Platform.android.js b/Libraries/Utilities/Platform.android.js new file mode 100644 index 0000000000..2d061d4148 --- /dev/null +++ b/Libraries/Utilities/Platform.android.js @@ -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. + * + * @providesModule Platform + * @flow + */ + +'use strict'; + +var AndroidConstants = require('NativeModules').AndroidConstants; + +var Platform = { + OS: 'android', + Version: AndroidConstants.Version, +}; + +module.exports = Platform; diff --git a/Libraries/vendor/react/vendor/core/ExecutionEnvironment.android.js b/Libraries/vendor/react/vendor/core/ExecutionEnvironment.android.js new file mode 100644 index 0000000000..23116e61cd --- /dev/null +++ b/Libraries/vendor/react/vendor/core/ExecutionEnvironment.android.js @@ -0,0 +1,51 @@ +/** + * Copyright 2013-2014 Facebook, Inc. + * + * 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. + * + * @providesModule ExecutionEnvironment + * + * NB: This is a temporary override that has not yet been merged upstream. It is NOT actually part + * of react-core (yet) + * + * Stubs SignedSource<<7afee88a05412d0c4eef54817419648e>> + */ + +/*jslint evil: true */ + +"use strict"; + +var canUseDOM = false; + +/** + * Simple, lightweight module assisting with the detection and context of + * Worker. Helps avoid circular dependencies and allows code to reason about + * whether or not they are in a Worker, even if they never include the main + * `ReactWorker` dependency. + */ +var ExecutionEnvironment = { + + canUseDOM: canUseDOM, + + canUseWorkers: typeof Worker !== 'undefined', + + canUseEventListeners: + canUseDOM && !!(window.addEventListener || window.attachEvent), + + canUseViewport: canUseDOM && !!window.screen, + + isInWorker: !canUseDOM // For now, this is true - might change in the future. + +}; + +module.exports = ExecutionEnvironment; diff --git a/README.md b/README.md index 7183879436..6d9c17996e 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,11 @@ Now open any example (the `.xcodeproj` file in each of the `Examples` subdirecto - Looking for a component? [react.parts](http://react.parts/) - Fellow developers write and publish React Native modules to npm and open source them on GitHub. - Making modules helps grow the React Native ecosystem and community. We recommend writing modules for your use cases and sharing them on npm. -- Read the [Native Modules iOS](http://facebook.github.io/react-native/docs/native-modules-ios.html#content) and [Native UI Components iOS](http://facebook.github.io/react-native/docs/native-components-ios.html#content) guides in the documentation if you are interested in extending native functionality. +- Read the guides on Native Modules ([iOS](http://facebook.github.io/react-native/docs/native-modules-ios.html), [Android](http://facebook.github.io/react-native/docs/native-modules-android.html)) and Native UI Components ([iOS](http://facebook.github.io/react-native/docs/native-components-ios.html), [Android](http://facebook.github.io/react-native/docs/native-components-android.html)) if you are interested in extending native functionality. ## Opening Issues -If you encounter a bug with React Native we would like to hear about it. Search the [existing issues](https://github.com/facebook/react-native/issues) and try to make sure your problem doesn’t already exist before opening a new issue. It’s helpful if you include the version of React Native and iOS you’re using. Please include a stack trace and reduced repro case when appropriate, too. +If you encounter a bug with React Native we would like to hear about it. Search the [existing issues](https://github.com/facebook/react-native/issues) and try to make sure your problem doesn’t already exist before opening a new issue. It’s helpful if you include the version of React Native and OS you’re using. Please include a stack trace and reduced repro case when appropriate, too. The GitHub issues are intended for bug reports and feature requests. For help and questions with using React Native please make use of the resources listed in the [Getting Help](#getting-help) section. There are limited resources available for handling issues and by keeping the list of open issues lean we can respond in a timely manner. diff --git a/React.podspec b/React.podspec index 9634b69644..3a71dc38a0 100644 --- a/React.podspec +++ b/React.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "React" - s.version = "0.8.0" + s.version = "0.11.0-rc" s.summary = "Build high quality mobile apps using React." s.description = <<-DESC React Native apps are built using the React JS diff --git a/ReactAndroid/DevExperience.md b/ReactAndroid/DevExperience.md new file mode 100644 index 0000000000..2defb47203 --- /dev/null +++ b/ReactAndroid/DevExperience.md @@ -0,0 +1,23 @@ +Here's how to test the whole dev experience end-to-end. This will be eventually merged into the [Getting Started guide](https://facebook.github.io/react-native/docs/getting-started.html). + +Assuming you have the [Android SDK](https://developer.android.com/sdk/installing/index.html) installed, run `android` to open the Android SDK Manager. + +Make sure you have the following installed: + +- Android SDK version 23 +- SDK build tools version 23 +- Android Support Repository 17 (for Android Support Library) + +Follow steps on https://github.com/facebook/react-native/blob/master/react-native-cli/CONTRIBUTING.md, but be sure to bump the verison of react-native in package.json to some version > 0.9 (latest published npm version) or set up proxying properly for react-native + +- From the react-native-android repo: + - `./gradlew :ReactAndroid:installArchives` + - *Assuming you already have android-jsc installed to local maven repo, no steps included here* +- `react-native init ProjectName` +- Open up your Android emulator (Genymotion is recommended) +- `cd ProjectName` +- `react-native run-android` + +In case the app crashed: + +- Run `adb logcat` and try to find a Java exception diff --git a/ReactAndroid/README.md b/ReactAndroid/README.md new file mode 100644 index 0000000000..ee19fdbde9 --- /dev/null +++ b/ReactAndroid/README.md @@ -0,0 +1,101 @@ +# Building React Native for Android + +This guide contains instructions for building the Android code and running the sample apps. + +## Supported Operating Systems + +This setup has only been tested on Mac OS so far. + +## Prerequisites + +Assuming you have the [Android SDK](https://developer.android.com/sdk/installing/index.html) installed, run `android` to open the Android SDK Manager. + +Make sure you have the following installed: + +- Android SDK version 22 (compileSdkVersion in [`build.gradle`](build.gradle)) +- SDK build tools version 22.0.1 (buildToolsVersion in [`build.gradle`](build.gradle)) +- Android Support Repository 17 (for Android Support Library) + +Point Gradle to your Android SDK - in the root of your clone of the github repo, create a file called `local.properties` with the following contents: + + sdk.dir=absolute_path_to_android_sdk + ndk.dir=absolute_path_to_android_ndk + +Example: + + sdk.dir=/Users/your_unix_name/android-sdk-macosx + ndk.dir=/Users/your_unix_name/android-ndk/android-ndk-r10c + +## Run `npm install` + +This is needed to fetch the dependencies for the packager. + +```bash +cd react-native-android +npm install +``` + +## Building from the command line + +To build the framework code: + +```bash +cd react-native-android +./gradlew :ReactAndroid:assembleDebug +``` + +To install a snapshot version of the framework code in your local Maven repo: + +```bash +./gradlew :ReactAndroid:installArchives +``` + +## Running the examples + +To run the Sample app: + +```bash +cd react-native-android +./gradlew :Examples:SampleApp:android:app:installDebug +# Start the packager in a separate shell: +# Make sure you ran npm install +./packager/packager.sh +# Open SampleApp in your emulator, Menu button -> Reload JS should work +``` + +You can run any other sample app the same way, e.g.: + +```bash +./gradlew :Examples:Movies:android:app:installDebug +./gradlew :Examples:UIExplorer:android:app:installDebug +``` + +## Building from Android Studio + +You'll need to do one additional step until we release the React Native Gradle plugin to Maven central. This is because Android Studio has its own local Maven repo: + + mkdir -p /Applications/Android\ Studio.app/Contents/gradle/m2repository/com/facebook/react + cp -r ~/.m2/repository/com/facebook/react/gradleplugin /Applications/Android\ Studio.app/Contents/gradle/m2repository/com/facebook/react/ + +Now, open Android Studio, click _Import Non-Android Studio project_ and find your `react-native-android` repo. + +In the configurations dropdown, _app_ should be selected. Click _Run_. + +## Installing the React Native .aar in your local Maven repo + +In some cases, for example when working on the `react-native-cli` it's useful to publish a snapshot version of React Native into your local Maven repo. This way, Gradle can pick it up when building projects that have a Maven dependency on React Native. + +Run: + +```bash +cd react-native-android +./gradlew :ReactAndroid:installArchives +``` + +## Troubleshooting + +Gradle build fails in `ndk-build`. See the section about `local.properties` file above. + +Gradle build fails "Could not find any version that matches com.facebook.react:gradleplugin:...". See the section about the React Native Gradle plugin above. + +Packager throws an error saying a module is not found. Try running `npm install` in the root of the repo. diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle new file mode 100644 index 0000000000..97c77d10d6 --- /dev/null +++ b/ReactAndroid/build.gradle @@ -0,0 +1,246 @@ +// Copyright 2015-present Facebook. All Rights Reserved. + +apply plugin: 'com.android.library' +apply plugin: 'maven' + +apply plugin: 'de.undercouch.download' + +import de.undercouch.gradle.tasks.download.Download +import org.apache.tools.ant.taskdefs.condition.Os +import org.apache.tools.ant.filters.ReplaceTokens + +// We download various C++ open-source dependencies into downloads. +// We then copy both downloaded code and our custom makefiles and headers into third-party-ndk +// After that we build native code from src/main/jni with module path pointing at third-party-ndk + +def downloadsDir = new File("$buildDir/downloads") +def thirdPartyNdkDir = new File("$buildDir/third-party-ndk") + +task createNativeDepsDirectories { + downloadsDir.mkdirs() + thirdPartyNdkDir.mkdirs() +} + +task downloadBoost(dependsOn: createNativeDepsDirectories, type: Download) { + // Use ZIP version as it's faster this way to selectively extract some parts of the archive + src 'https://downloads.sourceforge.net/project/boost/boost/1.57.0/boost_1_57_0.zip' + onlyIfNewer true + overwrite false + dest new File(downloadsDir, 'boost_1_57_0.zip') +} + +task prepareBoost(dependsOn: downloadBoost, type: Copy) { + from zipTree(downloadBoost.dest) + from 'src/main/jni/third-party/boost/Android.mk' + include 'boost_1_57_0/boost/**/*.hpp', 'Android.mk' + into "$thirdPartyNdkDir/boost" +} + +task downloadDoubleConversion(dependsOn: createNativeDepsDirectories, type: Download) { + src 'https://github.com/google/double-conversion/archive/v1.1.1.tar.gz' + onlyIfNewer true + overwrite false + dest new File(downloadsDir, 'double-conversion-1.1.1.tar.gz') +} + +task prepareDoubleConversion(dependsOn: downloadDoubleConversion, type: Copy) { + from tarTree(downloadDoubleConversion.dest) + from 'src/main/jni/third-party/double-conversion/Android.mk' + include 'double-conversion-1.1.1/src/**/*', 'Android.mk' + filesMatching('*/src/**/*', {fname -> fname.path = "double-conversion/${fname.name}"}) + includeEmptyDirs = false + into "$thirdPartyNdkDir/double-conversion" +} + +task downloadFolly(dependsOn: createNativeDepsDirectories, type: Download) { + src 'https://github.com/facebook/folly/archive/v0.50.0.tar.gz' + onlyIfNewer true + overwrite false + dest new File(downloadsDir, 'folly-0.50.0.tar.gz'); +} + +task prepareFolly(dependsOn: downloadFolly, type: Copy) { + from tarTree(downloadFolly.dest) + from 'src/main/jni/third-party/folly/Android.mk' + include 'folly-0.50.0/folly/**/*', 'Android.mk' + eachFile {fname -> fname.path = (fname.path - "folly-0.50.0/")} + includeEmptyDirs = false + into "$thirdPartyNdkDir/folly" +} + +task downloadGlog(dependsOn: createNativeDepsDirectories, type: Download) { + src 'https://github.com/google/glog/archive/v0.3.3.tar.gz' + onlyIfNewer true + overwrite false + dest new File(downloadsDir, 'glog-0.3.3.tar.gz') +} + +// Prepare glog sources to be compiled, this task will perform steps that normally shoudl've been +// executed by automake. This way we can avoid dependencies on make/automake +task prepareGlog(dependsOn: downloadGlog, type: Copy) { + from tarTree(downloadGlog.dest) + from 'src/main/jni/third-party/glog/' + include 'glog-0.3.3/src/**/*', 'Android.mk', 'config.h' + includeEmptyDirs = false + filesMatching('**/*.h.in') { + filter(ReplaceTokens, tokens: [ + ac_cv_have_unistd_h: '1', + ac_cv_have_stdint_h: '1', + ac_cv_have_systypes_h: '1', + ac_cv_have_inttypes_h: '1', + ac_cv_have_libgflags: '0', + ac_google_start_namespace: 'namespace google {', + ac_cv_have_uint16_t: '1', + ac_cv_have_u_int16_t: '1', + ac_cv_have___uint16: '0', + ac_google_end_namespace: '}', + ac_cv_have___builtin_expect: '1', + ac_google_namespace: 'google', + ac_cv___attribute___noinline: '__attribute__ ((noinline))', + ac_cv___attribute___noreturn: '__attribute__ ((noreturn))', + ac_cv___attribute___printf_4_5: '__attribute__((__format__ (__printf__, 4, 5)))' + ]) + it.path = (it.name - '.in') + } + into "$thirdPartyNdkDir/glog" +} + +task downloadJSCHeaders(type: Download) { + def jscAPIBaseURL = 'https://svn.webkit.org/repository/webkit/!svn/bc/174650/trunk/Source/JavaScriptCore/API/' + def jscHeaderFiles = ['JSBase.h', 'JSContextRef.h', 'JSObjectRef.h', 'JSRetainPtr.h', 'JSStringRef.h', 'JSValueRef.h', 'WebKitAvailability.h'] + def output = new File(downloadsDir, 'jsc') + output.mkdirs() + src(jscHeaderFiles.collect { headerName -> "$jscAPIBaseURL$headerName" }) + onlyIfNewer true + overwrite false + dest output +} + +// Create Android.mk library module based on so files from mvn + include headers fetched from webkit.org +task prepareJSC(dependsOn: downloadJSCHeaders) << { + copy { + from zipTree(configurations.compile.fileCollection { dep -> dep.name == 'android-jsc' }.singleFile) + from {downloadJSCHeaders.dest} + from 'src/main/jni/third-party/jsc/Android.mk' + include 'jni/**/*.so', '*.h', 'Android.mk' + filesMatching('*.h', { fname -> fname.path = "JavaScriptCore/${fname.path}"}) + into "$thirdPartyNdkDir/jsc"; + } +} + +def getNdkBuildName() { + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + return "ndk-build.cmd" + } else { + return "ndk-build" + } +} + +def findNdkBuildFullPath() { + // we allow to provide full path to ndk-build tool + if (hasProperty('ndk.command')) { + return property('ndk.command') + } + // or just a path to the containing directory + if (hasProperty('ndk.path')) { + def ndkDir = property('ndk.path') + return new File(ndkDir, getNdkBuildName()).getAbsolutePath() + } + if (System.getenv('ANDROID_NDK') != null) { + def ndkDir = System.getenv('ANDROID_NDK') + return new File(ndkDir, getNdkBuildName()).getAbsolutePath() + } + def ndkDir = android.hasProperty('plugin') ? android.plugin.ndkFolder : + plugins.getPlugin('com.android.library').sdkHandler.getNdkFolder() + if (ndkDir) { + return new File(ndkDir, getNdkBuildName()).getAbsolutePath() + } + return null +} + +def getNdkBuildFullPath() { + def ndkBuildFullPath = findNdkBuildFullPath() + if (ndkBuildFullPath == null || !new File(ndkBuildFullPath).canExecute()) { + throw new GradleScriptException( + "ndk-build binary cannot be found, check if you've set " + + "\$ANDROID_NDK environment variable correctly or if ndk.dir is " + + "setup in local.properties", + null) + } + return ndkBuildFullPath +} + +task buildReactNdkLib(dependsOn: [prepareJSC, prepareBoost, prepareDoubleConversion, prepareFolly, prepareGlog], type: Exec) { + inputs.file('src/main/jni/react') + outputs.dir("$buildDir/react-ndk/all") + commandLine getNdkBuildFullPath(), + 'NDK_PROJECT_PATH=null', + "NDK_APPLICATION_MK=$projectDir/src/main/jni/Application.mk", + 'NDK_OUT=' + temporaryDir, + "NDK_LIBS_OUT=$buildDir/react-ndk/all", + "THIRD_PARTY_NDK_DIR=$buildDir/third-party-ndk", + '-C', file('src/main/jni/react/jni').absolutePath, + '--jobs', Runtime.runtime.availableProcessors() +} + +task cleanReactNdkLib(type: Exec) { + commandLine getNdkBuildFullPath(), + '-C', file('src/main/jni/react/jni').absolutePath, + 'clean' +} + +task packageReactNdkLibs(dependsOn: buildReactNdkLib, type: Copy) { + from "$buildDir/react-ndk/all" + exclude '**/libjsc.so' + into "$buildDir/react-ndk/exported" +} + +android { + compileSdkVersion 22 + buildToolsVersion "22.0.1" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + + ndk { + moduleName "reactnativejni" + } + + buildConfigField 'boolean', 'IS_INTERNAL_BUILD', 'false' + } + + sourceSets.main { + jni.srcDirs = [] + jniLibs.srcDir "$buildDir/react-ndk/exported" + res.srcDirs = ['src/main/res/devsupport', 'src/main/res/shell'] + } + + tasks.withType(JavaCompile) { + compileTask -> compileTask.dependsOn packageReactNdkLibs + } + + clean.dependsOn cleanReactNdkLib + + lintOptions { + abortOnError false + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:22.2.0' + compile 'com.facebook.fresco:fresco:0.6.1' + compile 'com.facebook.fresco:imagepipeline-okhttp:0.6.1' + compile 'com.fasterxml.jackson.core:jackson-core:2.2.3' + compile 'com.google.code.findbugs:jsr305:3.0.0' + compile 'com.squareup.okhttp:okhttp:2.4.0' + compile 'com.squareup.okhttp:okhttp-ws:2.4.0' + compile 'com.squareup.okio:okio:1.5.0' + compile 'org.webkit:android-jsc:r174650' +} + +apply from: 'release.gradle' + diff --git a/ReactAndroid/gradle.properties b/ReactAndroid/gradle.properties new file mode 100644 index 0000000000..504653ca27 --- /dev/null +++ b/ReactAndroid/gradle.properties @@ -0,0 +1,6 @@ +VERSION_NAME=0.11.1-SNAPSHOT +GROUP=com.facebook.react + +POM_NAME=ReactNative +POM_ARTIFACT_ID=react-native +POM_PACKAGING=aar diff --git a/ReactAndroid/libs/infer-annotations-1.5.jar b/ReactAndroid/libs/infer-annotations-1.5.jar new file mode 100644 index 0000000000000000000000000000000000000000..eaab472f3782efae4c4fcdc02c7aad033cf36f0e GIT binary patch literal 11990 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

MTGfXx-r*jlFez~maRtD ztZ4z8X|oabr|FazKO6{DFzpV=<|C zEKi(kgL9#_z!=9J-5U3i?oz(=v{#<}LXvmvd3CW%&EDl}G!1Y(U~C2js=B(5@%IY9 z4=fs6KecO#Oi458Dmu(i^w{xrfAaxQ0$Sx)9go8rbI>+RWdm*Q6=?OxFP1ndirSOZ zz8@;~Q7a%<0x8&A)moG`Cd#_W=kf1-S_JgQ! zf`u7Eco>8Re6j9^?l5U;u3LIdR8{HSfp6aA+NlLYhkFB^I@qo$llc=+uw_v}093#o zUTNSMetm|2NRea$N3v?APOL+t$I>~7)bQY%%s{B*qvy#HTle|G!OQFuI<4V8ZRyQ| zDeGPDn8RPs>`R#Q85UuD5P!$DPT=8xsQFFl0Uu#$HTXKtfSX-P=iN;nF^1|cRBqL^ zlnIJGUyH$&;%AB|Oi}dhb*A1HT4_4I^LhuMPF*xlF+SGU;Cr2ZAKLrh5m&5YPY9J- z)1U6Pb6zRkSlO3W;+F*5)t02y=DWU$9FXK1oFVMMv#jGn3&|42S&R(c`V0~$Aw)Jb zmJ%oJ;mCA$RTU?klxH&6}4|tSJ^p%3MJ3f zR<)m?RH)BbZEk1oN=>)B`z#TDZsFuGxORMFRWnvYdQEvBYyGA@!V(-7rYX|mt~xoY zUR!8|e<*D*wU;^yYE|GHzST&OPy+m&1+G%f1{TT@AN6&$l$@J=YW~#lPw8IrP9ikJ znIagy5lDvfJ76puKB6jILtw&>S0-oKC_cIZe+7T=ENb{U+No3coG1c6ZP4NpDm8Hq zS{R%l#-cyQBL05(ll~-e)seDMNuBx>q-X*nn9`>RTI$5;K2ug=^y z3Bh!}`g}D=jd8elC)P}D1%UD7`Mx#oKAMG-t{FfarCNXDlx1x^AeKYKl{4TW;G@we z&**}x6@bMd_WTef8uBa^OZKc$p4#2QdnwmTp9^EXVGD7i3;~-tN%A-unZ$T^Pj{>& z_PJ_)o)%!2FQ2PtYu(}DiJe2vS$OF_Kr~a$J|wM+|7Oan;kJScnBgtpVV$b|P%f{` zS}>}}$2R(txd&{1Rk#5p%q|Nnt65Q>()Z1kWVrnz1#?&*I%}eop#^5W6r>2)5Nmez z;cPS1NHx)o)mr~l{`iXThP;Q)DTTggrq}er=g1!LrqGuXdlPekTh6`jNFKij+a`HD z#<%yH=+v)u?iqjl0PRb+LXd4+szVJ>yL*kmNWqIZ?*L!vrEOPJb%wlB6NOKx6cXF* z;1gn?@+bL<`{l@4H|J~4D4@&_N4;O10U`EdOxOzcB8#*CmG{nTFc-G(#22RAq}a{@ z5PAf@`!H*}nu7{Yk`B=QvKtfz0%7FIBjB638o+_cKa^;aX8W#@Mm)N`qo$6QvY$Hmu5;#VP ztBARV+K97H4i5a@NKfwQUMGZ{R9b#Jtz1N$`LBro>WKYB8>rAOMvzfzoohdK?EHo5 zz7Z}(Uu$jwz8Zn5S|Y=}0-zVG?ufn|U)b4A+%veqq&j93VI;i?@Ah=>>enZSe!i$q zVUonYZ2S7;9N??!$pY9?;#e~(`Q92s%hBut?J9b5tRHq6h}zsxRhjy0lwDe#NOj^g zodos{%jaI@$?16X59w{^a20(GEHPET=Vy+3>J(3l)wPGn$X<{avsGe%i;$b>F+}!~4*cB*wi3bb;(`$E86LfX^Cn zuAOwf1%J`k4j(yxl<@F+vWf!u6|!`zR3GCMTQCIbcdJ^*SP?_7YQwH!HWK+5LVf#- zw3O8q2+Gg@pGg1eto@F(JuK4MGD`VR89*>d_sC=i_J@eRq@-&mtfW9-TYoX%lsdWW z@7LUfxVkvhni=Acii%J=l%y>|lbk4KnW&=JP?zyKpMW1jUcV4oxLEHMBQ}^fj3sHf z{^iFBn`zLeROUcb2QlQ6RDE=whg_@zH3xBSCaTFLCC2;|q1i8;Ypy3wMq`XWRv&Ci zBFX@i8bnI%ty(WQe^?zSGsFZ(ocRNgaof9bk zwxmRu*^3GgDhSo3PtMX}FQTVXUTlQ-wJ8iVMFf@+_mOAX67JKaY}G?!HC2j`V+3kS$DaY ztHe^0llnyVgpnF>TOEF9j^Y?Z1KD;s0O+dz5{XABt!;lXx2yFA7S5ss z31_Dz=N11**KeV%J#A=bGG3gdAfn-y-N@DiYHI`f6ZYI%-e@CJ_kyN&LVUm7FjJN@^hp}v65dA-UL@{A;?^UNr9XhSxn}w?T zXk+)vs7l&V>~sz>aN3AQ?X4y%?Av3p`zfLR{~^M*T^M2%#NWD;7nw%%??(AUzwK8{ zC?RkXWKdUN1k8243K{mAn`;&J;>tv0OF+`DYD@ztnR~~ZSob(QLJCYn4Ga?Po$$fK zy`Ap%KF`tOcw0nd_C37be0O_u1R`z(R*e?1w*0m0k@GBUA9kX*U(m?L9=~%-<_K2! zBA@%3(aU)Wvp7Acnm@?eMf2(EX@?G7on z-OKi&r1i}9Z&uvvqufaf*J(MFh+#arcGMLMAbrNzHlL*0?$qE>F}9F~g+=GfH>Zfc z>{#-|W-ZJ-a|041%91m~mPzPS6V4TDMuXyu-^)~ZtNaJl5Od7_k&cA^vpN1L{eRj* z7}B~U5PDR5_hH<2B{c7%qHs~U8yCwn#mlSF!J~85&)2v(uQNF_?Ui$WMm$tOOTWuS zVBE7Z(&#%_aPZ}5A7>6jhGJis+>77Ls-vM#?GvUP7QM4T)(VKgUwqrVu)iWHF^*1u z;4qk*Tqd83I#)&oO?QJSADd6&S@68woKW+U&8Hp8*0}C?m-V#l+PNz5EcjbxESTWq zo142RGyJvn<9xJ2S&5a+J9|DR`^Qg* z*G|j1e^Bz0H!U^v&%TlLjpFESFhJ)S+Ps%^kukr1x=l?DFQVkD_t$fIchmOCuU2bW=O{IcZVe*fb5d5Dlaj^&Z(-5x}AweN5 zWkldNXKI==TxT@jrVY?6L&F(HBH`jr18C+O$X1RdddGjZWjlp~#6$xgF(7?+7rZh( zl78~t4)~}P9>VM16P8vTPy3Me@~Fde>oWSedUwQ&KuU=|AJL4; z_ASe*$k?iVmLPr1ow5J8OH)tD?~LZ~3Sr*0MvKD!dZrEbjldGg+&rziV275gU07Yu zFd-39SkYWP=JSN1CbGvwdK?7I48PmIK7;&~EZl;RyaN&uKuAOcP|`k!L z?i5YKPn_m0G~ZHiC2c3g`O^{^0E!yCHRL+!ce70nNT-XR9LL-o-SBouwXAV(Lm{=g zbsWuPd{q~x_S!nXl|MX`py;<>4pKsF@b-^(xb>gx0Mkvp%nZ>#%&dMFyKT^#6;}sgP#hJ`^1p%nu%G;6~2?XSC{xK z@^v6GUtUI%`E#rqx<_G0d0*q)3-e_)YFpRbbMDQ9UtIt2G1O(Tk>Rbsoa+NXIg#0& zu|x0G^uEoI*+x?zao8i*F`P59$p9|;xY z0KQ~cnlVt?aI@qunaXK$H_u^psT4(bugVd$X*B*w^G5t+{Be*(aeONCnI`>vfxueF zvY`nH3mR6b1~=>`aXx6G_(@Mu-{~o`$3+nu7BY~qm_-fI4Yyz?@08n}P=JKR86G52 zuo@QJ+MyT9uN*ILR=(OBD05ar8$NCm?Ie^t3aJnt+tt)xpr<$I^n`qop1kH~OtVGpjytoz$#`Z?ZB>ggtGfFN{U+ z17bDEOv(yCq%$9F-YYS3M}DEErz!4=qnS0saX6hPl$^rc%;A4=us~&I_5S#ayHTcZ zWbZV(l2yrQ6!TK(?pC*`N5_!1;+1I2q$I2^W>j6avez$_xjnJ>N0q;|p`<;{Oda5y zzGaNa3bsE9y#-+?qFUB3<9)SSv_X}CS zu3JGbWc@DZ?Mqj7*p<-9DeTo7sP=P#9xY&3!t^Skm$P6ML2v57Ueq8z*8=%v19>x8 zMX<&HMNQCGg1yiHy|!>JVUX4XtfpT@?L{rn%MY-__TRM>ApfrASHX5s3-n!JN6yf} z{JE$=?kd5)3(S}v78%k5qp(ERK8At!K=6{ZXm#KxF z(lMOtg#03cG=6^j>Yp!s@l11>R@m_sbi#KoAc)WYn^u?%eVJ0&X%2MqbuI~zVZvof zFU>cXsfHbM{BE|lIG3xwj5;rB{#8)@3Rmcu(E>#R$&qG@&=b}UUH^s1xm5VUL7N*cs#kp)j63CxlSnUt|cIijJ)?UzrbS?qc z{_AG{P~N3G!B!X0TJBuXDF4@;F0$aI8^NBRp~cF%#8ds3jbMoJ(tTjhg<|KMKzgFOmCPgCc@MR)nxN?8FJ1^N^@rs9vY literal 0 HcmV?d00001 diff --git a/ReactAndroid/release.gradle b/ReactAndroid/release.gradle new file mode 100644 index 0000000000..9b8cd35a8b --- /dev/null +++ b/ReactAndroid/release.gradle @@ -0,0 +1,128 @@ +// Copyright 2015-present Facebook. All Rights Reserved. + +apply plugin: 'maven' +apply plugin: 'signing' + +// Gradle tasks for publishing to maven +// 1) To install in local maven repo use :installArchives task +// 2) To upload artifact to maven central use: :uploadArchives (you'd need to have the permission to do that) + +def isReleaseBuild() { + return VERSION_NAME.contains('SNAPSHOT') == false +} + +def getRepositoryUrl() { + return hasProperty('repositoryUrl') ? property('repositoryUrl') : 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' +} + +def getRepositoryUsername() { + return hasProperty('repositoryUsername') ? property('repositoryUsername') : '' +} + +def getRepositoryPassword() { + return hasProperty('repositoryPassword') ? property('repositoryPassword') : '' +} + +def configureReactNativePom(def pom) { + pom.project { + name POM_NAME + artifactId POM_ARTIFACT_ID + packaging POM_PACKAGING + description 'A framework for building native apps with React' + url 'https://github.com/facebook/react-native' + + scm { + url 'https://github.com/facebook/react-native.git' + connection 'scm:git:https://github.com/facebook/react-native.git' + developerConnection 'scm:git:git@github.com:facebook/react-native.git' + } + + licenses { + license { + name 'BSD License' + url 'https://github.com/facebook/react-native/blob/master/LICENSE' + distribution 'repo' + } + } + + developers { + developer { + id 'facebook' + name 'Facebook' + } + } + } +} + +afterEvaluate { project -> + + task androidJavadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += files(android.bootClasspath) + classpath += files(project.getConfigurations().getByName('compile').asList()) + include '**/*.java' + exclude '**/ReactBuildConfig.java' + } + + task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) { + classifier = 'javadoc' + from androidJavadoc.destinationDir + } + + task androidSourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.srcDirs + include '**/*.java' + } + + android.libraryVariants.all { variant -> + def name = variant.name.capitalize() + task "jar${name}"(type: Jar, dependsOn: variant.javaCompile) { + from variant.javaCompile.destinationDir + } + } + + artifacts { + archives androidSourcesJar + // TODO Make Javadoc generation work with Java 1.8, currently only works with 1.7 + // archives androidJavadocJar + } + + version = VERSION_NAME + group = GROUP + + signing { + required { isReleaseBuild() && gradle.taskGraph.hasTask('uploadArchives') } + sign configurations.archives + } + + uploadArchives { + configuration = configurations.archives + repositories.mavenDeployer { + beforeDeployment { + MavenDeployment deployment -> signing.signPom(deployment) + } + + repository(url: getRepositoryUrl()) { + authentication( + userName: getRepositoryUsername(), + password: getRepositoryPassword()) + + } + + configureReactNativePom pom + } + } + + task installArchives(type: Upload) { + configuration = configurations.archives + repositories.mavenDeployer { + beforeDeployment { + MavenDeployment deployment -> signing.signPom(deployment) + } + + repository url: "file://${System.properties['user.home']}/.m2/repository" + configureReactNativePom pom + } + } +} diff --git a/ReactAndroid/src/main/AndroidManifest.xml b/ReactAndroid/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..900169cc7c --- /dev/null +++ b/ReactAndroid/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSAlign.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSAlign.java new file mode 100644 index 0000000000..7ca88e1463 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSAlign.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<0a1e3b1f834f027e7a5bc5303f945b0e>> + +package com.facebook.csslayout; + +public enum CSSAlign { + AUTO, + FLEX_START, + CENTER, + FLEX_END, + STRETCH, +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSConstants.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSConstants.java new file mode 100644 index 0000000000..f0441fc416 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSConstants.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<755069c4747cc9fc5624d70e5130e3d1>> + +package com.facebook.csslayout; + +public class CSSConstants { + + public static final float UNDEFINED = Float.NaN; + + public static boolean isUndefined(float value) { + return Float.compare(value, UNDEFINED) == 0; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSDirection.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSDirection.java new file mode 100644 index 0000000000..361a6f264e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSDirection.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<5dc7f205706089599859188712b3bd8a>> + +package com.facebook.csslayout; + +public enum CSSDirection { + INHERIT, + LTR, + RTL, +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSFlexDirection.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSFlexDirection.java new file mode 100644 index 0000000000..4a6a492e2a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSFlexDirection.java @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<6183a87290f3acd1caef7b6301bbf3a7>> + +package com.facebook.csslayout; + +public enum CSSFlexDirection { + COLUMN, + COLUMN_REVERSE, + ROW, + ROW_REVERSE +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSJustify.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSJustify.java new file mode 100644 index 0000000000..bdfd6aa5af --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSJustify.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<619fbefba1cfee797bbc7dd18e22f50c>> + +package com.facebook.csslayout; + +public enum CSSJustify { + FLEX_START, + CENTER, + FLEX_END, + SPACE_BETWEEN, + SPACE_AROUND, +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayout.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayout.java new file mode 100644 index 0000000000..5d594868da --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayout.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<153b6759d2dd8fe8cf6d58a422450b96>> + +package com.facebook.csslayout; + +/** + * Where the output of {@link LayoutEngine#layoutNode(CSSNode, float)} will go in the CSSNode. + */ +public class CSSLayout { + + public float top; + public float left; + public float right; + public float bottom; + public float width = CSSConstants.UNDEFINED; + public float height = CSSConstants.UNDEFINED; + public CSSDirection direction = CSSDirection.LTR; + + /** + * This should always get called before calling {@link LayoutEngine#layoutNode(CSSNode, float)} + */ + public void resetResult() { + left = 0; + top = 0; + right = 0; + bottom = 0; + width = CSSConstants.UNDEFINED; + height = CSSConstants.UNDEFINED; + direction = CSSDirection.LTR; + } + + public void copy(CSSLayout layout) { + left = layout.left; + top = layout.top; + right = layout.right; + bottom = layout.bottom; + width = layout.width; + height = layout.height; + direction = layout.direction; + } + + @Override + public String toString() { + return "layout: {" + + "left: " + left + ", " + + "top: " + top + ", " + + "width: " + width + ", " + + "height: " + height + + "direction: " + direction + + "}"; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayoutContext.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayoutContext.java new file mode 100644 index 0000000000..574c0bdf5f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayoutContext.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<9d48f3d4330e7b6cba0fff7d8f1e8b0c>> + +package com.facebook.csslayout; + +/** + * A context for holding values local to a given instance of layout computation. + * + * This is necessary for making layout thread-safe. A separate instance should + * be used when {@link CSSNode#calculateLayout} is called concurrently on + * different node hierarchies. + */ +public class CSSLayoutContext { + /*package*/ final MeasureOutput measureOutput = new MeasureOutput(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java new file mode 100644 index 0000000000..7952956089 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java @@ -0,0 +1,396 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<> + +package com.facebook.csslayout; + +import javax.annotation.Nullable; + +import java.util.ArrayList; + +import com.facebook.infer.annotation.Assertions; + +/** + * A CSS Node. It has a style object you can manipulate at {@link #style}. After calling + * {@link #calculateLayout()}, {@link #layout} will be filled with the results of the layout. + */ +public class CSSNode { + + private static enum LayoutState { + /** + * Some property of this node or its children has changes and the current values in + * {@link #layout} are not valid. + */ + DIRTY, + + /** + * This node has a new layout relative to the last time {@link #markLayoutSeen()} was called. + */ + HAS_NEW_LAYOUT, + + /** + * {@link #layout} is valid for the node's properties and this layout has been marked as + * having been seen. + */ + UP_TO_DATE, + } + + public static interface MeasureFunction { + + /** + * Should measure the given node and put the result in the given MeasureOutput. + * + * NB: measure is NOT guaranteed to be threadsafe/re-entrant safe! + */ + public void measure(CSSNode node, float width, MeasureOutput measureOutput); + } + + // VisibleForTesting + /*package*/ final CSSStyle style = new CSSStyle(); + /*package*/ final CSSLayout layout = new CSSLayout(); + /*package*/ final CachedCSSLayout lastLayout = new CachedCSSLayout(); + + public int lineIndex = 0; + + private @Nullable ArrayList mChildren; + private @Nullable CSSNode mParent; + private @Nullable MeasureFunction mMeasureFunction = null; + private LayoutState mLayoutState = LayoutState.DIRTY; + + public int getChildCount() { + return mChildren == null ? 0 : mChildren.size(); + } + + public CSSNode getChildAt(int i) { + Assertions.assertNotNull(mChildren); + return mChildren.get(i); + } + + public void addChildAt(CSSNode child, int i) { + if (child.mParent != null) { + throw new IllegalStateException("Child already has a parent, it must be removed first."); + } + if (mChildren == null) { + // 4 is kinda arbitrary, but the default of 10 seems really high for an average View. + mChildren = new ArrayList<>(4); + } + + mChildren.add(i, child); + child.mParent = this; + dirty(); + } + + public CSSNode removeChildAt(int i) { + Assertions.assertNotNull(mChildren); + CSSNode removed = mChildren.remove(i); + removed.mParent = null; + dirty(); + return removed; + } + + public @Nullable CSSNode getParent() { + return mParent; + } + + /** + * @return the index of the given child, or -1 if the child doesn't exist in this node. + */ + public int indexOf(CSSNode child) { + Assertions.assertNotNull(mChildren); + return mChildren.indexOf(child); + } + + public void setMeasureFunction(MeasureFunction measureFunction) { + if (!valuesEqual(mMeasureFunction, measureFunction)) { + mMeasureFunction = measureFunction; + dirty(); + } + } + + public boolean isMeasureDefined() { + return mMeasureFunction != null; + } + + /*package*/ MeasureOutput measure(MeasureOutput measureOutput, float width) { + if (!isMeasureDefined()) { + throw new RuntimeException("Measure function isn't defined!"); + } + measureOutput.height = CSSConstants.UNDEFINED; + measureOutput.width = CSSConstants.UNDEFINED; + Assertions.assertNotNull(mMeasureFunction).measure(this, width, measureOutput); + return measureOutput; + } + + /** + * Performs the actual layout and saves the results in {@link #layout} + */ + public void calculateLayout(CSSLayoutContext layoutContext) { + layout.resetResult(); + LayoutEngine.layoutNode(layoutContext, this, CSSConstants.UNDEFINED, null); + } + + /** + * See {@link LayoutState#DIRTY}. + */ + protected boolean isDirty() { + return mLayoutState == LayoutState.DIRTY; + } + + /** + * See {@link LayoutState#HAS_NEW_LAYOUT}. + */ + public boolean hasNewLayout() { + return mLayoutState == LayoutState.HAS_NEW_LAYOUT; + } + + protected void dirty() { + if (mLayoutState == LayoutState.DIRTY) { + return; + } else if (mLayoutState == LayoutState.HAS_NEW_LAYOUT) { + throw new IllegalStateException("Previous layout was ignored! markLayoutSeen() never called"); + } + + mLayoutState = LayoutState.DIRTY; + + if (mParent != null) { + mParent.dirty(); + } + } + + /*package*/ void markHasNewLayout() { + mLayoutState = LayoutState.HAS_NEW_LAYOUT; + } + + /** + * Tells the node that the current values in {@link #layout} have been seen. Subsequent calls + * to {@link #hasNewLayout()} will return false until this node is laid out with new parameters. + * You must call this each time the layout is generated if the node has a new layout. + */ + public void markLayoutSeen() { + if (!hasNewLayout()) { + throw new IllegalStateException("Expected node to have a new layout to be seen!"); + } + + mLayoutState = LayoutState.UP_TO_DATE; + } + + private void toStringWithIndentation(StringBuilder result, int level) { + // Spaces and tabs are dropped by IntelliJ logcat integration, so rely on __ instead. + StringBuilder indentation = new StringBuilder(); + for (int i = 0; i < level; ++i) { + indentation.append("__"); + } + + result.append(indentation.toString()); + result.append(layout.toString()); + + if (getChildCount() == 0) { + return; + } + + result.append(", children: [\n"); + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).toStringWithIndentation(result, level + 1); + result.append("\n"); + } + result.append(indentation + "]"); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + this.toStringWithIndentation(sb, 0); + return sb.toString(); + } + + protected boolean valuesEqual(float f1, float f2) { + return FloatUtil.floatsEqual(f1, f2); + } + + protected boolean valuesEqual(@Nullable T o1, @Nullable T o2) { + if (o1 == null) { + return o2 == null; + } + return o1.equals(o2); + } + + public void setDirection(CSSDirection direction) { + if (!valuesEqual(style.direction, direction)) { + style.direction = direction; + dirty(); + } + } + + public void setFlexDirection(CSSFlexDirection flexDirection) { + if (!valuesEqual(style.flexDirection, flexDirection)) { + style.flexDirection = flexDirection; + dirty(); + } + } + + public void setJustifyContent(CSSJustify justifyContent) { + if (!valuesEqual(style.justifyContent, justifyContent)) { + style.justifyContent = justifyContent; + dirty(); + } + } + + public void setAlignItems(CSSAlign alignItems) { + if (!valuesEqual(style.alignItems, alignItems)) { + style.alignItems = alignItems; + dirty(); + } + } + + public void setAlignSelf(CSSAlign alignSelf) { + if (!valuesEqual(style.alignSelf, alignSelf)) { + style.alignSelf = alignSelf; + dirty(); + } + } + + public void setPositionType(CSSPositionType positionType) { + if (!valuesEqual(style.positionType, positionType)) { + style.positionType = positionType; + dirty(); + } + } + + public void setWrap(CSSWrap flexWrap) { + if (!valuesEqual(style.flexWrap, flexWrap)) { + style.flexWrap = flexWrap; + dirty(); + } + } + + public void setFlex(float flex) { + if (!valuesEqual(style.flex, flex)) { + style.flex = flex; + dirty(); + } + } + + public void setMargin(int spacingType, float margin) { + if (style.margin.set(spacingType, margin)) { + dirty(); + } + } + + public void setPadding(int spacingType, float padding) { + if (style.padding.set(spacingType, padding)) { + dirty(); + } + } + + public void setBorder(int spacingType, float border) { + if (style.border.set(spacingType, border)) { + dirty(); + } + } + + public void setPositionTop(float positionTop) { + if (!valuesEqual(style.positionTop, positionTop)) { + style.positionTop = positionTop; + dirty(); + } + } + + public void setPositionBottom(float positionBottom) { + if (!valuesEqual(style.positionBottom, positionBottom)) { + style.positionBottom = positionBottom; + dirty(); + } + } + + public void setPositionLeft(float positionLeft) { + if (!valuesEqual(style.positionLeft, positionLeft)) { + style.positionLeft = positionLeft; + dirty(); + } + } + + public void setPositionRight(float positionRight) { + if (!valuesEqual(style.positionRight, positionRight)) { + style.positionRight = positionRight; + dirty(); + } + } + + public void setStyleWidth(float width) { + if (!valuesEqual(style.width, width)) { + style.width = width; + dirty(); + } + } + + public void setStyleHeight(float height) { + if (!valuesEqual(style.height, height)) { + style.height = height; + dirty(); + } + } + + public float getLayoutX() { + return layout.left; + } + + public float getLayoutY() { + return layout.top; + } + + public float getLayoutWidth() { + return layout.width; + } + + public float getLayoutHeight() { + return layout.height; + } + + public CSSDirection getLayoutDirection() { + return layout.direction; + } + + /** + * Get this node's padding, as defined by style + default padding. + */ + public Spacing getStylePadding() { + return style.padding; + } + + /** + * Get this node's width, as defined in the style. + */ + public float getStyleWidth() { + return style.width; + } + + /** + * Get this node's height, as defined in the style. + */ + public float getStyleHeight() { + return style.height; + } + + /** + * Get this node's direction, as defined in the style. + */ + public CSSDirection getStyleDirection() { + return style.direction; + } + + /** + * Set a default padding (left/top/right/bottom) for this node. + */ + public void setDefaultPadding(int spacingType, float padding) { + if (style.padding.setDefault(spacingType, padding)) { + dirty(); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSPositionType.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSPositionType.java new file mode 100644 index 0000000000..96fba2f5d0 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSPositionType.java @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<> + +package com.facebook.csslayout; + +public enum CSSPositionType { + RELATIVE, + ABSOLUTE, +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSStyle.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSStyle.java new file mode 100644 index 0000000000..a25afaf676 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSStyle.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<> + +package com.facebook.csslayout; + +/** + * The CSS style definition for a {@link CSSNode}. + */ +public class CSSStyle { + + public CSSDirection direction = CSSDirection.INHERIT; + public CSSFlexDirection flexDirection = CSSFlexDirection.COLUMN; + public CSSJustify justifyContent = CSSJustify.FLEX_START; + public CSSAlign alignContent = CSSAlign.FLEX_START; + public CSSAlign alignItems = CSSAlign.STRETCH; + public CSSAlign alignSelf = CSSAlign.AUTO; + public CSSPositionType positionType = CSSPositionType.RELATIVE; + public CSSWrap flexWrap = CSSWrap.NOWRAP; + public float flex; + + public Spacing margin = new Spacing(); + public Spacing padding = new Spacing(); + public Spacing border = new Spacing(); + + public float positionTop = CSSConstants.UNDEFINED; + public float positionBottom = CSSConstants.UNDEFINED; + public float positionLeft = CSSConstants.UNDEFINED; + public float positionRight = CSSConstants.UNDEFINED; + + public float width = CSSConstants.UNDEFINED; + public float height = CSSConstants.UNDEFINED; + + public float minWidth = CSSConstants.UNDEFINED; + public float minHeight = CSSConstants.UNDEFINED; + + public float maxWidth = CSSConstants.UNDEFINED; + public float maxHeight = CSSConstants.UNDEFINED; +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSWrap.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSWrap.java new file mode 100644 index 0000000000..476d907c72 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSWrap.java @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<21dab9bd1acf5892ad09370b69b7dd71>> + +package com.facebook.csslayout; + +public enum CSSWrap { + NOWRAP, + WRAP, +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CachedCSSLayout.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CachedCSSLayout.java new file mode 100644 index 0000000000..97ef4886f3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CachedCSSLayout.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<8276834951a75286a0b6d4a980bc43ce>> + +package com.facebook.csslayout; + +/** + * CSSLayout with additional information about the conditions under which it was generated. + * {@link #requestedWidth} and {@link #requestedHeight} are the width and height the parent set on + * this node before calling layout visited us. + */ +public class CachedCSSLayout extends CSSLayout { + + public float requestedWidth = CSSConstants.UNDEFINED; + public float requestedHeight = CSSConstants.UNDEFINED; + public float parentMaxWidth = CSSConstants.UNDEFINED; +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/FloatUtil.java b/ReactAndroid/src/main/java/com/facebook/csslayout/FloatUtil.java new file mode 100644 index 0000000000..420a679ca3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/FloatUtil.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<> + +package com.facebook.csslayout; + +public class FloatUtil { + + private static final float EPSILON = .00001f; + + public static boolean floatsEqual(float f1, float f2) { + if (Float.isNaN(f1) || Float.isNaN(f2)) { + return Float.isNaN(f1) && Float.isNaN(f2); + } + return Math.abs(f2 - f1) < EPSILON; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java b/ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java new file mode 100644 index 0000000000..a3646c8405 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java @@ -0,0 +1,1100 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<4795d7e8efc1dbbaadfe117105c8991a>> + +package com.facebook.csslayout; + +/** + * Calculates layouts based on CSS style. See {@link #layoutNode(CSSNode, float)}. + */ +public class LayoutEngine { + + private static enum PositionIndex { + TOP, + LEFT, + BOTTOM, + RIGHT, + START, + END, + } + + private static enum DimensionIndex { + WIDTH, + HEIGHT, + } + + private static void setLayoutPosition(CSSNode node, PositionIndex position, float value) { + switch (position) { + case TOP: + node.layout.top = value; + break; + case LEFT: + node.layout.left = value; + break; + case RIGHT: + node.layout.right = value; + break; + case BOTTOM: + node.layout.bottom = value; + break; + default: + throw new RuntimeException("Didn't get TOP, LEFT, RIGHT, or BOTTOM!"); + } + } + + private static float getLayoutPosition(CSSNode node, PositionIndex position) { + switch (position) { + case TOP: + return node.layout.top; + case LEFT: + return node.layout.left; + case RIGHT: + return node.layout.right; + case BOTTOM: + return node.layout.bottom; + default: + throw new RuntimeException("Didn't get TOP, LEFT, RIGHT, or BOTTOM!"); + } + } + + private static void setLayoutDimension(CSSNode node, DimensionIndex dimension, float value) { + switch (dimension) { + case WIDTH: + node.layout.width = value; + break; + case HEIGHT: + node.layout.height = value; + break; + default: + throw new RuntimeException("Someone added a third dimension..."); + } + } + + private static float getLayoutDimension(CSSNode node, DimensionIndex dimension) { + switch (dimension) { + case WIDTH: + return node.layout.width; + case HEIGHT: + return node.layout.height; + default: + throw new RuntimeException("Someone added a third dimension..."); + } + } + + private static void setLayoutDirection(CSSNode node, CSSDirection direction) { + node.layout.direction = direction; + } + + private static float getStylePosition(CSSNode node, PositionIndex position) { + switch (position) { + case TOP: + return node.style.positionTop; + case BOTTOM: + return node.style.positionBottom; + case LEFT: + return node.style.positionLeft; + case RIGHT: + return node.style.positionRight; + default: + throw new RuntimeException("Someone added a new cardinal direction..."); + } + } + + private static float getStyleDimension(CSSNode node, DimensionIndex dimension) { + switch (dimension) { + case WIDTH: + return node.style.width; + case HEIGHT: + return node.style.height; + default: + throw new RuntimeException("Someone added a third dimension..."); + } + } + + private static PositionIndex getLeading(CSSFlexDirection axis) { + switch (axis) { + case COLUMN: + return PositionIndex.TOP; + case COLUMN_REVERSE: + return PositionIndex.BOTTOM; + case ROW: + return PositionIndex.LEFT; + case ROW_REVERSE: + return PositionIndex.RIGHT; + default: + throw new RuntimeException("Didn't get TOP, LEFT, RIGHT, or BOTTOM!"); + } + } + + private static PositionIndex getTrailing(CSSFlexDirection axis) { + switch (axis) { + case COLUMN: + return PositionIndex.BOTTOM; + case COLUMN_REVERSE: + return PositionIndex.TOP; + case ROW: + return PositionIndex.RIGHT; + case ROW_REVERSE: + return PositionIndex.LEFT; + default: + throw new RuntimeException("Didn't get COLUMN, COLUMN_REVERSE, ROW, or ROW_REVERSE!"); + } + } + + private static PositionIndex getPos(CSSFlexDirection axis) { + switch (axis) { + case COLUMN: + return PositionIndex.TOP; + case COLUMN_REVERSE: + return PositionIndex.BOTTOM; + case ROW: + return PositionIndex.LEFT; + case ROW_REVERSE: + return PositionIndex.RIGHT; + default: + throw new RuntimeException("Didn't get COLUMN, COLUMN_REVERSE, ROW, or ROW_REVERSE!"); + } + } + + private static DimensionIndex getDim(CSSFlexDirection axis) { + switch (axis) { + case COLUMN: + case COLUMN_REVERSE: + return DimensionIndex.HEIGHT; + case ROW: + case ROW_REVERSE: + return DimensionIndex.WIDTH; + default: + throw new RuntimeException("Didn't get COLUMN, COLUMN_REVERSE, ROW, or ROW_REVERSE!"); + } + } + + private static boolean isDimDefined(CSSNode node, CSSFlexDirection axis) { + float value = getStyleDimension(node, getDim(axis)); + return !CSSConstants.isUndefined(value) && value > 0.0; + } + + private static boolean isPosDefined(CSSNode node, PositionIndex position) { + return !CSSConstants.isUndefined(getStylePosition(node, position)); + } + + private static float getPosition(CSSNode node, PositionIndex position) { + float result = getStylePosition(node, position); + return CSSConstants.isUndefined(result) ? 0 : result; + } + + private static float getMargin(CSSNode node, PositionIndex position) { + switch (position) { + case TOP: + return node.style.margin.get(Spacing.TOP); + case BOTTOM: + return node.style.margin.get(Spacing.BOTTOM); + case LEFT: + return node.style.margin.get(Spacing.LEFT); + case RIGHT: + return node.style.margin.get(Spacing.RIGHT); + case START: + return node.style.margin.get(Spacing.START); + case END: + return node.style.margin.get(Spacing.END); + default: + throw new RuntimeException("Someone added a new cardinal direction..."); + } + } + + private static float getLeadingMargin(CSSNode node, CSSFlexDirection axis) { + if (isRowDirection(axis)) { + float leadingMargin = node.style.margin.getRaw(Spacing.START); + if (!CSSConstants.isUndefined(leadingMargin)) { + return leadingMargin; + } + } + + return getMargin(node, getLeading(axis)); + } + + private static float getTrailingMargin(CSSNode node, CSSFlexDirection axis) { + if (isRowDirection(axis)) { + float trailingMargin = node.style.margin.getRaw(Spacing.END); + if (!CSSConstants.isUndefined(trailingMargin)) { + return trailingMargin; + } + } + + return getMargin(node, getTrailing(axis)); + } + + private static float getPadding(CSSNode node, PositionIndex position) { + switch (position) { + case TOP: + return node.style.padding.get(Spacing.TOP); + case BOTTOM: + return node.style.padding.get(Spacing.BOTTOM); + case LEFT: + return node.style.padding.get(Spacing.LEFT); + case RIGHT: + return node.style.padding.get(Spacing.RIGHT); + case START: + return node.style.padding.get(Spacing.START); + case END: + return node.style.padding.get(Spacing.END); + default: + throw new RuntimeException("Someone added a new cardinal direction..."); + } + } + + private static float getLeadingPadding(CSSNode node, CSSFlexDirection axis) { + if (isRowDirection(axis)) { + float leadingPadding = node.style.padding.getRaw(Spacing.START); + if (!CSSConstants.isUndefined(leadingPadding)) { + return leadingPadding; + } + } + + return getPadding(node, getLeading(axis)); + } + + private static float getTrailingPadding(CSSNode node, CSSFlexDirection axis) { + if (isRowDirection(axis)) { + float trailingPadding = node.style.padding.getRaw(Spacing.END); + if (!CSSConstants.isUndefined(trailingPadding)) { + return trailingPadding; + } + } + + return getPadding(node, getTrailing(axis)); + } + + private static float getBorder(CSSNode node, PositionIndex position) { + switch (position) { + case TOP: + return node.style.border.get(Spacing.TOP); + case BOTTOM: + return node.style.border.get(Spacing.BOTTOM); + case LEFT: + return node.style.border.get(Spacing.LEFT); + case RIGHT: + return node.style.border.get(Spacing.RIGHT); + case START: + return node.style.border.get(Spacing.START); + case END: + return node.style.border.get(Spacing.END); + default: + throw new RuntimeException("Someone added a new cardinal direction..."); + } + } + + private static float getLeadingBorder(CSSNode node, CSSFlexDirection axis) { + if (isRowDirection(axis)) { + float leadingBorder = node.style.border.getRaw(Spacing.START); + if (!CSSConstants.isUndefined(leadingBorder)) { + return leadingBorder; + } + } + + return getBorder(node, getLeading(axis)); + } + + private static float getTrailingBorder(CSSNode node, CSSFlexDirection axis) { + if (isRowDirection(axis)) { + float trailingBorder = node.style.border.getRaw(Spacing.END); + if (!CSSConstants.isUndefined(trailingBorder)) { + return trailingBorder; + } + } + + return getBorder(node, getTrailing(axis)); + } + + private static float getLeadingPaddingAndBorder(CSSNode node, CSSFlexDirection axis) { + return getLeadingPadding(node, axis) + getLeadingBorder(node, axis); + } + + private static float getTrailingPaddingAndBorder(CSSNode node, CSSFlexDirection axis) { + return getTrailingPadding(node, axis) + getTrailingBorder(node, axis); + } + + private static float getBorderAxis(CSSNode node, CSSFlexDirection axis) { + return getLeadingBorder(node, axis) + getTrailingBorder(node, axis); + } + + private static float getMarginAxis(CSSNode node, CSSFlexDirection axis) { + return getLeadingMargin(node, axis) + getTrailingMargin(node, axis); + } + + private static float getPaddingAndBorderAxis(CSSNode node, CSSFlexDirection axis) { + return getLeadingPaddingAndBorder(node, axis) + getTrailingPaddingAndBorder(node, axis); + } + + private static float boundAxis(CSSNode node, CSSFlexDirection axis, float value) { + float min = CSSConstants.UNDEFINED; + float max = CSSConstants.UNDEFINED; + + if (isColumnDirection(axis)) { + min = node.style.minHeight; + max = node.style.maxHeight; + } else if (isRowDirection(axis)) { + min = node.style.minWidth; + max = node.style.maxWidth; + } + + float boundValue = value; + + if (!CSSConstants.isUndefined(max) && max >= 0.0 && boundValue > max) { + boundValue = max; + } + if (!CSSConstants.isUndefined(min) && min >= 0.0 && boundValue < min) { + boundValue = min; + } + + return boundValue; + } + + private static void setDimensionFromStyle(CSSNode node, CSSFlexDirection axis) { + // The parent already computed us a width or height. We just skip it + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(axis)))) { + return; + } + // We only run if there's a width or height defined + if (!isDimDefined(node, axis)) { + return; + } + + // The dimensions can never be smaller than the padding and border + float maxLayoutDimension = Math.max( + boundAxis(node, axis, getStyleDimension(node, getDim(axis))), + getPaddingAndBorderAxis(node, axis)); + setLayoutDimension(node, getDim(axis), maxLayoutDimension); + } + + private static void setTrailingPosition( + CSSNode node, + CSSNode child, + CSSFlexDirection axis) { + setLayoutPosition( + child, + getTrailing(axis), + getLayoutDimension(node, getDim(axis)) - + getLayoutDimension(child, getDim(axis)) - + getLayoutPosition(child, getPos(axis))); + } + + private static float getRelativePosition(CSSNode node, CSSFlexDirection axis) { + float lead = getStylePosition(node, getLeading(axis)); + if (!CSSConstants.isUndefined(lead)) { + return lead; + } + return -getPosition(node, getTrailing(axis)); + } + + private static float getFlex(CSSNode node) { + return node.style.flex; + } + + private static boolean isRowDirection(CSSFlexDirection flexDirection) { + return flexDirection == CSSFlexDirection.ROW || + flexDirection == CSSFlexDirection.ROW_REVERSE; + } + + private static boolean isColumnDirection(CSSFlexDirection flexDirection) { + return flexDirection == CSSFlexDirection.COLUMN || + flexDirection == CSSFlexDirection.COLUMN_REVERSE; + } + + private static CSSFlexDirection resolveAxis( + CSSFlexDirection axis, + CSSDirection direction) { + if (direction == CSSDirection.RTL) { + if (axis == CSSFlexDirection.ROW) { + return CSSFlexDirection.ROW_REVERSE; + } else if (axis == CSSFlexDirection.ROW_REVERSE) { + return CSSFlexDirection.ROW; + } + } + + return axis; + } + + private static CSSDirection resolveDirection(CSSNode node, CSSDirection parentDirection) { + CSSDirection direction = node.style.direction; + if (direction == CSSDirection.INHERIT) { + direction = (parentDirection == null ? CSSDirection.LTR : parentDirection); + } + + return direction; + } + + private static CSSFlexDirection getFlexDirection(CSSNode node) { + return node.style.flexDirection; + } + + private static CSSFlexDirection getCrossFlexDirection( + CSSFlexDirection flexDirection, + CSSDirection direction) { + if (isColumnDirection(flexDirection)) { + return resolveAxis(CSSFlexDirection.ROW, direction); + } else { + return CSSFlexDirection.COLUMN; + } + } + + private static CSSPositionType getPositionType(CSSNode node) { + return node.style.positionType; + } + + private static CSSAlign getAlignItem(CSSNode node, CSSNode child) { + if (child.style.alignSelf != CSSAlign.AUTO) { + return child.style.alignSelf; + } + return node.style.alignItems; + } + + private static CSSAlign getAlignContent(CSSNode node) { + return node.style.alignContent; + } + + private static CSSJustify getJustifyContent(CSSNode node) { + return node.style.justifyContent; + } + + private static boolean isFlexWrap(CSSNode node) { + return node.style.flexWrap == CSSWrap.WRAP; + } + + private static boolean isFlex(CSSNode node) { + return getPositionType(node) == CSSPositionType.RELATIVE && getFlex(node) > 0; + } + + private static boolean isMeasureDefined(CSSNode node) { + return node.isMeasureDefined(); + } + + private static float getDimWithMargin(CSSNode node, CSSFlexDirection axis) { + return getLayoutDimension(node, getDim(axis)) + + getLeadingMargin(node, axis) + + getTrailingMargin(node, axis); + } + + private static boolean needsRelayout(CSSNode node, float parentMaxWidth) { + return node.isDirty() || + !FloatUtil.floatsEqual(node.lastLayout.requestedHeight, node.layout.height) || + !FloatUtil.floatsEqual(node.lastLayout.requestedWidth, node.layout.width) || + !FloatUtil.floatsEqual(node.lastLayout.parentMaxWidth, parentMaxWidth); + } + + /*package*/ static void layoutNode( + CSSLayoutContext layoutContext, + CSSNode node, + float parentMaxWidth, + CSSDirection parentDirection) { + if (needsRelayout(node, parentMaxWidth)) { + node.lastLayout.requestedWidth = node.layout.width; + node.lastLayout.requestedHeight = node.layout.height; + node.lastLayout.parentMaxWidth = parentMaxWidth; + + layoutNodeImpl(layoutContext, node, parentMaxWidth, parentDirection); + node.lastLayout.copy(node.layout); + } else { + node.layout.copy(node.lastLayout); + } + + node.markHasNewLayout(); + } + + private static void layoutNodeImpl( + CSSLayoutContext layoutContext, + CSSNode node, + float parentMaxWidth, + CSSDirection parentDirection) { + for (int i = 0; i < node.getChildCount(); i++) { + node.getChildAt(i).layout.resetResult(); + } + + /** START_GENERATED **/ + + CSSDirection direction = resolveDirection(node, parentDirection); + CSSFlexDirection mainAxis = resolveAxis(getFlexDirection(node), direction); + CSSFlexDirection crossAxis = getCrossFlexDirection(mainAxis, direction); + CSSFlexDirection resolvedRowAxis = resolveAxis(CSSFlexDirection.ROW, direction); + + // Handle width and height style attributes + setDimensionFromStyle(node, mainAxis); + setDimensionFromStyle(node, crossAxis); + + // Set the resolved resolution in the node's layout + setLayoutDirection(node, direction); + + // The position is set by the parent, but we need to complete it with a + // delta composed of the margin and left/top/right/bottom + setLayoutPosition(node, getLeading(mainAxis), getLayoutPosition(node, getLeading(mainAxis)) + getLeadingMargin(node, mainAxis) + + getRelativePosition(node, mainAxis)); + setLayoutPosition(node, getTrailing(mainAxis), getLayoutPosition(node, getTrailing(mainAxis)) + getTrailingMargin(node, mainAxis) + + getRelativePosition(node, mainAxis)); + setLayoutPosition(node, getLeading(crossAxis), getLayoutPosition(node, getLeading(crossAxis)) + getLeadingMargin(node, crossAxis) + + getRelativePosition(node, crossAxis)); + setLayoutPosition(node, getTrailing(crossAxis), getLayoutPosition(node, getTrailing(crossAxis)) + getTrailingMargin(node, crossAxis) + + getRelativePosition(node, crossAxis)); + + if (isMeasureDefined(node)) { + float width = CSSConstants.UNDEFINED; + if (isDimDefined(node, resolvedRowAxis)) { + width = node.style.width; + } else if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(resolvedRowAxis)))) { + width = getLayoutDimension(node, getDim(resolvedRowAxis)); + } else { + width = parentMaxWidth - + getMarginAxis(node, resolvedRowAxis); + } + width -= getPaddingAndBorderAxis(node, resolvedRowAxis); + + // We only need to give a dimension for the text if we haven't got any + // for it computed yet. It can either be from the style attribute or because + // the element is flexible. + boolean isRowUndefined = !isDimDefined(node, resolvedRowAxis) && + CSSConstants.isUndefined(getLayoutDimension(node, getDim(resolvedRowAxis))); + boolean isColumnUndefined = !isDimDefined(node, CSSFlexDirection.COLUMN) && + CSSConstants.isUndefined(getLayoutDimension(node, getDim(CSSFlexDirection.COLUMN))); + + // Let's not measure the text if we already know both dimensions + if (isRowUndefined || isColumnUndefined) { + MeasureOutput measureDim = node.measure( + layoutContext.measureOutput, + width + ); + if (isRowUndefined) { + node.layout.width = measureDim.width + + getPaddingAndBorderAxis(node, resolvedRowAxis); + } + if (isColumnUndefined) { + node.layout.height = measureDim.height + + getPaddingAndBorderAxis(node, CSSFlexDirection.COLUMN); + } + } + if (node.getChildCount() == 0) { + return; + } + } + + int i; + int ii; + CSSNode child; + CSSFlexDirection axis; + + // Pre-fill some dimensions straight from the parent + for (i = 0; i < node.getChildCount(); ++i) { + child = node.getChildAt(i); + // Pre-fill cross axis dimensions when the child is using stretch before + // we call the recursive layout pass + if (getAlignItem(node, child) == CSSAlign.STRETCH && + getPositionType(child) == CSSPositionType.RELATIVE && + !CSSConstants.isUndefined(getLayoutDimension(node, getDim(crossAxis))) && + !isDimDefined(child, crossAxis)) { + setLayoutDimension(child, getDim(crossAxis), Math.max( + boundAxis(child, crossAxis, getLayoutDimension(node, getDim(crossAxis)) - + getPaddingAndBorderAxis(node, crossAxis) - + getMarginAxis(child, crossAxis)), + // You never want to go smaller than padding + getPaddingAndBorderAxis(child, crossAxis) + )); + } else if (getPositionType(child) == CSSPositionType.ABSOLUTE) { + // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both + // left and right or top and bottom). + for (ii = 0; ii < 2; ii++) { + axis = (ii != 0) ? CSSFlexDirection.ROW : CSSFlexDirection.COLUMN; + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(axis))) && + !isDimDefined(child, axis) && + isPosDefined(child, getLeading(axis)) && + isPosDefined(child, getTrailing(axis))) { + setLayoutDimension(child, getDim(axis), Math.max( + boundAxis(child, axis, getLayoutDimension(node, getDim(axis)) - + getPaddingAndBorderAxis(node, axis) - + getMarginAxis(child, axis) - + getPosition(child, getLeading(axis)) - + getPosition(child, getTrailing(axis))), + // You never want to go smaller than padding + getPaddingAndBorderAxis(child, axis) + )); + } + } + } + } + + float definedMainDim = CSSConstants.UNDEFINED; + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(mainAxis)))) { + definedMainDim = getLayoutDimension(node, getDim(mainAxis)) - + getPaddingAndBorderAxis(node, mainAxis); + } + + // We want to execute the next two loops one per line with flex-wrap + int startLine = 0; + int endLine = 0; + // int nextOffset = 0; + int alreadyComputedNextLayout = 0; + // We aggregate the total dimensions of the container in those two variables + float linesCrossDim = 0; + float linesMainDim = 0; + int linesCount = 0; + while (endLine < node.getChildCount()) { + // Layout non flexible children and count children by type + + // mainContentDim is accumulation of the dimensions and margin of all the + // non flexible children. This will be used in order to either set the + // dimensions of the node if none already exist, or to compute the + // remaining space left for the flexible children. + float mainContentDim = 0; + + // There are three kind of children, non flexible, flexible and absolute. + // We need to know how many there are in order to distribute the space. + int flexibleChildrenCount = 0; + float totalFlexible = 0; + int nonFlexibleChildrenCount = 0; + + float maxWidth; + for (i = startLine; i < node.getChildCount(); ++i) { + child = node.getChildAt(i); + float nextContentDim = 0; + + // It only makes sense to consider a child flexible if we have a computed + // dimension for the node. + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(mainAxis))) && isFlex(child)) { + flexibleChildrenCount++; + totalFlexible = totalFlexible + getFlex(child); + + // Even if we don't know its exact size yet, we already know the padding, + // border and margin. We'll use this partial information, which represents + // the smallest possible size for the child, to compute the remaining + // available space. + nextContentDim = getPaddingAndBorderAxis(child, mainAxis) + + getMarginAxis(child, mainAxis); + + } else { + maxWidth = CSSConstants.UNDEFINED; + if (!isRowDirection(mainAxis)) { + maxWidth = parentMaxWidth - + getMarginAxis(node, resolvedRowAxis) - + getPaddingAndBorderAxis(node, resolvedRowAxis); + + if (isDimDefined(node, resolvedRowAxis)) { + maxWidth = getLayoutDimension(node, getDim(resolvedRowAxis)) - + getPaddingAndBorderAxis(node, resolvedRowAxis); + } + } + + // This is the main recursive call. We layout non flexible children. + if (alreadyComputedNextLayout == 0) { + layoutNode(layoutContext, child, maxWidth, direction); + } + + // Absolute positioned elements do not take part of the layout, so we + // don't use them to compute mainContentDim + if (getPositionType(child) == CSSPositionType.RELATIVE) { + nonFlexibleChildrenCount++; + // At this point we know the final size and margin of the element. + nextContentDim = getDimWithMargin(child, mainAxis); + } + } + + // The element we are about to add would make us go to the next line + if (isFlexWrap(node) && + !CSSConstants.isUndefined(getLayoutDimension(node, getDim(mainAxis))) && + mainContentDim + nextContentDim > definedMainDim && + // If there's only one element, then it's bigger than the content + // and needs its own line + i != startLine) { + nonFlexibleChildrenCount--; + alreadyComputedNextLayout = 1; + break; + } + alreadyComputedNextLayout = 0; + mainContentDim = mainContentDim + nextContentDim; + endLine = i + 1; + } + + // Layout flexible children and allocate empty space + + // In order to position the elements in the main axis, we have two + // controls. The space between the beginning and the first element + // and the space between each two elements. + float leadingMainDim = 0; + float betweenMainDim = 0; + + // The remaining available space that needs to be allocated + float remainingMainDim = 0; + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(mainAxis)))) { + remainingMainDim = definedMainDim - mainContentDim; + } else { + remainingMainDim = Math.max(mainContentDim, 0) - mainContentDim; + } + + // If there are flexible children in the mix, they are going to fill the + // remaining space + if (flexibleChildrenCount != 0) { + float flexibleMainDim = remainingMainDim / totalFlexible; + float baseMainDim; + float boundMainDim; + + // Iterate over every child in the axis. If the flex share of remaining + // space doesn't meet min/max bounds, remove this child from flex + // calculations. + for (i = startLine; i < endLine; ++i) { + child = node.getChildAt(i); + if (isFlex(child)) { + baseMainDim = flexibleMainDim * getFlex(child) + + getPaddingAndBorderAxis(child, mainAxis); + boundMainDim = boundAxis(child, mainAxis, baseMainDim); + + if (baseMainDim != boundMainDim) { + remainingMainDim -= boundMainDim; + totalFlexible -= getFlex(child); + } + } + } + flexibleMainDim = remainingMainDim / totalFlexible; + + // The non flexible children can overflow the container, in this case + // we should just assume that there is no space available. + if (flexibleMainDim < 0) { + flexibleMainDim = 0; + } + // We iterate over the full array and only apply the action on flexible + // children. This is faster than actually allocating a new array that + // contains only flexible children. + for (i = startLine; i < endLine; ++i) { + child = node.getChildAt(i); + if (isFlex(child)) { + // At this point we know the final size of the element in the main + // dimension + setLayoutDimension(child, getDim(mainAxis), boundAxis(child, mainAxis, + flexibleMainDim * getFlex(child) + getPaddingAndBorderAxis(child, mainAxis) + )); + + maxWidth = CSSConstants.UNDEFINED; + if (isDimDefined(node, resolvedRowAxis)) { + maxWidth = getLayoutDimension(node, getDim(resolvedRowAxis)) - + getPaddingAndBorderAxis(node, resolvedRowAxis); + } else if (!isRowDirection(mainAxis)) { + maxWidth = parentMaxWidth - + getMarginAxis(node, resolvedRowAxis) - + getPaddingAndBorderAxis(node, resolvedRowAxis); + } + + // And we recursively call the layout algorithm for this child + layoutNode(layoutContext, child, maxWidth, direction); + } + } + + // We use justifyContent to figure out how to allocate the remaining + // space available + } else { + CSSJustify justifyContent = getJustifyContent(node); + if (justifyContent == CSSJustify.CENTER) { + leadingMainDim = remainingMainDim / 2; + } else if (justifyContent == CSSJustify.FLEX_END) { + leadingMainDim = remainingMainDim; + } else if (justifyContent == CSSJustify.SPACE_BETWEEN) { + remainingMainDim = Math.max(remainingMainDim, 0); + if (flexibleChildrenCount + nonFlexibleChildrenCount - 1 != 0) { + betweenMainDim = remainingMainDim / + (flexibleChildrenCount + nonFlexibleChildrenCount - 1); + } else { + betweenMainDim = 0; + } + } else if (justifyContent == CSSJustify.SPACE_AROUND) { + // Space on the edges is half of the space between elements + betweenMainDim = remainingMainDim / + (flexibleChildrenCount + nonFlexibleChildrenCount); + leadingMainDim = betweenMainDim / 2; + } + } + + // Position elements in the main axis and compute dimensions + + // At this point, all the children have their dimensions set. We need to + // find their position. In order to do that, we accumulate data in + // variables that are also useful to compute the total dimensions of the + // container! + float crossDim = 0; + float mainDim = leadingMainDim + + getLeadingPaddingAndBorder(node, mainAxis); + + for (i = startLine; i < endLine; ++i) { + child = node.getChildAt(i); + child.lineIndex = linesCount; + + if (getPositionType(child) == CSSPositionType.ABSOLUTE && + isPosDefined(child, getLeading(mainAxis))) { + // In case the child is position absolute and has left/top being + // defined, we override the position to whatever the user said + // (and margin/border). + setLayoutPosition(child, getPos(mainAxis), getPosition(child, getLeading(mainAxis)) + + getLeadingBorder(node, mainAxis) + + getLeadingMargin(child, mainAxis)); + } else { + // If the child is position absolute (without top/left) or relative, + // we put it at the current accumulated offset. + setLayoutPosition(child, getPos(mainAxis), getLayoutPosition(child, getPos(mainAxis)) + mainDim); + + // Define the trailing position accordingly. + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(mainAxis)))) { + setTrailingPosition(node, child, mainAxis); + } + } + + // Now that we placed the element, we need to update the variables + // We only need to do that for relative elements. Absolute elements + // do not take part in that phase. + if (getPositionType(child) == CSSPositionType.RELATIVE) { + // The main dimension is the sum of all the elements dimension plus + // the spacing. + mainDim = mainDim + betweenMainDim + getDimWithMargin(child, mainAxis); + // The cross dimension is the max of the elements dimension since there + // can only be one element in that cross dimension. + crossDim = Math.max(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis))); + } + } + + float containerCrossAxis = getLayoutDimension(node, getDim(crossAxis)); + if (CSSConstants.isUndefined(getLayoutDimension(node, getDim(crossAxis)))) { + containerCrossAxis = Math.max( + // For the cross dim, we add both sides at the end because the value + // is aggregate via a max function. Intermediate negative values + // can mess this computation otherwise + boundAxis(node, crossAxis, crossDim + getPaddingAndBorderAxis(node, crossAxis)), + getPaddingAndBorderAxis(node, crossAxis) + ); + } + + // Position elements in the cross axis + for (i = startLine; i < endLine; ++i) { + child = node.getChildAt(i); + + if (getPositionType(child) == CSSPositionType.ABSOLUTE && + isPosDefined(child, getLeading(crossAxis))) { + // In case the child is absolutely positionned and has a + // top/left/bottom/right being set, we override all the previously + // computed positions to set it correctly. + setLayoutPosition(child, getPos(crossAxis), getPosition(child, getLeading(crossAxis)) + + getLeadingBorder(node, crossAxis) + + getLeadingMargin(child, crossAxis)); + + } else { + float leadingCrossDim = getLeadingPaddingAndBorder(node, crossAxis); + + // For a relative children, we're either using alignItems (parent) or + // alignSelf (child) in order to determine the position in the cross axis + if (getPositionType(child) == CSSPositionType.RELATIVE) { + CSSAlign alignItem = getAlignItem(node, child); + if (alignItem == CSSAlign.STRETCH) { + // You can only stretch if the dimension has not already been set + // previously. + if (!isDimDefined(child, crossAxis)) { + setLayoutDimension(child, getDim(crossAxis), Math.max( + boundAxis(child, crossAxis, containerCrossAxis - + getPaddingAndBorderAxis(node, crossAxis) - + getMarginAxis(child, crossAxis)), + // You never want to go smaller than padding + getPaddingAndBorderAxis(child, crossAxis) + )); + } + } else if (alignItem != CSSAlign.FLEX_START) { + // The remaining space between the parent dimensions+padding and child + // dimensions+margin. + float remainingCrossDim = containerCrossAxis - + getPaddingAndBorderAxis(node, crossAxis) - + getDimWithMargin(child, crossAxis); + + if (alignItem == CSSAlign.CENTER) { + leadingCrossDim = leadingCrossDim + remainingCrossDim / 2; + } else { // CSSAlign.FLEX_END + leadingCrossDim = leadingCrossDim + remainingCrossDim; + } + } + } + + // And we apply the position + setLayoutPosition(child, getPos(crossAxis), getLayoutPosition(child, getPos(crossAxis)) + linesCrossDim + leadingCrossDim); + + // Define the trailing position accordingly. + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(crossAxis)))) { + setTrailingPosition(node, child, crossAxis); + } + } + } + + linesCrossDim = linesCrossDim + crossDim; + linesMainDim = Math.max(linesMainDim, mainDim); + linesCount = linesCount + 1; + startLine = endLine; + } + + // + // + // Note(prenaux): More than one line, we need to layout the crossAxis + // according to alignContent. + // + // Note that we could probably remove and handle the one line case + // here too, but for the moment this is safer since it won't interfere with + // previously working code. + // + // See specs: + // http://www.w3.org/TR/2012/CR-css3-flexbox-20120918/#layout-algorithm + // section 9.4 + // + if (linesCount > 1 && + !CSSConstants.isUndefined(getLayoutDimension(node, getDim(crossAxis)))) { + float nodeCrossAxisInnerSize = getLayoutDimension(node, getDim(crossAxis)) - + getPaddingAndBorderAxis(node, crossAxis); + float remainingAlignContentDim = nodeCrossAxisInnerSize - linesCrossDim; + + float crossDimLead = 0; + float currentLead = getLeadingPaddingAndBorder(node, crossAxis); + + CSSAlign alignContent = getAlignContent(node); + if (alignContent == CSSAlign.FLEX_END) { + currentLead = currentLead + remainingAlignContentDim; + } else if (alignContent == CSSAlign.CENTER) { + currentLead = currentLead + remainingAlignContentDim / 2; + } else if (alignContent == CSSAlign.STRETCH) { + if (nodeCrossAxisInnerSize > linesCrossDim) { + crossDimLead = (remainingAlignContentDim / linesCount); + } + } + + int endIndex = 0; + for (i = 0; i < linesCount; ++i) { + int startIndex = endIndex; + + // compute the line's height and find the endIndex + float lineHeight = 0; + for (ii = startIndex; ii < node.getChildCount(); ++ii) { + child = node.getChildAt(ii); + if (getPositionType(child) != CSSPositionType.RELATIVE) { + continue; + } + if (child.lineIndex != i) { + break; + } + if (!CSSConstants.isUndefined(getLayoutDimension(child, getDim(crossAxis)))) { + lineHeight = Math.max( + lineHeight, + getLayoutDimension(child, getDim(crossAxis)) + getMarginAxis(child, crossAxis) + ); + } + } + endIndex = ii; + lineHeight = lineHeight + crossDimLead; + + for (ii = startIndex; ii < endIndex; ++ii) { + child = node.getChildAt(ii); + if (getPositionType(child) != CSSPositionType.RELATIVE) { + continue; + } + + CSSAlign alignContentAlignItem = getAlignItem(node, child); + if (alignContentAlignItem == CSSAlign.FLEX_START) { + setLayoutPosition(child, getPos(crossAxis), currentLead + getLeadingMargin(child, crossAxis)); + } else if (alignContentAlignItem == CSSAlign.FLEX_END) { + setLayoutPosition(child, getPos(crossAxis), currentLead + lineHeight - getTrailingMargin(child, crossAxis) - getLayoutDimension(child, getDim(crossAxis))); + } else if (alignContentAlignItem == CSSAlign.CENTER) { + float childHeight = getLayoutDimension(child, getDim(crossAxis)); + setLayoutPosition(child, getPos(crossAxis), currentLead + (lineHeight - childHeight) / 2); + } else if (alignContentAlignItem == CSSAlign.STRETCH) { + setLayoutPosition(child, getPos(crossAxis), currentLead + getLeadingMargin(child, crossAxis)); + // TODO(prenaux): Correctly set the height of items with undefined + // (auto) crossAxis dimension. + } + } + + currentLead = currentLead + lineHeight; + } + } + + boolean needsMainTrailingPos = false; + boolean needsCrossTrailingPos = false; + + // If the user didn't specify a width or height, and it has not been set + // by the container, then we set it via the children. + if (CSSConstants.isUndefined(getLayoutDimension(node, getDim(mainAxis)))) { + setLayoutDimension(node, getDim(mainAxis), Math.max( + // We're missing the last padding at this point to get the final + // dimension + boundAxis(node, mainAxis, linesMainDim + getTrailingPaddingAndBorder(node, mainAxis)), + // We can never assign a width smaller than the padding and borders + getPaddingAndBorderAxis(node, mainAxis) + )); + + needsMainTrailingPos = true; + } + + if (CSSConstants.isUndefined(getLayoutDimension(node, getDim(crossAxis)))) { + setLayoutDimension(node, getDim(crossAxis), Math.max( + // For the cross dim, we add both sides at the end because the value + // is aggregate via a max function. Intermediate negative values + // can mess this computation otherwise + boundAxis(node, crossAxis, linesCrossDim + getPaddingAndBorderAxis(node, crossAxis)), + getPaddingAndBorderAxis(node, crossAxis) + )); + + needsCrossTrailingPos = true; + } + + // Set trailing position if necessary + if (needsMainTrailingPos || needsCrossTrailingPos) { + for (i = 0; i < node.getChildCount(); ++i) { + child = node.getChildAt(i); + + if (needsMainTrailingPos) { + setTrailingPosition(node, child, mainAxis); + } + + if (needsCrossTrailingPos) { + setTrailingPosition(node, child, crossAxis); + } + } + } + + // Calculate dimensions for absolutely positioned elements + for (i = 0; i < node.getChildCount(); ++i) { + child = node.getChildAt(i); + if (getPositionType(child) == CSSPositionType.ABSOLUTE) { + // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both + // left and right or top and bottom). + for (ii = 0; ii < 2; ii++) { + axis = (ii != 0) ? CSSFlexDirection.ROW : CSSFlexDirection.COLUMN; + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(axis))) && + !isDimDefined(child, axis) && + isPosDefined(child, getLeading(axis)) && + isPosDefined(child, getTrailing(axis))) { + setLayoutDimension(child, getDim(axis), Math.max( + boundAxis(child, axis, getLayoutDimension(node, getDim(axis)) - + getBorderAxis(node, axis) - + getMarginAxis(child, axis) - + getPosition(child, getLeading(axis)) - + getPosition(child, getTrailing(axis)) + ), + // You never want to go smaller than padding + getPaddingAndBorderAxis(child, axis) + )); + } + } + for (ii = 0; ii < 2; ii++) { + axis = (ii != 0) ? CSSFlexDirection.ROW : CSSFlexDirection.COLUMN; + if (isPosDefined(child, getTrailing(axis)) && + !isPosDefined(child, getLeading(axis))) { + setLayoutPosition(child, getLeading(axis), getLayoutDimension(node, getDim(axis)) - + getLayoutDimension(child, getDim(axis)) - + getPosition(child, getTrailing(axis))); + } + } + } + } + } + /** END_GENERATED **/ +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/MeasureOutput.java b/ReactAndroid/src/main/java/com/facebook/csslayout/MeasureOutput.java new file mode 100644 index 0000000000..8c0190db55 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/MeasureOutput.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<177254872216bd05e2c0667dc29ef032>> + +package com.facebook.csslayout; + +/** + * POJO to hold the output of the measure function. + */ +public class MeasureOutput { + + public float width; + public float height; +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/README b/ReactAndroid/src/main/java/com/facebook/csslayout/README new file mode 100644 index 0000000000..65821d0075 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/README @@ -0,0 +1,12 @@ +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/7104f7c8eb6c160a8d10badc08f9b981bb65e19c + +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. + diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook b/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook new file mode 100644 index 0000000000..bc13278338 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook @@ -0,0 +1,14 @@ +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/7104f7c8eb6c160a8d10badc08f9b981bb65e19c + +There is generated code in: + - README.facebook (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 running: + + ./syncFromGithub.sh + diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/Spacing.java b/ReactAndroid/src/main/java/com/facebook/csslayout/Spacing.java new file mode 100644 index 0000000000..2f3672ecdf --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/Spacing.java @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<6853e87a84818f1abb6573aee7d6bd55>> + +package com.facebook.csslayout; + +import javax.annotation.Nullable; + +/** + * Class representing CSS spacing (padding, margin, and borders). This is mostly necessary to + * properly implement interactions and updates for properties like margin, marginLeft, and + * marginHorizontal. + */ +public class Spacing { + + /** + * Spacing type that represents the left direction. E.g. {@code marginLeft}. + */ + public static final int LEFT = 0; + /** + * Spacing type that represents the top direction. E.g. {@code marginTop}. + */ + public static final int TOP = 1; + /** + * Spacing type that represents the right direction. E.g. {@code marginRight}. + */ + public static final int RIGHT = 2; + /** + * Spacing type that represents the bottom direction. E.g. {@code marginBottom}. + */ + public static final int BOTTOM = 3; + /** + * Spacing type that represents vertical direction (top and bottom). E.g. {@code marginVertical}. + */ + public static final int VERTICAL = 4; + /** + * Spacing type that represents horizontal direction (left and right). E.g. + * {@code marginHorizontal}. + */ + public static final int HORIZONTAL = 5; + /** + * Spacing type that represents start direction e.g. left in left-to-right, right in right-to-left. + */ + public static final int START = 6; + /** + * Spacing type that represents end direction e.g. right in left-to-right, left in right-to-left. + */ + public static final int END = 7; + /** + * Spacing type that represents all directions (left, top, right, bottom). E.g. {@code margin}. + */ + public static final int ALL = 8; + + private final float[] mSpacing = newFullSpacingArray(); + @Nullable private float[] mDefaultSpacing = null; + + /** + * Set a spacing value. + * + * @param spacingType one of {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, {@link #BOTTOM}, + * {@link #VERTICAL}, {@link #HORIZONTAL}, {@link #ALL} + * @param value the value for this direction + * @return {@code true} if the spacing has changed, or {@code false} if the same value was already + * set + */ + public boolean set(int spacingType, float value) { + if (!FloatUtil.floatsEqual(mSpacing[spacingType], value)) { + mSpacing[spacingType] = value; + return true; + } + return false; + } + + /** + * Set a default spacing value. This is used as a fallback when no spacing has been set for a + * particular direction. + * + * @param spacingType one of {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, {@link #BOTTOM} + * @param value the default value for this direction + * @return + */ + public boolean setDefault(int spacingType, float value) { + if (mDefaultSpacing == null) { + mDefaultSpacing = newSpacingResultArray(); + } + if (!FloatUtil.floatsEqual(mDefaultSpacing[spacingType], value)) { + mDefaultSpacing[spacingType] = value; + return true; + } + return false; + } + + /** + * Get the spacing for a direction. This takes into account any default values that have been set. + * + * @param spacingType one of {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, {@link #BOTTOM} + */ + public float get(int spacingType) { + int secondType = spacingType == TOP || spacingType == BOTTOM ? VERTICAL : HORIZONTAL; + float defaultValue = spacingType == START || spacingType == END ? CSSConstants.UNDEFINED : 0; + return + !CSSConstants.isUndefined(mSpacing[spacingType]) + ? mSpacing[spacingType] + : !CSSConstants.isUndefined(mSpacing[secondType]) + ? mSpacing[secondType] + : !CSSConstants.isUndefined(mSpacing[ALL]) + ? mSpacing[ALL] + : mDefaultSpacing != null + ? mDefaultSpacing[spacingType] + : defaultValue; + } + + /** + * Get the raw value (that was set using {@link #set(int, float)}), without taking into account + * any default values. + * + * @param spacingType one of {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, {@link #BOTTOM}, + * {@link #VERTICAL}, {@link #HORIZONTAL}, {@link #ALL} + */ + public float getRaw(int spacingType) { + return mSpacing[spacingType]; + } + + private static float[] newFullSpacingArray() { + return new float[] { + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + }; + } + + private static float[] newSpacingResultArray() { + return newSpacingResultArray(0); + } + + private static float[] newSpacingResultArray(float defaultValue) { + return new float[] { + defaultValue, + defaultValue, + defaultValue, + defaultValue, + defaultValue, + defaultValue, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + defaultValue, + }; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/syncFromGithub.sh b/ReactAndroid/src/main/java/com/facebook/csslayout/syncFromGithub.sh new file mode 100755 index 0000000000..130f0c3e83 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/syncFromGithub.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +function usage { + echo "usage: syncFromGithub.sh "; +} + +function patchfile { + # Add React Native copyright + printf "/**\n" >> /tmp/csslayoutsync.tmp + printf " * Copyright (c) 2014-present, Facebook, Inc.\n" >> /tmp/csslayoutsync.tmp + printf " * All rights reserved.\n" >> /tmp/csslayoutsync.tmp + printf " * This source code is licensed under the BSD-style license found in the\n" >> /tmp/csslayoutsync.tmp + printf " * LICENSE file in the root directory of this source tree. An additional grant\n" >> /tmp/csslayoutsync.tmp + printf " * of patent rights can be found in the PATENTS file in the same directory.\n" >> /tmp/csslayoutsync.tmp + printf " */\n\n" >> /tmp/csslayoutsync.tmp + printf "// NOTE: this file is auto-copied from https://github.com/facebook/css-layout\n" >> /tmp/csslayoutsync.tmp + # The following is split over four lines so Phabricator doesn't think this file is generated + printf "// @g" >> /tmp/csslayoutsync.tmp + printf "enerated <> /tmp/csslayoutsync.tmp + printf "ignedSource::*O*zOeWoEQle#+L" >> /tmp/csslayoutsync.tmp + printf "!plEphiEmie@IsG>>\n\n" >> /tmp/csslayoutsync.tmp + tail -n +9 $1 >> /tmp/csslayoutsync.tmp + mv /tmp/csslayoutsync.tmp $1 + $ROOT/scripts/signedsource.py sign $1 +} + +if [ -z $1 ]; then + usage + exit 1 +fi + +if [ -z $2 ]; then + usage + exit 1 +fi + +GITHUB=$1 +ROOT=$2 + +set -e # exit if any command fails + +echo "Making github project..." +pushd $GITHUB +COMMIT_ID=$(git rev-parse HEAD) +make +popd + +SRC=$GITHUB/src/java/src/com/facebook/csslayout +TESTS=$GITHUB/src/java/tests/com/facebook/csslayout +FBA_SRC=. +FBA_TESTS=$ROOT/javatests/com/facebook/csslayout + +echo "Copying src files over..." +cp $SRC/*.java $FBA_SRC +echo "Copying test files over..." +cp $TESTS/*.java $FBA_TESTS + +echo "Patching files..." +for sourcefile in $FBA_SRC/*.java; do + patchfile $sourcefile +done +for testfile in $FBA_TESTS/*.java; do + patchfile $testfile +done + +echo "Writing README.facebook" + +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.facebook (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 running: + + ./syncFromGithub.sh +" > $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 0000000000..75892f48ca --- /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 0000000000..3006da53a9 --- /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 0000000000..18f754bf47 --- /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 0000000000..fcb4ca3362 --- /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 0000000000..b669e546b1 --- /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 0000000000..fa6e971f6d --- /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 0000000000..86a3f2c3fb --- /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 0000000000..11f4f32b98 --- /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 0000000000..b1ef5f7ce9 --- /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 0000000000..8f78569131 --- /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 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 0000000000..df524cf917 --- /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 0000000000..f8598e9088 --- /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 0000000000..3cd5cb7351 --- /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 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 0000000000..f11c2408cf --- /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 0000000000..9d6d1bfd1d --- /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 0000000000..0ef9cc9e6a --- /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 0000000000..e50bbfeaf4 --- /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 0000000000..37a6d2a196 --- /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 0000000000..a3678ed91f --- /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 0000000000..3cbdbf3327 --- /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 0000000000..74f0bedf47 --- /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 0000000000..da72250548 --- /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 0000000000..83dbe3d9ae --- /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 0000000000..d1acf3da59 --- /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 0000000000..b90740c517 --- /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 0000000000..214c84f667 --- /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 0000000000..9eb5567557 --- /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 0000000000..3ca9429d01 --- /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 0000000000..25b02f2d0d --- /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 0000000000..15d498df12 --- /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 0000000000..fa574cc3e9 --- /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 0000000000..5e4760f7c7 --- /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 0000000000..ab72c46ba2 --- /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 0000000000..8b5153e5cc --- /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 0000000000..be3cd5d0aa --- /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 0000000000..917c1279c6 --- /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 0000000000..eea4c1d072 --- /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 0000000000..38ad4a2f5c --- /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 0000000000..faf123e889 --- /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 0000000000..ee42c51531 --- /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 0000000000..d371cf5be6 --- /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 0000000000..8940ffc086 --- /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 0000000000..2bc5e26c5c --- /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 0000000000..af23afcfe0 --- /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 0000000000..5e20f09705 --- /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 mModuleInterface; + private final Map mMethodsToIds; + private final Map mMethodsToTracingNames; + + JavaScriptModuleRegistration(int moduleId, Class 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 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 0000000000..fab0f231e3 --- /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 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 0000000000..bc75da2779 --- /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 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 0000000000..551ca5ac24 --- /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 0000000000..faecb9730c --- /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 0000000000..7efeb14766 --- /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 0000000000..1091ce0bad --- /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 0000000000..9b5ded014c --- /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 0000000000..02df619597 --- /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 0000000000..708bdfd8d0 --- /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 0000000000..42a794ecae --- /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 0000000000..4d5630f9fd --- /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 0000000000..aa571141c0 --- /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 0000000000..9b374c145e --- /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 0000000000..25db113ae0 --- /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 0000000000..08447802bf --- /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 0000000000..ccf9f79983 --- /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 0000000000..137ca098ec --- /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 0000000000..7e4376c568 --- /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 0000000000..27363232b8 --- /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 0000000000..4d0470f46b --- /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 0000000000..0cc44f6e9c --- /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 0000000000..47e5ed30cf --- /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 0000000000..5aa5adb43b --- /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 0000000000..3218611d38 --- /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 0000000000..2dd03c3f87 --- /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 0000000000..e2bfa848ef --- /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 0000000000..0c6e2a0441 --- /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 0000000000..f5d1f7a475 --- /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 0000000000..4fefe98a32 --- /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 0000000000..fee3ebbde7 --- /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 0000000000..4557d9fe67 --- /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 0000000000..6861669cb4 --- /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 0000000000..765fe39a58 --- /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 0000000000..e10a0b57d4 --- /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 0000000000..c630a59b51 --- /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 0000000000..d874f5aa2a --- /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 0000000000..10be2a44df --- /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 0000000000..c5eb9ad685 --- /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 0000000000..0090b82ad0 --- /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 0000000000..6350180fdb --- /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 0000000000..89673016ca --- /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 0000000000..23eb266d43 --- /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 0000000000..262f4aa5c4 --- /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 0000000000..74bced6cfa --- /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 0000000000..f73fbdae35 --- /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 0000000000..9023f4150e --- /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 0000000000..6aa627eaf0 --- /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 0000000000..0197980e7f --- /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 0000000000..29c31b416c --- /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 0000000000..f9a71ab183 --- /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 0000000000..a94a65cbc6 --- /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 0000000000..8e8524c731 --- /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 0000000000..00e075c509 --- /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 0000000000..59f80aba46 --- /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 0000000000..ca3558939d --- /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 0000000000..48deca9b15 --- /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 0000000000..46260652e7 --- /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 0000000000..b9a70a79d1 --- /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 0000000000..432b44ef01 --- /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 0000000000..89ae7d9bf4 --- /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 0000000000..dfefbc10c5 --- /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 0000000000..5489853b08 --- /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 0000000000..1a3973a3dd --- /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 0000000000..0e811e1a04 --- /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 0000000000..55c2810bb1 --- /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 0000000000..1329a5b7c6 --- /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 0000000000..87ca48b392 --- /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 0000000000..67f5ca2312 --- /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 0000000000..ef2fcb29d0 --- /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 0000000000..326b6c58e1 --- /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 0000000000..6b914aae26 --- /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 0000000000..2ecad91c3c --- /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 0000000000..28144b9afa --- /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 0000000000..1e63f525df --- /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 0000000000..07022a7e76 --- /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 0000000000..20f163fe36 --- /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 0000000000..b957173cbb --- /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 0000000000..fb70020130 --- /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 0000000000..7ce69c37e2 --- /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 0000000000..36340f0aa5 --- /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 0000000000..75f25617e5 --- /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 0000000000..601528a0fb --- /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 0000000000..facf52e153 --- /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 0000000000..d07eb4a5fa --- /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 0000000000..d401bfa1d0 --- /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 0000000000..65b7b38bcb --- /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 0000000000..6bc10f034a --- /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 0000000000..2e8ba61f22 --- /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 0000000000..299d2f4ae0 --- /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 0000000000..036badada7 --- /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 0000000000..1b7a37bdb4 --- /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 0000000000..dcf4457ebe --- /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 0000000000..810abd520d --- /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 0000000000..d8f00bfcc8 --- /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 0000000000..e61150ba96 --- /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 0000000000..80bdec2198 --- /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 0000000000..18135146ee --- /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 0000000000..7abbdae892 --- /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 0000000000..d515ef10d0 --- /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 0000000000..3247709e61 --- /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 0000000000..01b2201027 --- /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 0000000000..94f6ccab64 --- /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 0000000000..d8c125a72a --- /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 0000000000..f553858bd3 --- /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 0000000000..bcb24667fc --- /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 0000000000..1d86fe7494 --- /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 0000000000..5b23ceda7f --- /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 0000000000..13abb16e22 --- /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 0000000000..e219f18ccd --- /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 0000000000..2d99e79f56 --- /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 0000000000..e47e13e483 --- /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 0000000000..c4d5aa7b7a --- /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 0000000000..05a11ee955 --- /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 0000000000..b9ee413b4a --- /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 0000000000..e12a76488a --- /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 0000000000..9ffdcd7a43 --- /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 0000000000..f338776e91 --- /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 0000000000..fbfd531c89 --- /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 0000000000..f3511bd287 --- /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 0000000000..0902c8dd9a --- /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 0000000000..25b97a8639 --- /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 0000000000..3287ebf785 --- /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 0000000000..61ca838c29 --- /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 0000000000..ef10ca1eaa --- /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 0000000000..60891ae2d1 --- /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 0000000000..6ceef2632d --- /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 0000000000..5ee3cc36ab --- /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 0000000000..eb0b3ee63e --- /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 0000000000..eaf442d232 --- /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 0000000000..2dffc8c26c --- /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 0000000000..525a1d8926 --- /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 0000000000..859c74c99b --- /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 0000000000..1f4a5690b5 --- /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 0000000000..505fea7942 --- /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 0000000000..aa3315c169 --- /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 0000000000..6ef3011b2a --- /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 0000000000..4fa6f36770 --- /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 0000000000..62e52373fb --- /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 0000000000..e5783e3c5d --- /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 0000000000..36f3296611 --- /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 0000000000..56c6ff0ada --- /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 0000000000..9d0d32405f --- /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 0000000000..eb07905a7e --- /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 0000000000..83bc9f50f8 --- /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 0000000000..916c301c96 --- /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 0000000000..b35bbc8d0f --- /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 0000000000..dc6c9cd9c3 --- /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 0000000000..fd7a6f67f2 --- /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 0000000000..b7cabec785 --- /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 0000000000..c8ba6c0614 --- /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 0000000000..b6d9f315ff --- /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 0000000000..296e9d4005 --- /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 0000000000..a9078c420a --- /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 0000000000..ec528cedf2 --- /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 0000000000..aa8c784ccf --- /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 0000000000..cc8098efcd --- /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 0000000000..840fde9dde --- /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 0000000000..c0b72def62 --- /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 0000000000..185394151b --- /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 0000000000..af49619366 --- /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 0000000000..79b39058bd --- /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 0000000000..4b9251dc62 --- /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 0000000000..9f62f5d0da --- /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 0000000000..eac5ed6db9 --- /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 0000000000..78aa65a3d1 --- /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 0000000000..1dec2e7265 --- /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 0000000000..9bdc7c03f6 --- /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 0000000000..dcd997b6c1 --- /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 0000000000..36d3923fec --- /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 0000000000..e78b15cefd --- /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 0000000000..b11af8c572 --- /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 0000000000..863d996079 --- /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 0000000000..f7363441b9 --- /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 0000000000..2b77c31408 --- /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 0000000000..ff99d68f7f --- /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 0000000000..f2cbc8b914 --- /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 0000000000..e593e851e8 --- /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 0000000000..d2f4251ebb --- /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 0000000000..6052b644d9 --- /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 0000000000..0b378dbb85 --- /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 0000000000..fc6c443c4c --- /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 0000000000..fb96b4478f --- /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 0000000000..4e6bfa0ff5 --- /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 0000000000..a8efbd777d --- /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 0000000000..af73bda94c --- /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 0000000000..67a2e9d453 --- /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 0000000000..503d01a6cd --- /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 0000000000..4360c33fcf --- /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 0000000000..2af6bb2542 --- /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 0000000000..a3decbb6ef --- /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 0000000000..eba1eeaf58 --- /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 0000000000..47cdb02320 --- /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 0000000000..a9ec0713dd --- /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 0000000000..a398ffe789 --- /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 0000000000..95e2c27b29 --- /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 0000000000..35fc8599cd --- /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 0000000000..89f2ddbdf1 --- /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 0000000000..4f6fa44ce2 --- /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 0000000000..b6436cbcb0 --- /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 0000000000..36e8693d46 --- /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 0000000000..1520aa1c96 --- /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 0000000000..96a11f9948 --- /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 0000000000..0477ad71dc --- /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 0000000000..7277474d6a --- /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 0000000000..cd5d15e48e --- /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 0000000000..a070ed9a96 --- /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 0000000000..016013e15a --- /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 0000000000..91f28583e1 --- /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 0000000000..a7bcd49a58 --- /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 0000000000..4a832314c5 --- /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 0000000000..dd523375b4 --- /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 0000000000..3a255eb940 --- /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 0000000000..d8f9dda846 --- /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 0000000000..3361c433d2 --- /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 0000000000..1e402a3fcb --- /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 0000000000..36f7737f64 --- /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 0000000000..d21fe697ea --- /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 0000000000..6d943972a6 --- /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 0000000000..d86a2f0dea --- /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 static_number; + * int value = *static_number; + * + * So, syntax-wise it's similar to pointers. T can be primitive types, and if + * it's a class, there has to be a default constructor. + */ +template +class ThreadLocal { +public: + /** + * Constructor that has to be called from a thread-neutral place. + */ + ThreadLocal() : + m_key(0), + m_cleanup(OnThreadExit) { + initialize(); + } + + /** + * As above but with a custom cleanup function + */ + typedef void (*CleanupFunction)(void* obj); + explicit ThreadLocal(CleanupFunction cleanup) : + m_key(0), + m_cleanup(cleanup) { + FBASSERT(cleanup); + initialize(); + } + + /** + * Access object's member or method through this operator overload. + */ + T *operator->() const { + return get(); + } + + T &operator*() const { + return *get(); + } + + T *get() const { + return (T*)pthread_getspecific(m_key); + } + + T* release() { + T* obj = get(); + pthread_setspecific(m_key, NULL); + return obj; + } + + void reset(T* other = NULL) { + T* old = (T*)pthread_getspecific(m_key); + if (old != other) { + FBASSERT(m_cleanup); + m_cleanup(old); + pthread_setspecific(m_key, other); + } + } + +private: + void initialize() { + int ret = pthread_key_create(&m_key, m_cleanup); + if (ret != 0) { + const char *msg = "(unknown error)"; + switch (ret) { + case EAGAIN: + msg = "PTHREAD_KEYS_MAX (1024) is exceeded"; + break; + case ENOMEM: + msg = "Out-of-memory"; + break; + } + (void) msg; + FBASSERTMSGF(0, "pthread_key_create failed: %d %s", ret, msg); + } + } + + static void OnThreadExit(void *obj) { + if (NULL != obj) { + delete (T*)obj; + } + } + + pthread_key_t m_key; + CleanupFunction m_cleanup; +}; + +} diff --git a/ReactAndroid/src/main/jni/first-party/fb/assert.cpp b/ReactAndroid/src/main/jni/first-party/fb/assert.cpp new file mode 100644 index 0000000000..db9a431599 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/assert.cpp @@ -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. + */ + +#include +#include + +#include +#include + +namespace facebook { + +#define ASSERT_BUF_SIZE 4096 +static char sAssertBuf[ASSERT_BUF_SIZE]; +static AssertHandler gAssertHandler; + +void assertInternal(const char* formatstr ...) { + va_list va_args; + va_start(va_args, formatstr); + vsnprintf(sAssertBuf, sizeof(sAssertBuf), formatstr, va_args); + va_end(va_args); + if (gAssertHandler != NULL) { + gAssertHandler(sAssertBuf); + } + FBLOG(LOG_FATAL, "fbassert", "%s", sAssertBuf); + // crash at this specific address so that we can find our crashes easier + *(int*)0xdeadb00c = 0; + // let the compiler know we won't reach the end of the function + __builtin_unreachable(); +} + +void setAssertHandler(AssertHandler assertHandler) { + gAssertHandler = assertHandler; +} + +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/first-party/fb/include/fb/assert.h b/ReactAndroid/src/main/jni/first-party/fb/include/fb/assert.h new file mode 100644 index 0000000000..648ab2cdc4 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/include/fb/assert.h @@ -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. + */ + +#ifndef FBASSERT_H +#define FBASSERT_H + +namespace facebook { +#define ENABLE_FBASSERT 1 + +#if ENABLE_FBASSERT +#define FBASSERTMSGF(expr, msg, ...) !(expr) ? facebook::assertInternal("Assert (%s:%d): " msg, __FILE__, __LINE__, ##__VA_ARGS__) : (void) 0 +#else +#define FBASSERTMSGF(expr, msg, ...) +#endif // ENABLE_FBASSERT + +#define FBASSERT(expr) FBASSERTMSGF(expr, "%s", #expr) + +#define FBCRASH(msg, ...) facebook::assertInternal("Fatal error (%s:%d): " msg, __FILE__, __LINE__, ##__VA_ARGS__) +#define FBUNREACHABLE() facebook::assertInternal("This code should be unreachable (%s:%d)", __FILE__, __LINE__) + +void assertInternal(const char* formatstr, ...) __attribute__((noreturn)); + +// This allows storing the assert message before the current process terminates due to a crash +typedef void (*AssertHandler)(const char* message); +void setAssertHandler(AssertHandler assertHandler); + +} // namespace facebook +#endif // FBASSERT_H diff --git a/ReactAndroid/src/main/jni/first-party/fb/include/fb/log.h b/ReactAndroid/src/main/jni/first-party/fb/include/fb/log.h new file mode 100644 index 0000000000..4e08b558df --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/include/fb/log.h @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2005 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. + */ + +/* + * FB Wrapper for logging functions. + * + * The android logging API uses the macro "LOG()" for its logic, which means + * that it conflicts with random other places that use LOG for their own + * purposes and doesn't work right half the places you include it + * + * FBLOG uses exactly the same semantics (FBLOGD for debug etc) but because of + * the FB prefix it's strictly better. FBLOGV also gets stripped out based on + * whether NDEBUG is set, but can be overridden by FBLOG_NDEBUG + * + * Most of the rest is a copy of with minor changes. + */ + +// +// C/C++ logging functions. See the logging documentation for API details. +// +// We'd like these to be available from C code (in case we import some from +// somewhere), so this has a C interface. +// +// The output will be correct when the log file is shared between multiple +// threads and/or multiple processes so long as the operating system +// supports O_APPEND. These calls have mutex-protected data structures +// and so are NOT reentrant. Do not use LOG in a signal handler. +// +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef ANDROID +#include +#else + // These declarations are needed for our internal use even on non-Android builds. + // (they are borrowed from ) + + /* + * Android log priority values, in ascending priority order. + */ + typedef enum android_LogPriority { + ANDROID_LOG_UNKNOWN = 0, + ANDROID_LOG_DEFAULT, /* only for SetMinPriority() */ + ANDROID_LOG_VERBOSE, + ANDROID_LOG_DEBUG, + ANDROID_LOG_INFO, + ANDROID_LOG_WARN, + ANDROID_LOG_ERROR, + ANDROID_LOG_FATAL, + ANDROID_LOG_SILENT, /* only for SetMinPriority(); must be last */ + } android_LogPriority; + + /* + * Send a simple string to the log. + */ + int __android_log_write(int prio, const char *tag, const char *text); + + /* + * Send a formatted string to the log, used like printf(fmt,...) + */ + int __android_log_print(int prio, const char *tag, const char *fmt, ...) +#if defined(__GNUC__) + __attribute__ ((format(printf, 3, 4))) +#endif + ; + +#endif + +// --------------------------------------------------------------------- + +/* + * Normally we strip FBLOGV (VERBOSE messages) from release builds. + * You can modify this (for example with "#define FBLOG_NDEBUG 0" + * at the top of your source file) to change that behavior. + */ +#ifndef FBLOG_NDEBUG +#ifdef NDEBUG +#define FBLOG_NDEBUG 1 +#else +#define FBLOG_NDEBUG 0 +#endif +#endif + +/* + * This is the local tag used for the following simplified + * logging macros. You can change this preprocessor definition + * before using the other macros to change the tag. + */ +#ifndef LOG_TAG +#define LOG_TAG NULL +#endif + +// --------------------------------------------------------------------- + +/* + * Simplified macro to send a verbose log message using the current LOG_TAG. + */ +#ifndef FBLOGV +#if FBLOG_NDEBUG +#define FBLOGV(...) ((void)0) +#else +#define FBLOGV(...) ((void)FBLOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) +#endif +#endif + +#define CONDITION(cond) (__builtin_expect((cond)!=0, 0)) + +#ifndef FBLOGV_IF +#if FBLOG_NDEBUG +#define FBLOGV_IF(cond, ...) ((void)0) +#else +#define FBLOGV_IF(cond, ...) \ + ( (CONDITION(cond)) \ + ? ((void)FBLOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) \ + : (void)0 ) +#endif +#endif + +/* + * Simplified macro to send a debug log message using the current LOG_TAG. + */ +#ifndef FBLOGD +#define FBLOGD(...) ((void)FBLOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef FBLOGD_IF +#define FBLOGD_IF(cond, ...) \ + ( (CONDITION(cond)) \ + ? ((void)FBLOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) \ + : (void)0 ) +#endif + +/* + * Simplified macro to send an info log message using the current LOG_TAG. + */ +#ifndef FBLOGI +#define FBLOGI(...) ((void)FBLOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef FBLOGI_IF +#define FBLOGI_IF(cond, ...) \ + ( (CONDITION(cond)) \ + ? ((void)FBLOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) \ + : (void)0 ) +#endif + +/* + * Simplified macro to send a warning log message using the current LOG_TAG. + */ +#ifndef FBLOGW +#define FBLOGW(...) ((void)FBLOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef FBLOGW_IF +#define FBLOGW_IF(cond, ...) \ + ( (CONDITION(cond)) \ + ? ((void)FBLOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) \ + : (void)0 ) +#endif + +/* + * Simplified macro to send an error log message using the current LOG_TAG. + */ +#ifndef FBLOGE +#define FBLOGE(...) ((void)FBLOG(LOG_ERROR, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef FBLOGE_IF +#define FBLOGE_IF(cond, ...) \ + ( (CONDITION(cond)) \ + ? ((void)FBLOG(LOG_ERROR, LOG_TAG, __VA_ARGS__)) \ + : (void)0 ) +#endif + +// --------------------------------------------------------------------- + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * verbose priority. + */ +#ifndef IF_FBLOGV +#if FBLOG_NDEBUG +#define IF_FBLOGV() if (false) +#else +#define IF_FBLOGV() IF_FBLOG(LOG_VERBOSE, LOG_TAG) +#endif +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * debug priority. + */ +#ifndef IF_FBLOGD +#define IF_FBLOGD() IF_FBLOG(LOG_DEBUG, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * info priority. + */ +#ifndef IF_FBLOGI +#define IF_FBLOGI() IF_FBLOG(LOG_INFO, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * warn priority. + */ +#ifndef IF_FBLOGW +#define IF_FBLOGW() IF_FBLOG(LOG_WARN, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * error priority. + */ +#ifndef IF_FBLOGE +#define IF_FBLOGE() IF_FBLOG(LOG_ERROR, LOG_TAG) +#endif + + +// --------------------------------------------------------------------- + +/* + * Log a fatal error. If the given condition fails, this stops program + * execution like a normal assertion, but also generating the given message. + * It is NOT stripped from release builds. Note that the condition test + * is -inverted- from the normal assert() semantics. + */ +#define FBLOG_ALWAYS_FATAL_IF(cond, ...) \ + ( (CONDITION(cond)) \ + ? ((void)fb_printAssert(#cond, LOG_TAG, __VA_ARGS__)) \ + : (void)0 ) + +#define FBLOG_ALWAYS_FATAL(...) \ + ( ((void)fb_printAssert(NULL, LOG_TAG, __VA_ARGS__)) ) + +/* + * Versions of LOG_ALWAYS_FATAL_IF and LOG_ALWAYS_FATAL that + * are stripped out of release builds. + */ +#if FBLOG_NDEBUG + +#define FBLOG_FATAL_IF(cond, ...) ((void)0) +#define FBLOG_FATAL(...) ((void)0) + +#else + +#define FBLOG_FATAL_IF(cond, ...) FBLOG_ALWAYS_FATAL_IF(cond, __VA_ARGS__) +#define FBLOG_FATAL(...) FBLOG_ALWAYS_FATAL(__VA_ARGS__) + +#endif + +/* + * Assertion that generates a log message when the assertion fails. + * Stripped out of release builds. Uses the current LOG_TAG. + */ +#define FBLOG_ASSERT(cond, ...) FBLOG_FATAL_IF(!(cond), __VA_ARGS__) +//#define LOG_ASSERT(cond) LOG_FATAL_IF(!(cond), "Assertion failed: " #cond) + +// --------------------------------------------------------------------- + +/* + * Basic log message macro. + * + * Example: + * FBLOG(LOG_WARN, NULL, "Failed with error %d", errno); + * + * The second argument may be NULL or "" to indicate the "global" tag. + */ +#ifndef FBLOG +#define FBLOG(priority, tag, ...) \ + FBLOG_PRI(ANDROID_##priority, tag, __VA_ARGS__) +#endif + +#ifndef FBLOG_BY_DELIMS +#define FBLOG_BY_DELIMS(priority, tag, delims, msg, ...) \ + logPrintByDelims(ANDROID_##priority, tag, delims, msg, ##__VA_ARGS__) +#endif + +/* + * Log macro that allows you to specify a number for the priority. + */ +#ifndef FBLOG_PRI +#define FBLOG_PRI(priority, tag, ...) \ + fb_printLog(priority, tag, __VA_ARGS__) +#endif + +/* + * Log macro that allows you to pass in a varargs ("args" is a va_list). + */ +#ifndef FBLOG_PRI_VA +#define FBLOG_PRI_VA(priority, tag, fmt, args) \ + fb_vprintLog(priority, NULL, tag, fmt, args) +#endif + +/* + * Conditional given a desired logging priority and tag. + */ +#ifndef IF_FBLOG +#define IF_FBLOG(priority, tag) \ + if (fb_testLog(ANDROID_##priority, tag)) +#endif + +typedef void (*LogHandler)(int priority, const char* tag, const char* message); +void setLogHandler(LogHandler logHandler); + +/* + * =========================================================================== + * + * The stuff in the rest of this file should not be used directly. + */ +int fb_printLog(int prio, const char *tag, const char *fmt, ...) +#if defined(__GNUC__) + __attribute__ ((format(printf, 3, 4))) +#endif +; + +#define fb_vprintLog(prio, cond, tag, fmt...) \ + __android_log_vprint(prio, tag, fmt) + +#define fb_printAssert(cond, tag, fmt...) \ + __android_log_assert(cond, tag, fmt) + +#define fb_writeLog(prio, tag, text) \ + __android_log_write(prio, tag, text) + +#define fb_bWriteLog(tag, payload, len) \ + __android_log_bwrite(tag, payload, len) +#define fb_btWriteLog(tag, type, payload, len) \ + __android_log_btwrite(tag, type, payload, len) + +#define fb_testLog(prio, tag) (1) + +/* + * FB extensions + */ +void logPrintByDelims(int priority, const char* tag, const char* delims, + const char* msg, ...); + + +#ifdef __cplusplus +} +#endif + diff --git a/ReactAndroid/src/main/jni/first-party/fb/log.cpp b/ReactAndroid/src/main/jni/first-party/fb/log.cpp new file mode 100644 index 0000000000..b58b7ac945 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/log.cpp @@ -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. + */ + +#include +#include +#include +#include + +#define LOG_BUFFER_SIZE 4096 +static LogHandler gLogHandler; + +void setLogHandler(LogHandler logHandler) { + gLogHandler = logHandler; +} + +int fb_printLog(int prio, const char *tag, const char *fmt, ...) { + char logBuffer[LOG_BUFFER_SIZE]; + + va_list va_args; + va_start(va_args, fmt); + int result = vsnprintf(logBuffer, sizeof(logBuffer), fmt, va_args); + va_end(va_args); + if (gLogHandler != NULL) { + gLogHandler(prio, tag, logBuffer); + } + __android_log_write(prio, tag, logBuffer); + return result; +} + +void logPrintByDelims(int priority, const char* tag, const char* delims, + const char* msg, ...) +{ + va_list ap; + char buf[32768]; + char* context; + char* tok; + + va_start(ap, msg); + vsnprintf(buf, sizeof(buf), msg, ap); + va_end(ap); + + tok = strtok_r(buf, delims, &context); + + if (!tok) { + return; + } + + do { + __android_log_write(priority, tag, tok); + } while ((tok = strtok_r(NULL, delims, &context))); +} + +#ifndef ANDROID + +// Implementations of the basic android logging functions for non-android platforms. + +static char logTagChar(int prio) { + switch (prio) { + default: + case ANDROID_LOG_UNKNOWN: + case ANDROID_LOG_DEFAULT: + case ANDROID_LOG_SILENT: + return ' '; + case ANDROID_LOG_VERBOSE: + return 'V'; + case ANDROID_LOG_DEBUG: + return 'D'; + case ANDROID_LOG_INFO: + return 'I'; + case ANDROID_LOG_WARN: + return 'W'; + case ANDROID_LOG_ERROR: + return 'E'; + case ANDROID_LOG_FATAL: + return 'F'; + } +} + +int __android_log_write(int prio, const char *tag, const char *text) { + return fprintf(stderr, "[%c/%.16s] %s\n", logTagChar(prio), tag, text); +} + +int __android_log_print(int prio, const char *tag, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + + int res = fprintf(stderr, "[%c/%.16s] ", logTagChar(prio), tag); + res += vfprintf(stderr, "%s\n", ap); + + va_end(ap); + return res; +} + +#endif diff --git a/ReactAndroid/src/main/jni/first-party/fb/noncopyable.h b/ReactAndroid/src/main/jni/first-party/fb/noncopyable.h new file mode 100644 index 0000000000..7212cc4d08 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/noncopyable.h @@ -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. + */ + +#pragma once + +namespace facebook { + +struct noncopyable { + noncopyable(const noncopyable&) = delete; + noncopyable& operator=(const noncopyable&) = delete; +protected: + noncopyable() = default; +}; + +} diff --git a/ReactAndroid/src/main/jni/first-party/fb/nonmovable.h b/ReactAndroid/src/main/jni/first-party/fb/nonmovable.h new file mode 100644 index 0000000000..37f006a498 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/nonmovable.h @@ -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. + */ + +#pragma once + +namespace facebook { + +struct nonmovable { + nonmovable(nonmovable&&) = delete; + nonmovable& operator=(nonmovable&&) = delete; +protected: + nonmovable() = default; +}; + +} diff --git a/ReactAndroid/src/main/jni/first-party/jni/ALog.h b/ReactAndroid/src/main/jni/first-party/jni/ALog.h new file mode 100644 index 0000000000..0ed1c5fd6a --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/ALog.h @@ -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. + */ + +/** @file ALog.h + * + * Very simple android only logging. Define LOG_TAG to enable the macros. + */ + +#pragma once + +#ifdef __ANDROID__ + +#include + +namespace facebook { +namespace alog { + +template +inline void log(int level, const char* tag, const char* msg, ARGS... args) noexcept { + __android_log_print(level, tag, msg, args...); +} + +template +inline void log(int level, const char* tag, const char* msg) noexcept { + __android_log_write(level, tag, msg); +} + +template +inline void logv(const char* tag, const char* msg, ARGS... args) noexcept { + log(ANDROID_LOG_VERBOSE, tag, msg, args...); +} + +template +inline void logd(const char* tag, const char* msg, ARGS... args) noexcept { + log(ANDROID_LOG_DEBUG, tag, msg, args...); +} + +template +inline void logi(const char* tag, const char* msg, ARGS... args) noexcept { + log(ANDROID_LOG_INFO, tag, msg, args...); +} + +template +inline void logw(const char* tag, const char* msg, ARGS... args) noexcept { + log(ANDROID_LOG_WARN, tag, msg, args...); +} + +template +inline void loge(const char* tag, const char* msg, ARGS... args) noexcept { + log(ANDROID_LOG_ERROR, tag, msg, args...); +} + +template +inline void logf(const char* tag, const char* msg, ARGS... args) noexcept { + log(ANDROID_LOG_FATAL, tag, msg, args...); +} + + +#ifdef LOG_TAG +# define ALOGV(...) ::facebook::alog::logv(LOG_TAG, __VA_ARGS__) +# define ALOGD(...) ::facebook::alog::logd(LOG_TAG, __VA_ARGS__) +# define ALOGI(...) ::facebook::alog::logi(LOG_TAG, __VA_ARGS__) +# define ALOGW(...) ::facebook::alog::logw(LOG_TAG, __VA_ARGS__) +# define ALOGE(...) ::facebook::alog::loge(LOG_TAG, __VA_ARGS__) +# define ALOGF(...) ::facebook::alog::logf(LOG_TAG, __VA_ARGS__) +#endif + +}} + +#else +# define ALOGV(...) ((void)0) +# define ALOGD(...) ((void)0) +# define ALOGI(...) ((void)0) +# define ALOGW(...) ((void)0) +# define ALOGE(...) ((void)0) +# define ALOGF(...) ((void)0) +#endif diff --git a/ReactAndroid/src/main/jni/first-party/jni/Android.mk b/ReactAndroid/src/main/jni/first-party/jni/Android.mk new file mode 100644 index 0000000000..e77eaf1a0a --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Android.mk @@ -0,0 +1,35 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + Countable.cpp \ + Environment.cpp \ + fbjni.cpp \ + jni_helpers.cpp \ + LocalString.cpp \ + OnLoad.cpp \ + WeakReference.cpp \ + fbjni/Exceptions.cpp \ + fbjni/Hybrid.cpp \ + fbjni/References.cpp + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/.. +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/.. + +LOCAL_CFLAGS := -DLOG_TAG=\"fbjni\" -fexceptions -frtti +LOCAL_CFLAGS += -Wall -Werror + +CXX11_FLAGS := -std=gnu++11 +LOCAL_CFLAGS += $(CXX11_FLAGS) + +LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) + +LOCAL_LDLIBS := -landroid + +LOCAL_SHARED_LIBRARIES := libfb + +LOCAL_MODULE := libfbjni + +include $(BUILD_SHARED_LIBRARY) + +$(call import-module,fb) diff --git a/ReactAndroid/src/main/jni/first-party/jni/Countable.cpp b/ReactAndroid/src/main/jni/first-party/jni/Countable.cpp new file mode 100644 index 0000000000..6ff7efe906 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Countable.cpp @@ -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. + */ + +#include +#include +#include +#include + +namespace facebook { +namespace jni { + +static jfieldID gCountableNativePtr; + +static RefPtr* rawCountableFromJava(JNIEnv* env, jobject obj) { + FBASSERT(obj); + return reinterpret_cast*>(env->GetLongField(obj, gCountableNativePtr)); +} + +const RefPtr& countableFromJava(JNIEnv* env, jobject obj) { + FBASSERT(obj); + return *rawCountableFromJava(env, obj); +} + +void setCountableForJava(JNIEnv* env, jobject obj, RefPtr&& countable) { + int oldValue = env->GetLongField(obj, gCountableNativePtr); + FBASSERTMSGF(oldValue == 0, "Cannot reinitialize object; expected nullptr, got %x", oldValue); + + FBASSERT(countable); + uintptr_t fieldValue = (uintptr_t) new RefPtr(std::move(countable)); + env->SetLongField(obj, gCountableNativePtr, fieldValue); +} + +/** + * NB: THREAD SAFETY (this comment also exists at Countable.java) + * + * This method 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. + */ +void dispose(JNIEnv* env, jobject obj) { + // Grab the pointer + RefPtr* countable = rawCountableFromJava(env, obj); + if (!countable) { + // That was easy. + return; + } + + // Clear out the old value to avoid double-frees + env->SetLongField(obj, gCountableNativePtr, 0); + + delete countable; +} + +void CountableOnLoad(JNIEnv* env) { + jclass countable = env->FindClass("com/facebook/jni/Countable"); + gCountableNativePtr = env->GetFieldID(countable, "mInstance", "J"); + registerNatives(env, countable, { + { "dispose", "()V", (void*) dispose }, + }); +} + +} } diff --git a/ReactAndroid/src/main/jni/first-party/jni/Countable.h b/ReactAndroid/src/main/jni/first-party/jni/Countable.h new file mode 100644 index 0000000000..69460d8a7d --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Countable.h @@ -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. + */ + +#pragma once +#include +#include +#include + +namespace facebook { +namespace jni { + +const RefPtr& countableFromJava(JNIEnv* env, jobject obj); + +template RefPtr extractRefPtr(JNIEnv* env, jobject obj) { + return static_cast>(countableFromJava(env, obj)); +} + +template RefPtr extractPossiblyNullRefPtr(JNIEnv* env, jobject obj) { + return obj ? extractRefPtr(env, obj) : nullptr; +} + +void setCountableForJava(JNIEnv* env, jobject obj, RefPtr&& countable); + +void CountableOnLoad(JNIEnv* env); + +} } + diff --git a/ReactAndroid/src/main/jni/first-party/jni/Doxyfile b/ReactAndroid/src/main/jni/first-party/jni/Doxyfile new file mode 100644 index 0000000000..8b4df6a7c9 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Doxyfile @@ -0,0 +1,18 @@ +PROJECT_NAME = "Facebook JNI" +PROJECT_BRIEF = "Helper library to provide safe and convenient access to JNI with very low overhead" +JAVADOC_AUTOBRIEF = YES +EXTRACT_ALL = YES +RECURSIVE = YES +EXCLUDE = tests Asserts.h Countable.h GlobalReference.h LocalReference.h LocalString.h Registration.h WeakReference.h jni_helpers.h Environment.h +EXCLUDE_PATTERNS = *-inl.h *.cpp +GENERATE_HTML = YES +GENERATE_LATEX = NO +ENABLE_PREPROCESSING = YES +HIDE_UNDOC_MEMBERS = YES +HIDE_SCOPE_NAMES = YES +HIDE_FRIEND_COMPOUNDS = YES +HIDE_UNDOC_CLASSES = YES +SHOW_INCLUDE_FILES = NO +PREDEFINED = LOG_TAG=fbjni +EXAMPLE_PATH = samples +#ENABLED_SECTIONS = INTERNAL diff --git a/ReactAndroid/src/main/jni/first-party/jni/Environment.cpp b/ReactAndroid/src/main/jni/first-party/jni/Environment.cpp new file mode 100644 index 0000000000..02b88ce73a --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Environment.cpp @@ -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. + */ + +#include +#include +#include +#include +#include + +namespace facebook { +namespace jni { + +static StaticInitialized> g_env; +static JavaVM* g_vm = nullptr; + +/* static */ +JNIEnv* Environment::current() { + JNIEnv* env = g_env->get(); + if ((env == nullptr) && (g_vm != nullptr)) { + if (g_vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) { + FBLOGE("Error retrieving JNI Environment, thread is probably not attached to JVM"); + env = nullptr; + } else { + g_env->reset(env); + } + } + return env; +} + +/* static */ +void Environment::detachCurrentThread() { + auto env = g_env->get(); + if (env) { + FBASSERT(g_vm); + g_vm->DetachCurrentThread(); + g_env->reset(); + } +} + +struct EnvironmentInitializer { + EnvironmentInitializer(JavaVM* vm) { + FBASSERT(!g_vm); + FBASSERT(vm); + g_vm = vm; + g_env.initialize([] (void*) {}); + } +}; + +/* static */ +void Environment::initialize(JavaVM* vm) { + static EnvironmentInitializer init(vm); +} + +/* static */ +JNIEnv* Environment::ensureCurrentThreadIsAttached() { + auto env = g_env->get(); + if (!env) { + FBASSERT(g_vm); + g_vm->AttachCurrentThread(&env, nullptr); + g_env->reset(env); + } + return env; +} + +ThreadScope::ThreadScope() + : attachedWithThisScope_(false) { + JNIEnv* env = nullptr; + if (g_vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_EDETACHED) { + return; + } + env = facebook::jni::Environment::ensureCurrentThreadIsAttached(); + FBASSERT(env); + attachedWithThisScope_ = true; +} + +ThreadScope::~ThreadScope() { + if (attachedWithThisScope_) { + Environment::detachCurrentThread(); + } +} + +} } + diff --git a/ReactAndroid/src/main/jni/first-party/jni/Environment.h b/ReactAndroid/src/main/jni/first-party/jni/Environment.h new file mode 100644 index 0000000000..4dc6966a21 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Environment.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 { +namespace jni { + +// Keeps a thread-local reference to the current thread's JNIEnv. +struct Environment { + // May be null if this thread isn't attached to the JVM + static JNIEnv* current(); + static void initialize(JavaVM* vm); + static JNIEnv* ensureCurrentThreadIsAttached(); + static void detachCurrentThread(); +}; + +/** + * RAII Object that attaches a thread to the JVM. Failing to detach from a thread before it + * exits will cause a crash, as will calling Detach an extra time, and this guard class helps + * keep that straight. In addition, it remembers whether it performed the attach or not, so it + * is safe to nest it with itself or with non-fbjni code that manages the attachment correctly. + * + * Potential concerns: + * - Attaching to the JVM is fast (~100us on MotoG), but ideally you would attach while the + * app is not busy. + * - Having a thread detach at arbitrary points is not safe in Dalvik; you need to be sure that + * there is no Java code on the current stack or you run the risk of a crash like: + * ERROR: detaching thread with interp frames (count=18) + * (More detail at https://groups.google.com/forum/#!topic/android-ndk/2H8z5grNqjo) + * ThreadScope won't do a detach if the thread was already attached before the guard is + * instantiated, but there's probably some usage that could trip this up. + * - Newly attached C++ threads only get the bootstrap class loader -- i.e. java language + * classes, not any of our application's classes. This will be different behavior than threads + * that were initiated on the Java side. A workaround is to pass a global reference for a + * class or instance to the new thread; this bypasses the need for the class loader. + * (See http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/invocation.html#attach_current_thread) + */ +class ThreadScope { + public: + ThreadScope(); + ThreadScope(ThreadScope&) = delete; + ThreadScope(ThreadScope&&) = default; + ThreadScope& operator=(ThreadScope&) = delete; + ThreadScope& operator=(ThreadScope&&) = delete; + ~ThreadScope(); + + private: + bool attachedWithThisScope_; +}; + +} } + diff --git a/ReactAndroid/src/main/jni/first-party/jni/GlobalReference.h b/ReactAndroid/src/main/jni/first-party/jni/GlobalReference.h new file mode 100644 index 0000000000..55f537ab57 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/GlobalReference.h @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 + +namespace facebook { namespace jni { + +template +class GlobalReference { + static_assert(std::is_convertible::value, + "GlobalReference instantiated with type that is not " + "convertible to jobject"); + + public: + explicit GlobalReference(T globalReference) : + reference_(globalReference? Environment::current()->NewGlobalRef(globalReference) : nullptr) { + } + + ~GlobalReference() { + reset(); + } + + GlobalReference() : + reference_(nullptr) { + } + + // enable move constructor and assignment + GlobalReference(GlobalReference&& rhs) : + reference_(std::move(rhs.reference_)) { + rhs.reference_ = nullptr; + } + + GlobalReference& operator=(GlobalReference&& rhs) { + if (this != &rhs) { + reset(); + reference_ = std::move(rhs.reference_); + rhs.reference_ = nullptr; + } + return *this; + } + + GlobalReference(const GlobalReference& rhs) : + reference_{} { + reset(rhs.get()); + } + + GlobalReference& operator=(const GlobalReference& rhs) { + if (this == &rhs) { + return *this; + } + reset(rhs.get()); + return *this; + } + + explicit operator bool() const { + return (reference_ != nullptr); + } + + T get() const { + return reinterpret_cast(reference_); + } + + void reset(T globalReference = nullptr) { + if (reference_) { + Environment::current()->DeleteGlobalRef(reference_); + } + if (globalReference) { + reference_ = Environment::current()->NewGlobalRef(globalReference); + } else { + reference_ = nullptr; + } + } + + private: + jobject reference_; +}; + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/LocalReference.h b/ReactAndroid/src/main/jni/first-party/jni/LocalReference.h new file mode 100644 index 0000000000..b5d9c54a13 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/LocalReference.h @@ -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. + */ + +#pragma once + +#include +#include + +#include + +#include + +namespace facebook { +namespace jni { + +template +struct LocalReferenceDeleter { + static_assert(std::is_convertible::value, + "LocalReferenceDeleter instantiated with type that is not convertible to jobject"); + void operator()(T localReference) { + if (localReference != nullptr) { + Environment::current()->DeleteLocalRef(localReference); + } + } + }; + +template +using LocalReference = + std::unique_ptr::type, LocalReferenceDeleter>; + +} } diff --git a/ReactAndroid/src/main/jni/first-party/jni/LocalString.cpp b/ReactAndroid/src/main/jni/first-party/jni/LocalString.cpp new file mode 100644 index 0000000000..5fc8d0c848 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/LocalString.cpp @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include +#include + +#include + +namespace facebook { +namespace jni { + +namespace { + +inline void encode3ByteUTF8(char32_t code, uint8_t* out) { + FBASSERTMSGF((code & 0xffff0000) == 0, "3 byte utf-8 encodings only valid for up to 16 bits"); + + out[0] = 0xE0 | (code >> 12); + out[1] = 0x80 | ((code >> 6) & 0x3F); + out[2] = 0x80 | (code & 0x3F); +} + +inline char32_t decode3ByteUTF8(const uint8_t* in) { + return (((in[0] & 0x0f) << 12) | + ((in[1] & 0x3f) << 6) | + ( in[2] & 0x3f)); +} + +inline void encode4ByteUTF8(char32_t code, std::string& out, size_t offset) { + FBASSERTMSGF((code & 0xfff80000) == 0, "4 byte utf-8 encodings only valid for up to 21 bits"); + + out[offset] = (char) (0xF0 | (code >> 18)); + out[offset + 1] = (char) (0x80 | ((code >> 12) & 0x3F)); + out[offset + 2] = (char) (0x80 | ((code >> 6) & 0x3F)); + out[offset + 3] = (char) (0x80 | (code & 0x3F)); +} + +template +inline bool isFourByteUTF8Encoding(const T* utf8) { + return ((*utf8 & 0xF8) == 0xF0); +} + +} + +namespace detail { + +size_t modifiedLength(const std::string& str) { + // Scan for supplementary characters + size_t j = 0; + for (size_t i = 0; i < str.size(); ) { + if (str[i] == 0) { + i += 1; + j += 2; + } else if (i + 4 > str.size() || + !isFourByteUTF8Encoding(&(str[i]))) { + // See the code in utf8ToModifiedUTF8 for what's happening here. + i += 1; + j += 1; + } else { + i += 4; + j += 6; + } + } + + return j; +} + +// returns modified utf8 length; *length is set to strlen(str) +size_t modifiedLength(const uint8_t* str, size_t* length) { + // NUL-terminated: Scan for length and supplementary characters + size_t i = 0; + size_t j = 0; + while (str[i] != 0) { + if (str[i + 1] == 0 || + str[i + 2] == 0 || + str[i + 3] == 0 || + !isFourByteUTF8Encoding(&(str[i]))) { + i += 1; + j += 1; + } else { + i += 4; + j += 6; + } + } + + *length = i; + return j; +} + +void utf8ToModifiedUTF8(const uint8_t* utf8, size_t len, uint8_t* modified, size_t modifiedBufLen) +{ + size_t j = 0; + for (size_t i = 0; i < len; ) { + FBASSERTMSGF(j < modifiedBufLen, "output buffer is too short"); + if (utf8[i] == 0) { + FBASSERTMSGF(j + 1 < modifiedBufLen, "output buffer is too short"); + modified[j] = 0xc0; + modified[j + 1] = 0x80; + i += 1; + j += 2; + continue; + } + + if (i + 4 > len || + !isFourByteUTF8Encoding(utf8 + i)) { + // If the input is too short for this to be a four-byte + // encoding, or it isn't one for real, just copy it on through. + modified[j] = utf8[i]; + i++; + j++; + continue; + } + + // Convert 4 bytes of input to 2 * 3 bytes of output + char32_t code = (((utf8[i] & 0x07) << 18) | + ((utf8[i + 1] & 0x3f) << 12) | + ((utf8[i + 2] & 0x3f) << 6) | + ( utf8[i + 3] & 0x3f)); + char32_t first; + char32_t second; + + if (code > 0x10ffff) { + // These could be valid utf-8, but cannot be represented as modified UTF-8, due to the 20-bit + // limit on that representation. Encode two replacement characters, so the expected output + // length lines up. + const char32_t kUnicodeReplacementChar = 0xfffd; + first = kUnicodeReplacementChar; + second = kUnicodeReplacementChar; + } else { + // split into surrogate pair + first = ((code - 0x010000) >> 10) | 0xd800; + second = ((code - 0x010000) & 0x3ff) | 0xdc00; + } + + // encode each as a 3 byte surrogate value + FBASSERTMSGF(j + 5 < modifiedBufLen, "output buffer is too short"); + encode3ByteUTF8(first, modified + j); + encode3ByteUTF8(second, modified + j + 3); + i += 4; + j += 6; + } + + FBASSERTMSGF(j < modifiedBufLen, "output buffer is too short"); + modified[j++] = '\0'; +} + +std::string modifiedUTF8ToUTF8(const uint8_t* modified, size_t len) { + // Converting from modified utf8 to utf8 will always shrink, so this will always be sufficient + std::string utf8(len, 0); + size_t j = 0; + for (size_t i = 0; i < len; ) { + // surrogate pair: 1101 10xx xxxx xxxx 1101 11xx xxxx xxxx + // encoded pair: 1110 1101 1010 xxxx 10xx xxxx 1110 1101 1011 xxxx 10xx xxxx + + if (len >= i + 6 && + modified[i] == 0xed && + (modified[i + 1] & 0xf0) == 0xa0 && + modified[i + 3] == 0xed && + (modified[i + 4] & 0xf0) == 0xb0) { + // Valid surrogate pair + char32_t pair1 = decode3ByteUTF8(modified + i); + char32_t pair2 = decode3ByteUTF8(modified + i + 3); + char32_t ch = 0x10000 + (((pair1 & 0x3ff) << 10) | + ( pair2 & 0x3ff)); + encode4ByteUTF8(ch, utf8, j); + i += 6; + j += 4; + continue; + } else if (len >= i + 2 && + modified[i] == 0xc0 && + modified[i + 1] == 0x80) { + utf8[j] = 0; + i += 2; + j += 1; + continue; + } + + // copy one byte. This might be a one, two, or three-byte encoding. It might be an invalid + // encoding of some sort, but garbage in garbage out is ok. + + utf8[j] = (char) modified[i]; + i++; + j++; + } + + utf8.resize(j); + + return utf8; +} + +} + +LocalString::LocalString(const std::string& str) +{ + size_t modlen = detail::modifiedLength(str); + if (modlen == str.size()) { + // no supplementary characters, build jstring from input buffer + m_string = Environment::current()->NewStringUTF(str.data()); + return; + } + auto modified = std::vector(modlen + 1); // allocate extra byte for \0 + detail::utf8ToModifiedUTF8( + reinterpret_cast(str.data()), str.size(), + reinterpret_cast(modified.data()), modified.size()); + m_string = Environment::current()->NewStringUTF(modified.data()); +} + +LocalString::LocalString(const char* str) +{ + size_t len; + size_t modlen = detail::modifiedLength(reinterpret_cast(str), &len); + if (modlen == len) { + // no supplementary characters, build jstring from input buffer + m_string = Environment::current()->NewStringUTF(str); + return; + } + auto modified = std::vector(modlen + 1); // allocate extra byte for \0 + detail::utf8ToModifiedUTF8( + reinterpret_cast(str), len, + reinterpret_cast(modified.data()), modified.size()); + m_string = Environment::current()->NewStringUTF(modified.data()); +} + +LocalString::~LocalString() { + Environment::current()->DeleteLocalRef(m_string); +} + +std::string fromJString(JNIEnv* env, jstring str) { + const char* modified = env->GetStringUTFChars(str, NULL); + jsize length = env->GetStringUTFLength(str); + std::string s = detail::modifiedUTF8ToUTF8(reinterpret_cast(modified), length); + env->ReleaseStringUTFChars(str, modified); + return s; +} + +} } diff --git a/ReactAndroid/src/main/jni/first-party/jni/LocalString.h b/ReactAndroid/src/main/jni/first-party/jni/LocalString.h new file mode 100644 index 0000000000..a85efa48ad --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/LocalString.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 { +namespace jni { + +namespace detail { + +void utf8ToModifiedUTF8(const uint8_t* bytes, size_t len, uint8_t* modified, size_t modifiedLength); +size_t modifiedLength(const std::string& str); +size_t modifiedLength(const uint8_t* str, size_t* length); +std::string modifiedUTF8ToUTF8(const uint8_t* modified, size_t len); + +} + +// JNI represents strings encoded with modified version of UTF-8. The difference between UTF-8 and +// Modified UTF-8 is that the latter support only 1-byte, 2-byte, and 3-byte formats. Supplementary +// character (4 bytes in unicode) needs to be represented in the form of surrogate pairs. To create +// a Modified UTF-8 surrogate pair that Dalvik would understand we take 4-byte unicode character, +// encode it with UTF-16 which gives us two 2 byte chars (surrogate pair) and then we encode each +// pair as UTF-8. This result in 2 x 3 byte characters. To convert modified UTF-8 to standard +// UTF-8, this mus tbe reversed. +// +// The second difference is that Modified UTF-8 is encoding NUL byte in 2-byte format. +// +// In order to avoid complex error handling, only a minimum of validity checking is done to avoid +// crashing. If the input is invalid, the output may be invalid as well. +// +// Relevant links: +// - http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html +// - https://docs.oracle.com/javase/6/docs/api/java/io/DataInput.html#modified-utf-8 + +class LocalString { +public: + // Assumes UTF8 encoding and make a required convertion to modified UTF-8 when the string + // contains unicode supplementary characters. + explicit LocalString(const std::string& str); + explicit LocalString(const char* str); + jstring string() const { + return m_string; + } + ~LocalString(); +private: + jstring m_string; +}; + +// The string from JNI is converted to standard UTF-8 if the string contains supplementary +// characters. +std::string fromJString(JNIEnv* env, jstring str); + +} } diff --git a/ReactAndroid/src/main/jni/first-party/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/first-party/jni/OnLoad.cpp new file mode 100644 index 0000000000..a4c4040908 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/OnLoad.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include +#include +#include +#include + +using namespace facebook::jni; + +JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { + return facebook::jni::initialize(vm, [] { + CountableOnLoad(Environment::current()); + HybridDataOnLoad(); + }); +} diff --git a/ReactAndroid/src/main/jni/first-party/jni/Registration.h b/ReactAndroid/src/main/jni/first-party/jni/Registration.h new file mode 100644 index 0000000000..243a947889 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Registration.h @@ -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. + */ + +#pragma once +#include +#include +#include + +namespace facebook { +namespace jni { + +static inline void registerNatives(JNIEnv* env, jclass cls, std::initializer_list methods) { + auto result = env->RegisterNatives(cls, methods.begin(), methods.size()); + FBASSERT(result == 0); +} + +static inline void registerNatives(JNIEnv* env, const char* cls, std::initializer_list list) { + registerNatives(env, env->FindClass(cls), list); +} + +} } diff --git a/ReactAndroid/src/main/jni/first-party/jni/WeakReference.cpp b/ReactAndroid/src/main/jni/first-party/jni/WeakReference.cpp new file mode 100644 index 0000000000..d87ea33c07 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/WeakReference.cpp @@ -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. + */ + +#include +#include + +namespace facebook { +namespace jni { + +WeakReference::WeakReference(jobject strongRef) : + m_weakReference(Environment::current()->NewWeakGlobalRef(strongRef)) +{ +} + +WeakReference::~WeakReference() { + auto env = Environment::current(); + FBASSERTMSGF(env, "Attempt to delete jni::WeakReference from non-JNI thread"); + env->DeleteWeakGlobalRef(m_weakReference); +} + +ResolvedWeakReference::ResolvedWeakReference(jobject weakRef) : + m_strongReference(Environment::current()->NewLocalRef(weakRef)) +{ +} + +ResolvedWeakReference::ResolvedWeakReference(const RefPtr& weakRef) : + m_strongReference(Environment::current()->NewLocalRef(weakRef->weakRef())) +{ +} + +ResolvedWeakReference::~ResolvedWeakReference() { + if (m_strongReference) + Environment::current()->DeleteLocalRef(m_strongReference); +} + +} } + diff --git a/ReactAndroid/src/main/jni/first-party/jni/WeakReference.h b/ReactAndroid/src/main/jni/first-party/jni/WeakReference.h new file mode 100644 index 0000000000..2723155fe1 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/WeakReference.h @@ -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. + */ + +#pragma once +#include +#include +#include +#include + +namespace facebook { +namespace jni { + +class WeakReference : public Countable { +public: + typedef RefPtr Ptr; + WeakReference(jobject strongRef); + ~WeakReference(); + jweak weakRef() { + return m_weakReference; + } + +private: + jweak m_weakReference; +}; + +// This class is intended to take a weak reference and turn it into a strong +// local reference. Consequently, it should only be allocated on the stack. +class ResolvedWeakReference : public noncopyable { +public: + ResolvedWeakReference(jobject weakRef); + ResolvedWeakReference(const RefPtr& weakRef); + ~ResolvedWeakReference(); + + operator jobject () { + return m_strongReference; + } + + explicit operator bool () { + return m_strongReference != nullptr; + } + +private: + jobject m_strongReference; +}; + +} } + diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni.cpp b/ReactAndroid/src/main/jni/first-party/jni/fbjni.cpp new file mode 100644 index 0000000000..fb80b45539 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni.cpp @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "fbjni.h" + +#include +#include +#include + +namespace facebook { +namespace jni { + +template +static void log(Args... args) { +// TODO (7623232) Migrate to glog +#ifdef __ANDROID__ + facebook::alog::loge("fbjni", args...); +#endif +} + +jint initialize(JavaVM* vm, void(*init_fn)()) noexcept { + static std::once_flag init_flag; + static auto failed = false; + + std::call_once(init_flag, [vm] { + try { + Environment::initialize(vm); + internal::initExceptionHelpers(); + } catch (std::exception& ex) { + log("Failed to initialize fbjni: %s", ex.what()); + failed = true; + } catch (...) { + log("Failed to initialize fbjni"); + failed = true; + } + }); + + if (failed) { + return JNI_ERR; + } + + try { + init_fn(); + } catch (...) { + translatePendingCppExceptionToJavaException(); + // So Java will handle the translated exception, fall through and + // return a good version number. + } + return JNI_VERSION_1_6; +} + +alias_ref findClassStatic(const char* name) { + const auto env = internal::getEnv(); + auto cls = env->FindClass(name); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!cls); + auto leaking_ref = (jclass)env->NewGlobalRef(cls); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!leaking_ref); + return wrap_alias(leaking_ref); +} + +local_ref findClassLocal(const char* name) { + const auto env = internal::getEnv(); + auto cls = env->FindClass(name); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!cls); + return adopt_local(cls); +} + + +// jstring ///////////////////////////////////////////////////////////////////////////////////////// + +std::string JObjectWrapper::toStdString() const { + const auto env = internal::getEnv(); + auto modified = env->GetStringUTFChars(self(), nullptr); + auto length = env->GetStringUTFLength(self()); + auto string = detail::modifiedUTF8ToUTF8(reinterpret_cast(modified), length); + env->ReleaseStringUTFChars(self(), modified); + return string; +} + +local_ref make_jstring(const char* utf8) { + if (!utf8) { + return {}; + } + const auto env = internal::getEnv(); + size_t len; + size_t modlen = detail::modifiedLength(reinterpret_cast(utf8), &len); + jstring result; + if (modlen == len) { + // The only difference between utf8 and modifiedUTF8 is in encoding 4-byte UTF8 chars + // and '\0' that is encoded on 2 bytes. + // + // Since modifiedUTF8-encoded string can be no shorter than it's UTF8 conterpart we + // know that if those two strings are of the same length we don't need to do any + // conversion -> no 4-byte chars nor '\0'. + result = env->NewStringUTF(utf8); + } else { + auto modified = std::vector(modlen + 1); // allocate extra byte for \0 + detail::utf8ToModifiedUTF8( + reinterpret_cast(utf8), len, + reinterpret_cast(modified.data()), modified.size()); + result = env->NewStringUTF(modified.data()); + } + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return adopt_local(result); +} + + +// PinnedPrimitiveArray /////////////////////////////////////////////////////////////////////////// + +// TODO(T7847300): Allow array to be specified as constant so that JNI_ABORT can be passed +// on release, as opposed to 0, which results in unnecessary copying. +#pragma push_macro("DEFINE_PRIMITIVE_METHODS") +#undef DEFINE_PRIMITIVE_METHODS +#define DEFINE_PRIMITIVE_METHODS(TYPE, NAME) \ +template<> \ +TYPE* PinnedPrimitiveArray::get() { \ + FACEBOOK_JNI_THROW_EXCEPTION_IF(array_.get() == nullptr); \ + const auto env = internal::getEnv(); \ + elements_ = env->Get ## NAME ## ArrayElements( \ + static_cast(array_.get()), &isCopy_); \ + size_ = array_->size(); \ + return elements_; \ +} \ +template<> \ +void PinnedPrimitiveArray::release() { \ + FACEBOOK_JNI_THROW_EXCEPTION_IF(array_.get() == nullptr); \ + const auto env = internal::getEnv(); \ + env->Release ## NAME ## ArrayElements( \ + static_cast(array_.get()), elements_, 0); \ + elements_ = nullptr; \ + size_ = 0; \ +} + +DEFINE_PRIMITIVE_METHODS(jboolean, Boolean) +DEFINE_PRIMITIVE_METHODS(jbyte, Byte) +DEFINE_PRIMITIVE_METHODS(jchar, Char) +DEFINE_PRIMITIVE_METHODS(jshort, Short) +DEFINE_PRIMITIVE_METHODS(jint, Int) +DEFINE_PRIMITIVE_METHODS(jlong, Long) +DEFINE_PRIMITIVE_METHODS(jfloat, Float) +DEFINE_PRIMITIVE_METHODS(jdouble, Double) +#pragma pop_macro("DEFINE_PRIMITIVE_METHODS") + + +#define DEFINE_PRIMITIVE_ARRAY_UTILS(TYPE, NAME) \ +local_ref make_ ## TYPE ## _array(jsize size) { \ + auto array = internal::getEnv()->New ## NAME ## Array(size); \ + FACEBOOK_JNI_THROW_EXCEPTION_IF(!array); \ + return adopt_local(array); \ +} \ + \ +j ## TYPE* \ +JObjectWrapper::getRegion(jsize start, jsize length, j ## TYPE* buf) { \ + internal::getEnv()->Get ## NAME ## ArrayRegion(self(), start, length, buf); \ + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); \ + return buf; \ +} \ + \ +std::unique_ptr \ +JObjectWrapper::getRegion(jsize start, jsize length) { \ + auto buf = std::unique_ptr{new j ## TYPE[length]}; \ + internal::getEnv()->Get ## NAME ## ArrayRegion(self(), start, length, buf.get()); \ + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); \ + return buf; \ +} \ + \ +void JObjectWrapper::setRegion(jsize start, jsize length, j ## TYPE* buf) { \ + internal::getEnv()->Set ## NAME ## ArrayRegion(self(), start, length, buf); \ + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); \ +} \ + \ +PinnedPrimitiveArray JObjectWrapper::pin() { \ + return PinnedPrimitiveArray{self()}; \ +} \ + +DEFINE_PRIMITIVE_ARRAY_UTILS(boolean, Boolean) +DEFINE_PRIMITIVE_ARRAY_UTILS(byte, Byte) +DEFINE_PRIMITIVE_ARRAY_UTILS(char, Char) +DEFINE_PRIMITIVE_ARRAY_UTILS(short, Short) +DEFINE_PRIMITIVE_ARRAY_UTILS(int, Int) +DEFINE_PRIMITIVE_ARRAY_UTILS(long, Long) +DEFINE_PRIMITIVE_ARRAY_UTILS(float, Float) +DEFINE_PRIMITIVE_ARRAY_UTILS(double, Double) + + +// Internal debug ///////////////////////////////////////////////////////////////////////////////// + +namespace internal { +ReferenceStats g_reference_stats; + +void facebook::jni::internal::ReferenceStats::reset() noexcept { + locals_deleted = globals_deleted = weaks_deleted = 0; +} +} + +}} + diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni.h new file mode 100644 index 0000000000..7ea816b60e --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 "Environment.h" +#include "ALog.h" +#include "fbjni/Common.h" +#include "fbjni/Exceptions.h" +#include "fbjni/ReferenceAllocators.h" +#include "fbjni/References.h" +#include "fbjni/Meta.h" +#include "fbjni/CoreClasses.h" +#include "fbjni/Hybrid.h" +#include "fbjni/Registration.h" diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Common.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Common.h new file mode 100644 index 0000000000..50111ef83e --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Common.h @@ -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. + */ + +/** @file Common.h + * + * Defining the stuff that don't deserve headers of their own... + */ + +#pragma once + +#include + +#include "../Environment.h" +#include "../ALog.h" + +/// @cond INTERNAL + +namespace facebook { +namespace jni { + +/** + * This needs to be called at library load time, typically in your JNI_OnLoad method. + * + * The intended use is to return the result of initialize() directly + * from JNI_OnLoad and to do nothing else there. Library specific + * initialization code should go in the function passed to initialize + * (which can be, and probably should be, a C++ lambda). This approach + * provides correct error handling and translation errors during + * initialization into Java exceptions when appropriate. + * + * Failure to call this will cause your code to crash in a remarkably + * unhelpful way (typically a segfault) while trying to handle an exception + * which occurs later. + */ +jint initialize(JavaVM*, void(*)()) noexcept; + +namespace internal { + +/** + * Retrieve a pointer the JNI environment of the current thread. + * + * @pre The current thread must be attached to the VM + */ +inline JNIEnv* getEnv() noexcept { + // TODO(T6594868) Benchmark against raw JNI access + return Environment::current(); +} + +// Define to get extremely verbose logging of references and to enable reference stats +#if defined(__ANDROID__) && defined(FBJNI_DEBUG_REFS) +template +inline void dbglog(Args... args) noexcept { + facebook::alog::logv("fbjni_ref", args...); +} +#else +template +inline void dbglog(Args...) noexcept {} +#endif + +}}} + +/// @endcond diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses-inl.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses-inl.h new file mode 100644 index 0000000000..c52c323065 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses-inl.h @@ -0,0 +1,451 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 "Common.h" +#include "Exceptions.h" + +namespace facebook { +namespace jni { + +inline bool isSameObject(alias_ref lhs, alias_ref rhs) noexcept { + return internal::getEnv()->IsSameObject(lhs.get(), rhs.get()) != JNI_FALSE; +} + + +// jobject ///////////////////////////////////////////////////////////////////////////////////////// + +inline JObjectWrapper::JObjectWrapper(jobject reference) noexcept + : this_{reference} +{} + +inline JObjectWrapper::JObjectWrapper(const JObjectWrapper& other) noexcept + : this_{other.this_} { + internal::dbglog("wrapper copy from this=%p ref=%p other=%p", this, other.this_, &other); +} + +inline local_ref JObjectWrapper::getClass() const noexcept { + return adopt_local(internal::getEnv()->GetObjectClass(self())); +} + +inline bool JObjectWrapper::isInstanceOf(alias_ref cls) const noexcept { + return internal::getEnv()->IsInstanceOf(self(), cls.get()) != JNI_FALSE; +} + +template +inline T JObjectWrapper::getFieldValue(JField field) const noexcept { + return field.get(self()); +} + +template +inline local_ref JObjectWrapper::getFieldValue(JField field) noexcept { + return adopt_local(field.get(self())); +} + +template +inline void JObjectWrapper::setFieldValue(JField field, T value) noexcept { + field.set(self(), value); +} + +inline std::string JObjectWrapper::toString() const { + static auto method = findClassLocal("java/lang/Object")->getMethod("toString"); + + return method(self())->toStdString(); +} + +inline void JObjectWrapper::set(jobject reference) noexcept { + this_ = reference; +} + +inline jobject JObjectWrapper::get() const noexcept { + return this_; +} + +inline jobject JObjectWrapper::self() const noexcept { + return this_; +} + +inline void swap(JObjectWrapper& a, JObjectWrapper& b) noexcept { + using std::swap; + swap(a.this_, b.this_); +} + + +// jclass ////////////////////////////////////////////////////////////////////////////////////////// + +namespace detail { + +// This is not a real type. It is used so people won't accidentally +// use a void* to initialize a NativeMethod. +struct NativeMethodWrapper; + +}; + +struct NativeMethod { + const char* name; + std::string descriptor; + detail::NativeMethodWrapper* wrapper; +}; + +inline local_ref JObjectWrapper::getSuperclass() const noexcept { + return adopt_local(internal::getEnv()->GetSuperclass(self())); +} + +inline void JObjectWrapper::registerNatives(std::initializer_list methods) { + const auto env = internal::getEnv(); + + JNINativeMethod jnimethods[methods.size()]; + size_t i = 0; + for (auto it = methods.begin(); it < methods.end(); ++it, ++i) { + jnimethods[i].name = it->name; + jnimethods[i].signature = it->descriptor.c_str(); + jnimethods[i].fnPtr = reinterpret_cast(it->wrapper); + } + + auto result = env->RegisterNatives(self(), jnimethods, methods.size()); + FACEBOOK_JNI_THROW_EXCEPTION_IF(result != JNI_OK); +} + +inline bool JObjectWrapper::isAssignableFrom(alias_ref other) const noexcept { + const auto env = internal::getEnv(); + const auto result = env->IsAssignableFrom(self(), other.get()); + return result; +} + +template +inline JConstructor JObjectWrapper::getConstructor() const { + return getConstructor(jmethod_traits::constructor_descriptor().c_str()); +} + +template +inline JConstructor JObjectWrapper::getConstructor(const char* descriptor) const { + constexpr auto constructor_method_name = ""; + return getMethod(constructor_method_name, descriptor); +} + +template +inline JMethod JObjectWrapper::getMethod(const char* name) const { + return getMethod(name, jmethod_traits::descriptor().c_str()); +} + +template +inline JMethod JObjectWrapper::getMethod( + const char* name, + const char* descriptor) const { + const auto env = internal::getEnv(); + const auto method = env->GetMethodID(self(), name, descriptor); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!method); + return JMethod{method}; +} + +template +inline JStaticMethod JObjectWrapper::getStaticMethod(const char* name) const { + return getStaticMethod(name, jmethod_traits::descriptor().c_str()); +} + +template +inline JStaticMethod JObjectWrapper::getStaticMethod( + const char* name, + const char* descriptor) const { + const auto env = internal::getEnv(); + const auto method = env->GetStaticMethodID(self(), name, descriptor); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!method); + return JStaticMethod{method}; +} + +template +inline JNonvirtualMethod JObjectWrapper::getNonvirtualMethod(const char* name) const { + return getNonvirtualMethod(name, jmethod_traits::descriptor().c_str()); +} + +template +inline JNonvirtualMethod JObjectWrapper::getNonvirtualMethod( + const char* name, + const char* descriptor) const { + const auto env = internal::getEnv(); + const auto method = env->GetMethodID(self(), name, descriptor); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!method); + return JNonvirtualMethod{method}; +} + +template +inline JField(), T>> +JObjectWrapper::getField(const char* name) const { + return getField(name, jtype_traits::descriptor().c_str()); +} + +template +inline JField(), T>> JObjectWrapper::getField( + const char* name, + const char* descriptor) const { + const auto env = internal::getEnv(); + auto field = env->GetFieldID(self(), name, descriptor); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!field); + return JField{field}; +} + +template +inline JStaticField(), T>> JObjectWrapper::getStaticField( + const char* name) const { + return getStaticField(name, jtype_traits::descriptor().c_str()); +} + +template +inline JStaticField(), T>> JObjectWrapper::getStaticField( + const char* name, + const char* descriptor) const { + const auto env = internal::getEnv(); + auto field = env->GetStaticFieldID(self(), name, descriptor); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!field); + return JStaticField{field}; +} + +template +inline T JObjectWrapper::getStaticFieldValue(JStaticField field) const noexcept { + return field.get(self()); +} + +template +inline local_ref JObjectWrapper::getStaticFieldValue(JStaticField field) noexcept { + return adopt_local(field.get(self())); +} + +template +inline void JObjectWrapper::setStaticFieldValue(JStaticField field, T value) noexcept { + field.set(self(), value); +} + +template +inline local_ref JObjectWrapper::newObject( + JConstructor constructor, + Args... args) const { + const auto env = internal::getEnv(); + auto object = env->NewObject(self(), constructor.getId(), args...); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!object); + return adopt_local(static_cast(object)); +} + +inline jclass JObjectWrapper::self() const noexcept { + return static_cast(this_); +} + +inline void registerNatives(const char* name, std::initializer_list methods) { + findClassLocal(name)->registerNatives(methods); +} + + +// jstring ///////////////////////////////////////////////////////////////////////////////////////// + +inline local_ref make_jstring(const std::string& modifiedUtf8) { + return make_jstring(modifiedUtf8.c_str()); +} + +inline jstring JObjectWrapper::self() const noexcept { + return static_cast(this_); +} + + +// jthrowable ////////////////////////////////////////////////////////////////////////////////////// + +inline jthrowable JObjectWrapper::self() const noexcept { + return static_cast(this_); +} + + +// jtypeArray ////////////////////////////////////////////////////////////////////////////////////// +template +inline ElementProxy::ElementProxy( + JObjectWrapper<_jtypeArray*>* target, + size_t idx) + : target_{target}, idx_{idx} {} + +template +inline ElementProxy& ElementProxy::operator=(const T& o) { + target_->setElement(idx_, o); + return *this; +} + +template +inline ElementProxy& ElementProxy::operator=(alias_ref& o) { + target_->setElement(idx_, o.get()); + return *this; +} + +template +inline ElementProxy& ElementProxy::operator=(alias_ref&& o) { + target_->setElement(idx_, o.get()); + return *this; +} + +template +inline ElementProxy& ElementProxy::operator=(const ElementProxy& o) { + auto src = o.target_->getElement(o.idx_); + target_->setElement(idx_, src.get()); + return *this; +} + +template +inline ElementProxy::ElementProxy::operator const local_ref () const { + return target_->getElement(idx_); +} + +template +inline ElementProxy::ElementProxy::operator local_ref () { + return target_->getElement(idx_); +} + +template +inline std::string JObjectWrapper>::bareClassName() { + // Use the initializer to strip off the leading and trailing character. + const char* className = JObjectWrapper::kJavaDescriptor; + return std::string(className + 1, strlen(className) - 2); +} + +template +local_ref> JObjectWrapper>::newArray(size_t size) { + static auto elementClass = findClassStatic( + JObjectWrapper>::bareClassName().c_str()); + const auto env = internal::getEnv(); + auto rawArray = env->NewObjectArray(size, elementClass.get(), nullptr); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!rawArray); + return adopt_local(static_cast>(rawArray)); +} + +template +inline void JObjectWrapper>::setElement(size_t idx, const T& value) { + const auto env = internal::getEnv(); + env->SetObjectArrayElement(static_cast(self()), idx, value); +} + +template +inline local_ref JObjectWrapper>::getElement(size_t idx) { + const auto env = internal::getEnv(); + auto rawElement = env->GetObjectArrayElement(static_cast(self()), idx); + return adopt_local(static_cast(rawElement)); +} + +template +inline size_t JObjectWrapper>::size() { + const auto env = internal::getEnv(); + return env->GetArrayLength(static_cast(self())); +} + +template +inline ElementProxy JObjectWrapper>::operator[](size_t index) { + return ElementProxy(this, index); +} + +template +inline jtypeArray JObjectWrapper>::self() const noexcept { + return static_cast>(this_); +} + + +// jarray ///////////////////////////////////////////////////////////////////////////////////////// + +inline size_t JObjectWrapper::size() const noexcept { + const auto env = internal::getEnv(); + return env->GetArrayLength(self()); +} + +inline jarray JObjectWrapper::self() const noexcept { + return static_cast(this_); +} + + +// PinnedPrimitiveArray /////////////////////////////////////////////////////////////////////////// + +template +inline PinnedPrimitiveArray::PinnedPrimitiveArray(alias_ref array) noexcept + : array_{array} { + get(); +} + +template +PinnedPrimitiveArray::PinnedPrimitiveArray(PinnedPrimitiveArray&& o) noexcept { + array_ = std::move(o.array_); + elements_ = o.elements_; + isCopy_ = o.isCopy_; + size_ = o.size_; + o.elements_ = nullptr; + o.isCopy_ = false; + o.size_ = 0; +} + +template +PinnedPrimitiveArray& +PinnedPrimitiveArray::operator=(PinnedPrimitiveArray&& o) noexcept { + array_ = std::move(o.array_); + elements_ = o.elements_; + isCopy_ = o.isCopy_; + size_ = o.size_; + o.elements_ = nullptr; + o.isCopy_ = false; + o.size_ = 0; + return *this; +} + +template +inline T& PinnedPrimitiveArray::operator[](size_t index) { + FACEBOOK_JNI_THROW_EXCEPTION_IF(elements_ == nullptr); + return elements_[index]; +} + +template +inline bool PinnedPrimitiveArray::isCopy() const noexcept { + return isCopy_ == JNI_TRUE; +} + +template +inline size_t PinnedPrimitiveArray::size() const noexcept { + return size_; +} + +template +inline PinnedPrimitiveArray::~PinnedPrimitiveArray() noexcept { + if (elements_) { + release(); + } +} + +#pragma push_macro("DECLARE_PRIMITIVE_METHODS") +#undef DECLARE_PRIMITIVE_METHODS +#define DECLARE_PRIMITIVE_METHODS(TYPE, NAME) \ +template<> TYPE* PinnedPrimitiveArray::get(); \ +template<> void PinnedPrimitiveArray::release(); \ + +DECLARE_PRIMITIVE_METHODS(jboolean, Boolean) +DECLARE_PRIMITIVE_METHODS(jbyte, Byte) +DECLARE_PRIMITIVE_METHODS(jchar, Char) +DECLARE_PRIMITIVE_METHODS(jshort, Short) +DECLARE_PRIMITIVE_METHODS(jint, Int) +DECLARE_PRIMITIVE_METHODS(jlong, Long) +DECLARE_PRIMITIVE_METHODS(jfloat, Float) +DECLARE_PRIMITIVE_METHODS(jdouble, Double) +#pragma pop_macro("DECLARE_PRIMITIVE_METHODS") + + +template +inline alias_ref JavaClass::javaClassStatic() { + static auto cls = findClassStatic( + std::string(T::kJavaDescriptor + 1, strlen(T::kJavaDescriptor) - 2).c_str()); + return cls; +} + +template +inline local_ref JavaClass::javaClassLocal() { + std::string className(T::kJavaDescriptor + 1, strlen(T::kJavaDescriptor) - 2); + return findClassLocal(className.c_str()); +} + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses.h new file mode 100644 index 0000000000..33e83b8869 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses.h @@ -0,0 +1,488 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 + +/** @file CoreClasses.h + * + * In CoreClasses.h wrappers for the core classes (jobject, jclass, and jstring) is defined + * to provide access to corresponding JNI functions + some conveniance. + */ + +#include "Meta.h" +#include "References.h" + +#include + +#include + +namespace facebook { +namespace jni { + +/// Lookup a class by name. Note this functions returns an alias_ref that +/// points to a leaked global reference. This is appropriate for classes +/// that are never unloaded (which is any class in an Android app and most +/// Java programs). +/// +/// The most common use case for this is storing the result +/// in a "static auto" variable, or a static global. +/// +/// @return Returns a leaked global reference to the class +alias_ref findClassStatic(const char* name); + +/// Lookup a class by name. Note this functions returns a local reference, +/// which means that it must not be stored in a static variable. +/// +/// The most common use case for this is one-time initialization +/// (like caching method ids). +/// +/// @return Returns a global reference to the class +local_ref findClassLocal(const char* name); + +/// Check to see if two references refer to the same object. Comparison with nullptr +/// returns true if and only if compared to another nullptr. A weak reference that +/// refers to a reclaimed object count as nullptr. +bool isSameObject(alias_ref lhs, alias_ref rhs) noexcept; + + +/// Wrapper to provide functionality to jobject references +template<> +class JObjectWrapper { + public: + /// Java type descriptor + static constexpr const char* kJavaDescriptor = "Ljava/lang/Object;"; + + static constexpr const char* get_instantiated_java_descriptor() { return nullptr; } + + /// Wrap an existing JNI reference + JObjectWrapper(jobject reference = nullptr) noexcept; + + // Copy constructor + JObjectWrapper(const JObjectWrapper& other) noexcept; + + /// Get a @ref local_ref of the object's class + local_ref getClass() const noexcept; + + /// Checks if the object is an instance of a class + bool isInstanceOf(alias_ref cls) const noexcept; + + /// Get the primitive value of a field + template + T getFieldValue(JField field) const noexcept; + + /// Get and wrap the value of a field in a @ref local_ref + template + local_ref getFieldValue(JField field) noexcept; + + /// Set the value of field. Any Java type is accepted, including the primitive types + /// and raw reference types. + template + void setFieldValue(JField field, T value) noexcept; + + /// Convenience method to create a std::string representing the object + std::string toString() const; + + protected: + jobject this_; + + private: + template + friend class base_owned_ref; + + template + friend class alias_ref; + + friend void swap(JObjectWrapper& a, JObjectWrapper& b) noexcept; + + void set(jobject reference) noexcept; + jobject get() const noexcept; + jobject self() const noexcept; +}; + +using JObject = JObjectWrapper; + +void swap(JObjectWrapper& a, JObjectWrapper& b) noexcept; + + +/// Wrapper to provide functionality to jclass references +struct NativeMethod; + +template<> +class JObjectWrapper : public JObjectWrapper { + public: + /// Java type descriptor + static constexpr const char* kJavaDescriptor = "Ljava/lang/Class;"; + + using JObjectWrapper::JObjectWrapper; + + /// Get a @local_ref to the super class of this class + local_ref getSuperclass() const noexcept; + + /// Register native methods for the class. Usage looks like this: + /// + /// classRef->registerNatives({ + /// makeNativeMethod("nativeMethodWithAutomaticDescriptor", + /// methodWithAutomaticDescriptor), + /// makeNativeMethod("nativeMethodWithExplicitDescriptor", + /// "(Lcom/facebook/example/MyClass;)V", + /// methodWithExplicitDescriptor), + /// }); + /// + /// By default, C++ exceptions raised will be converted to Java exceptions. + /// To avoid this and get the "standard" JNI behavior of a crash when a C++ + /// exception is crashing out of the JNI method, declare the method noexcept. + void registerNatives(std::initializer_list methods); + + /// Check to see if the class is assignable from another class + /// @pre cls != nullptr + bool isAssignableFrom(alias_ref cls) const noexcept; + + /// Convenience method to lookup the constructor with descriptor as specified by the + /// type arguments + template + JConstructor getConstructor() const; + + /// Convenience method to lookup the constructor with specified descriptor + template + JConstructor getConstructor(const char* descriptor) const; + + /// Look up the method with given name and descriptor as specified with the type arguments + template + JMethod getMethod(const char* name) const; + + /// Look up the method with given name and descriptor + template + JMethod getMethod(const char* name, const char* descriptor) const; + + /// Lookup the field with the given name and deduced descriptor + template + JField(), T>> getField(const char* name) const; + + /// Lookup the field with the given name and descriptor + template + JField(), T>> getField(const char* name, const char* descriptor) const; + + /// Lookup the static field with the given name and deduced descriptor + template + JStaticField(), T>> getStaticField(const char* name) const; + + /// Lookup the static field with the given name and descriptor + template + JStaticField(), T>> getStaticField( + const char* name, + const char* descriptor) const; + + /// Get the primitive value of a static field + template + T getStaticFieldValue(JStaticField field) const noexcept; + + /// Get and wrap the value of a field in a @ref local_ref + template + local_ref getStaticFieldValue(JStaticField field) noexcept; + + /// Set the value of field. Any Java type is accepted, including the primitive types + /// and raw reference types. + template + void setStaticFieldValue(JStaticField field, T value) noexcept; + + /// Allocates a new object and invokes the specified constructor + template + local_ref newObject(JConstructor constructor, Args... args) const; + + /// Look up the static method with given name and descriptor as specified with the type arguments + template + JStaticMethod getStaticMethod(const char* name) const; + + /// Look up the static method with given name and descriptor + template + JStaticMethod getStaticMethod(const char* name, const char* descriptor) const; + + /// Look up the non virtual method with given name and descriptor as specified with the + /// type arguments + template + JNonvirtualMethod getNonvirtualMethod(const char* name) const; + + /// Look up the non virtual method with given name and descriptor + template + JNonvirtualMethod getNonvirtualMethod(const char* name, const char* descriptor) const; + + private: + jclass self() const noexcept; +}; + +using JClass = JObjectWrapper; + +// Convenience method to register methods on a class without holding +// onto the class object. +void registerNatives(const char* name, std::initializer_list methods); + +/// Wrapper to provide functionality to jstring references +template<> +class JObjectWrapper : public JObjectWrapper { + public: + /// Java type descriptor + static constexpr const char* kJavaDescriptor = "Ljava/lang/String;"; + + using JObjectWrapper::JObjectWrapper; + + /// Convenience method to convert a jstring object to a std::string + std::string toStdString() const; + + private: + jstring self() const noexcept; +}; + +/// Convenience functions to convert a std::string or const char* into a @ref local_ref to a +/// jstring +local_ref make_jstring(const char* modifiedUtf8); +local_ref make_jstring(const std::string& modifiedUtf8); + +using JString = JObjectWrapper; + +/// Wrapper to provide functionality to jthrowable references +template<> +class JObjectWrapper : public JObjectWrapper { + public: + /// Java type descriptor + static constexpr const char* kJavaDescriptor = "Ljava/lang/Throwable;"; + + using JObjectWrapper::JObjectWrapper; + + private: + jthrowable self() const noexcept; +}; + + +/// @cond INTERNAL +template class _jtypeArray : public _jobjectArray {}; +// @endcond +/// Wrapper to provide functionality for arrays of j-types +template using jtypeArray = _jtypeArray*; + +template +class ElementProxy { + private: + JObjectWrapper<_jtypeArray*>* target_; + size_t idx_; + + public: + ElementProxy(JObjectWrapper<_jtypeArray*>* target, size_t idx); + + ElementProxy& operator=(const T& o); + + ElementProxy& operator=(alias_ref& o); + + ElementProxy& operator=(alias_ref&& o); + + ElementProxy& operator=(const ElementProxy& o); + + operator const local_ref () const; + + operator local_ref (); + }; + +template +class JObjectWrapper> : public JObjectWrapper { + public: + static constexpr const char* kJavaDescriptor = nullptr; + static std::string get_instantiated_java_descriptor() { + return jtype_traits::array_descriptor(); + }; + + using JObjectWrapper::JObjectWrapper; + + /// Allocate a new array from Java heap, for passing as a JNI parameter or return value. + /// NOTE: if using as a return value, you want to call release() instead of get() on the + /// smart pointer. + static local_ref> newArray(size_t count); + + /// Assign an object to the array. + /// Typically you will use the shorthand (*ref)[idx]=value; + void setElement(size_t idx, const T& value); + + /// Read an object from the array. + /// Typically you will use the shorthand + /// T value = (*ref)[idx]; + /// If you use auto, you'll get an ElementProxy, which may need to be cast. + local_ref getElement(size_t idx); + + /// Get the size of the array. + size_t size(); + + /// EXPERIMENTAL SUBSCRIPT SUPPORT + /// This implementation of [] returns a proxy object which then has a bunch of specializations + /// (adopt_local free function, operator= and casting overloads on the ElementProxy) that can + /// make code look like it is dealing with a T rather than an obvious proxy. In particular, the + /// proxy in this iteration does not read a value and therefore does not create a LocalRef + /// until one of these other operators is used. There are certainly holes that you may find + /// by using idioms that haven't been tried yet. Consider yourself warned. On the other hand, + /// it does make for some idiomatic assignment code; see TestBuildStringArray in fbjni_tests + /// for some examples. + ElementProxy operator[](size_t idx); + + private: + jtypeArray self() const noexcept; + static std::string bareClassName(); +}; + +template +using JArrayClass = JObjectWrapper>; + +template +local_ref> adopt_local_array(jobjectArray ref) { + return adopt_local(static_cast>(ref)); +} + +template +local_ref adopt_local(ElementProxy elementProxy) { + return static_cast>(elementProxy); +} + +/// Wrapper to provide functionality to jarray references. +/// This is an empty holder by itself. Construct a PinnedPrimitiveArray to actually interact with +/// the elements of the array. +template<> +class JObjectWrapper : public JObjectWrapper { + public: + static constexpr const char* kJavaDescriptor = nullptr; + + using JObjectWrapper::JObjectWrapper; + size_t size() const noexcept; + + private: + jarray self() const noexcept; +}; + +using JArray = JObjectWrapper; + +template +class PinnedPrimitiveArray; + +#pragma push_macro("DECLARE_PRIMITIVE_ARRAY_UTILS") +#undef DECLARE_PRIMITIVE_ARRAY_UTILS +#define DECLARE_PRIMITIVE_ARRAY_UTILS(TYPE, DESC) \ +local_ref make_ ## TYPE ## _array(jsize size); \ + \ +template<> class JObjectWrapper : public JArray { \ + public: \ + static constexpr const char* kJavaDescriptor = "[" # DESC; \ + \ + using JArray::JArray; \ + \ + j ## TYPE* getRegion(jsize start, jsize length, j ## TYPE* buf); \ + std::unique_ptr getRegion(jsize start, jsize length); \ + void setRegion(jsize start, jsize length, j ## TYPE* buf); \ + PinnedPrimitiveArray pin(); \ + \ + private: \ + j ## TYPE ## Array self() const noexcept { \ + return static_cast(this_); \ + } \ +} \ + +DECLARE_PRIMITIVE_ARRAY_UTILS(boolean, "Z"); +DECLARE_PRIMITIVE_ARRAY_UTILS(byte, "B"); +DECLARE_PRIMITIVE_ARRAY_UTILS(char, "C"); +DECLARE_PRIMITIVE_ARRAY_UTILS(short, "S"); +DECLARE_PRIMITIVE_ARRAY_UTILS(int, "I"); +DECLARE_PRIMITIVE_ARRAY_UTILS(long, "J"); +DECLARE_PRIMITIVE_ARRAY_UTILS(float, "F"); +DECLARE_PRIMITIVE_ARRAY_UTILS(double, "D"); + +#pragma pop_macro("DECLARE_PRIMITIVE_ARRAY_UTILS") + + +/// RAII class for pinned primitive arrays +/// This currently only supports read/write access to existing java arrays. You can't create a +/// primitive array this way yet. This class also pins the entire array into memory during the +/// lifetime of the PinnedPrimitiveArray. If you need to unpin the array manually, call the +/// release() function. During a long-running block of code, you should unpin the array as soon +/// as you're done with it, to avoid holding up the Java garbage collector. +template +class PinnedPrimitiveArray { + public: + static_assert(is_jni_primitive::value, + "PinnedPrimitiveArray requires primitive jni type."); + + PinnedPrimitiveArray(PinnedPrimitiveArray&&) noexcept; + PinnedPrimitiveArray(const PinnedPrimitiveArray&) = delete; + ~PinnedPrimitiveArray() noexcept; + + PinnedPrimitiveArray& operator=(PinnedPrimitiveArray&&) noexcept; + PinnedPrimitiveArray& operator=(const PinnedPrimitiveArray&) = delete; + + T* get(); + void release(); + + const T& operator[](size_t index) const; + T& operator[](size_t index); + bool isCopy() const noexcept; + size_t size() const noexcept; + + private: + alias_ref array_; + T* elements_; + jboolean isCopy_; + size_t size_; + + PinnedPrimitiveArray(alias_ref) noexcept; + + friend class JObjectWrapper; + friend class JObjectWrapper; + friend class JObjectWrapper; + friend class JObjectWrapper; + friend class JObjectWrapper; + friend class JObjectWrapper; + friend class JObjectWrapper; + friend class JObjectWrapper; +}; + + +// Together, these classes allow convenient use of any class with the fbjni +// helpers. To use: +// +// struct MyClass : public JavaClass { +// constexpr static auto kJavaDescriptor = "Lcom/example/package/MyClass;"; +// }; +// +// alias_ref myClass = foo(); + +template +class JavaClass { +public: + // JNI pattern for jobject assignable pointer + struct _javaobject : public _jobject { + typedef T javaClass; + }; + typedef _javaobject* javaobject; + + static alias_ref javaClassStatic(); + static local_ref javaClassLocal(); +}; + +template +class JObjectWrapper::value && + std::is_class::type::javaClass>::value + >::type> + : public JObjectWrapper { +public: + static constexpr const char* kJavaDescriptor = + std::remove_pointer::type::javaClass::kJavaDescriptor; + + using JObjectWrapper::JObjectWrapper; +}; + +}} + +#include "CoreClasses-inl.h" +// This is here because code in Meta-inl.h uses alias_ref, which +// requires JObjectWrapper to be concrete before it can work. +#include "Meta-inl.h" diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.cpp b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.cpp new file mode 100644 index 0000000000..c5dfb3f144 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.cpp @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "Exceptions.h" +#include "CoreClasses.h" +#include "../ALog.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace facebook { +namespace jni { + +// CommonJniExceptions ///////////////////////////////////////////////////////////////////////////// + +class CommonJniExceptions { + public: + static void init(); + + static jclass getThrowableClass() { + return throwableClass_; + } + + static jclass getUnknownCppExceptionClass() { + return unknownCppExceptionClass_; + } + + static jthrowable getUnknownCppExceptionObject() { + return unknownCppExceptionObject_; + } + + static jthrowable getRuntimeExceptionObject() { + return runtimeExceptionObject_; + } + + private: + static jclass throwableClass_; + static jclass unknownCppExceptionClass_; + static jthrowable unknownCppExceptionObject_; + static jthrowable runtimeExceptionObject_; +}; + +// The variables in this class are all JNI global references and are intentionally leaked because +// we assume this library cannot be unloaded. These global references are created manually instead +// of using global_ref from References.h to avoid circular dependency. +jclass CommonJniExceptions::throwableClass_ = nullptr; +jclass CommonJniExceptions::unknownCppExceptionClass_ = nullptr; +jthrowable CommonJniExceptions::unknownCppExceptionObject_ = nullptr; +jthrowable CommonJniExceptions::runtimeExceptionObject_ = nullptr; + + +// Variable to guarantee that fallback exceptions have been initialized early. We don't want to +// do pure dynamic initialization -- we want to warn programmers early that they need to run the +// helpers at library load time instead of lazily getting them when the exception helpers are +// first used. +static std::atomic gIsInitialized(false); + +void CommonJniExceptions::init() { + JNIEnv* env = internal::getEnv(); + FBASSERTMSGF(env, "Could not get JNI Environment"); + + // Throwable class + jclass localThrowableClass = env->FindClass("java/lang/Throwable"); + FBASSERT(localThrowableClass); + throwableClass_ = static_cast(env->NewGlobalRef(localThrowableClass)); + FBASSERT(throwableClass_); + env->DeleteLocalRef(localThrowableClass); + + // UnknownCppException class + jclass localUnknownCppExceptionClass = env->FindClass("com/facebook/jni/UnknownCppException"); + FBASSERT(localUnknownCppExceptionClass); + jmethodID unknownCppExceptionConstructorMID = env->GetMethodID( + localUnknownCppExceptionClass, + "", + "()V"); + FBASSERT(unknownCppExceptionConstructorMID); + unknownCppExceptionClass_ = static_cast(env->NewGlobalRef(localUnknownCppExceptionClass)); + FBASSERT(unknownCppExceptionClass_); + env->DeleteLocalRef(localUnknownCppExceptionClass); + + // UnknownCppException object + jthrowable localUnknownCppExceptionObject = static_cast(env->NewObject( + unknownCppExceptionClass_, + unknownCppExceptionConstructorMID)); + FBASSERT(localUnknownCppExceptionObject); + unknownCppExceptionObject_ = static_cast(env->NewGlobalRef( + localUnknownCppExceptionObject)); + FBASSERT(unknownCppExceptionObject_); + env->DeleteLocalRef(localUnknownCppExceptionObject); + + // RuntimeException object + jclass localRuntimeExceptionClass = env->FindClass("java/lang/RuntimeException"); + FBASSERT(localRuntimeExceptionClass); + + jmethodID runtimeExceptionConstructorMID = env->GetMethodID( + localRuntimeExceptionClass, + "", + "()V"); + FBASSERT(runtimeExceptionConstructorMID); + jthrowable localRuntimeExceptionObject = static_cast(env->NewObject( + localRuntimeExceptionClass, + runtimeExceptionConstructorMID)); + FBASSERT(localRuntimeExceptionObject); + runtimeExceptionObject_ = static_cast(env->NewGlobalRef(localRuntimeExceptionObject)); + FBASSERT(runtimeExceptionObject_); + + env->DeleteLocalRef(localRuntimeExceptionClass); + env->DeleteLocalRef(localRuntimeExceptionObject); +} + + +// initExceptionHelpers() ////////////////////////////////////////////////////////////////////////// + +void internal::initExceptionHelpers() { + CommonJniExceptions::init(); + gIsInitialized.store(true, std::memory_order_seq_cst); +} + +void assertIfExceptionsNotInitialized() { + // Use relaxed memory order because we don't need memory barriers. + // The real init-once enforcement is done by the compiler for the + // "static" in initExceptionHelpers. + FBASSERTMSGF(gIsInitialized.load(std::memory_order_relaxed), + "initExceptionHelpers was never called!"); +} + +// Exception throwing & translating functions ////////////////////////////////////////////////////// + +// Functions that throw Java exceptions + +namespace { + +void setJavaExceptionAndAbortOnFailure(jthrowable throwable) noexcept { + assertIfExceptionsNotInitialized(); + JNIEnv* env = internal::getEnv(); + if (throwable) { + env->Throw(throwable); + } + if (env->ExceptionCheck() != JNI_TRUE) { + std::abort(); + } +} + +void setDefaultException() noexcept { + assertIfExceptionsNotInitialized(); + setJavaExceptionAndAbortOnFailure(CommonJniExceptions::getRuntimeExceptionObject()); +} + +void setCppSystemErrorExceptionInJava(const std::system_error& ex) noexcept { + assertIfExceptionsNotInitialized(); + JNIEnv* env = internal::getEnv(); + jclass cppSystemErrorExceptionClass = env->FindClass( + "com/facebook/jni/CppSystemErrorException"); + if (!cppSystemErrorExceptionClass) { + setDefaultException(); + return; + } + jmethodID constructorMID = env->GetMethodID( + cppSystemErrorExceptionClass, + "", + "(Ljava/lang/String;I)V"); + if (!constructorMID) { + setDefaultException(); + return; + } + jthrowable cppSystemErrorExceptionObject = static_cast(env->NewObject( + cppSystemErrorExceptionClass, + constructorMID, + env->NewStringUTF(ex.what()), + ex.code().value())); + setJavaExceptionAndAbortOnFailure(cppSystemErrorExceptionObject); +} + +template +void setNewJavaException(jclass exceptionClass, const char* fmt, ARGS... args) { + assertIfExceptionsNotInitialized(); + int msgSize = snprintf(nullptr, 0, fmt, args...); + JNIEnv* env = internal::getEnv(); + + try { + char *msg = (char*) alloca(msgSize); + snprintf(msg, kMaxExceptionMessageBufferSize, fmt, args...); + env->ThrowNew(exceptionClass, msg); + } catch (...) { + env->ThrowNew(exceptionClass, ""); + } + + if (env->ExceptionCheck() != JNI_TRUE) { + setDefaultException(); + } +} + +void setNewJavaException(jclass exceptionClass, const char* msg) { + assertIfExceptionsNotInitialized(); + setNewJavaException(exceptionClass, "%s", msg); +} + +template +void setNewJavaException(const char* className, const char* fmt, ARGS... args) { + assertIfExceptionsNotInitialized(); + JNIEnv* env = internal::getEnv(); + jclass exceptionClass = env->FindClass(className); + if (env->ExceptionCheck() != JNI_TRUE && !exceptionClass) { + // If FindClass() has failed but no exception has been thrown, throw a default exception. + setDefaultException(); + return; + } + setNewJavaException(exceptionClass, fmt, args...); +} + +} + +// Functions that throw C++ exceptions + +// TODO(T6618159) Take a stack dump here to save context if it results in a crash when propagated +void throwPendingJniExceptionAsCppException() { + assertIfExceptionsNotInitialized(); + JNIEnv* env = internal::getEnv(); + if (env->ExceptionCheck() == JNI_FALSE) { + return; + } + + jthrowable throwable = env->ExceptionOccurred(); + if (!throwable) { + throw std::runtime_error("Unable to get pending JNI exception."); + } + + env->ExceptionClear(); + throw JniException(throwable); +} + +void throwCppExceptionIf(bool condition) { + assertIfExceptionsNotInitialized(); + if (!condition) { + return; + } + + JNIEnv* env = internal::getEnv(); + if (env->ExceptionCheck() == JNI_TRUE) { + throwPendingJniExceptionAsCppException(); + return; + } + + throw JniException(); +} + +void throwNewJavaException(jthrowable throwable) { + throw JniException(throwable); +} + +void throwNewJavaException(const char* throwableName, const char* msg) { + // If anything of the fbjni calls fail, an exception of a suitable + // form will be thrown, which is what we want. + auto throwableClass = findClassLocal(throwableName); + auto throwable = throwableClass->newObject( + throwableClass->getConstructor(), + make_jstring(msg).release()); + throwNewJavaException(throwable.get()); +} + +// Translate C++ to Java Exception + +void translatePendingCppExceptionToJavaException() noexcept { + assertIfExceptionsNotInitialized(); + try { + try { + throw; + } catch(const JniException& ex) { + ex.setJavaException(); + } catch(const std::ios_base::failure& ex) { + setNewJavaException("java/io/IOException", ex.what()); + } catch(const std::bad_alloc& ex) { + setNewJavaException("java/lang/OutOfMemoryError", ex.what()); + } catch(const std::out_of_range& ex) { + setNewJavaException("java/lang/ArrayIndexOutOfBoundsException", ex.what()); + } catch(const std::system_error& ex) { + setCppSystemErrorExceptionInJava(ex); + } catch(const std::runtime_error& ex) { + setNewJavaException("java/lang/RuntimeException", ex.what()); + } catch(const std::exception& ex) { + setNewJavaException("com/facebook/jni/CppException", ex.what()); + } catch(const char* msg) { + setNewJavaException(CommonJniExceptions::getUnknownCppExceptionClass(), msg); + } catch(...) { + setJavaExceptionAndAbortOnFailure(CommonJniExceptions::getUnknownCppExceptionObject()); + } + } catch(...) { + // This block aborts the program, if something bad happens when handling exceptions, thus + // keeping this function noexcept. + std::abort(); + } +} + +// JniException //////////////////////////////////////////////////////////////////////////////////// + +const std::string JniException::kExceptionMessageFailure_ = "Unable to get exception message."; + +JniException::JniException() : JniException(CommonJniExceptions::getRuntimeExceptionObject()) { } + +JniException::JniException(jthrowable throwable) : isMessageExtracted_(false) { + assertIfExceptionsNotInitialized(); + throwableGlobalRef_ = static_cast(internal::getEnv()->NewGlobalRef(throwable)); + if (!throwableGlobalRef_) { + throw std::bad_alloc(); + } +} + +JniException::JniException(JniException &&rhs) + : throwableGlobalRef_(std::move(rhs.throwableGlobalRef_)), + what_(std::move(rhs.what_)), + isMessageExtracted_(rhs.isMessageExtracted_) { + rhs.throwableGlobalRef_ = nullptr; +} + +JniException::JniException(const JniException &rhs) + : what_(rhs.what_), isMessageExtracted_(rhs.isMessageExtracted_) { + JNIEnv* env = internal::getEnv(); + if (rhs.getThrowable()) { + throwableGlobalRef_ = static_cast(env->NewGlobalRef(rhs.getThrowable())); + if (!throwableGlobalRef_) { + throw std::bad_alloc(); + } + } else { + throwableGlobalRef_ = nullptr; + } +} + +JniException::~JniException() noexcept { + if (throwableGlobalRef_) { + internal::getEnv()->DeleteGlobalRef(throwableGlobalRef_); + } +} + +jthrowable JniException::getThrowable() const noexcept { + return throwableGlobalRef_; +} + +// TODO 6900503: consider making this thread-safe. +void JniException::populateWhat() const noexcept { + JNIEnv* env = internal::getEnv(); + + jmethodID toStringMID = env->GetMethodID( + CommonJniExceptions::getThrowableClass(), + "toString", + "()Ljava/lang/String;"); + jstring messageJString = (jstring) env->CallObjectMethod( + throwableGlobalRef_, + toStringMID); + + isMessageExtracted_ = true; + + if (env->ExceptionCheck()) { + env->ExceptionClear(); + what_ = kExceptionMessageFailure_; + return; + } + + const char* chars = env->GetStringUTFChars(messageJString, nullptr); + if (!chars) { + what_ = kExceptionMessageFailure_; + return; + } + + try { + what_ = std::string(chars); + } catch(...) { + what_ = kExceptionMessageFailure_; + } + + env->ReleaseStringUTFChars(messageJString, chars); +} + +const char* JniException::what() const noexcept { + if (!isMessageExtracted_) { + populateWhat(); + } + return what_.c_str(); +} + +void JniException::setJavaException() const noexcept { + setJavaExceptionAndAbortOnFailure(throwableGlobalRef_); +} + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.h new file mode 100644 index 0000000000..9ba9367f67 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.h @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + * @file Exceptions.h + * + * After invoking a JNI function that can throw a Java exception, the macro + * @ref FACEBOOK_JNI_THROW_PENDING_EXCEPTION() or @ref FACEBOOK_JNI_THROW_EXCEPTION_IF() + * should be invoked. + * + * IMPORTANT! IMPORTANT! IMPORTANT! IMPORTANT! IMPORTANT! IMPORTANT! IMPORTANT! IMPORTANT! + * To use these methods you MUST call initExceptionHelpers() when your library is loaded. + */ + +#pragma once + +#include +#include +#include + +#include + +#include "Common.h" + +// If a pending JNI Java exception is found, wraps it in a JniException object and throws it as +// a C++ exception. +#define FACEBOOK_JNI_THROW_PENDING_EXCEPTION() \ + ::facebook::jni::throwPendingJniExceptionAsCppException() + +// If the condition is true, throws a JniException object, which wraps the pending JNI Java +// exception if any. If no pending exception is found, throws a JniException object that wraps a +// RuntimeException throwable.  +#define FACEBOOK_JNI_THROW_EXCEPTION_IF(CONDITION) \ + ::facebook::jni::throwCppExceptionIf(CONDITION) + +namespace facebook { +namespace jni { + +namespace internal { + void initExceptionHelpers(); +} + +/** + * Before using any of the state initialized above, call this. It + * will assert if initialization has not yet occurred. + */ +void assertIfExceptionsNotInitialized(); + +// JniException //////////////////////////////////////////////////////////////////////////////////// + +/** + * This class wraps a Java exception into a C++ exception; if the exception is routed back + * to the Java side, it can be unwrapped and just look like a pure Java interaction. The class + * is resilient to errors while creating the exception, falling back to some pre-allocated + * exceptions if a new one cannot be allocated or populated. + * + * Note: the what() method of this class is not thread-safe (t6900503). + */ +class JniException : public std::exception { + public: + JniException(); + + explicit JniException(jthrowable throwable); + + JniException(JniException &&rhs); + + JniException(const JniException &other); + + ~JniException() noexcept; + + jthrowable getThrowable() const noexcept; + + virtual const char* what() const noexcept; + + void setJavaException() const noexcept; + + private: + jthrowable throwableGlobalRef_; + mutable std::string what_; + mutable bool isMessageExtracted_; + const static std::string kExceptionMessageFailure_; + + void populateWhat() const noexcept; +}; + +// Exception throwing & translating functions ////////////////////////////////////////////////////// + +// Functions that throw C++ exceptions + +void throwPendingJniExceptionAsCppException(); + +void throwCppExceptionIf(bool condition); + +static const int kMaxExceptionMessageBufferSize = 512; + +[[noreturn]] void throwNewJavaException(jthrowable); + +[[noreturn]] void throwNewJavaException(const char* throwableName, const char* msg); + +// These methods are the preferred way to throw a Java exception from +// a C++ function. They create and throw a C++ exception which wraps +// a Java exception, so the C++ flow is interrupted. Then, when +// translatePendingCppExceptionToJavaException is called at the +// topmost level of the native stack, the wrapped Java exception is +// thrown to the java caller. +template +[[noreturn]] void throwNewJavaException(const char* throwableName, const char* fmt, Args... args) { + assertIfExceptionsNotInitialized(); + int msgSize = snprintf(nullptr, 0, fmt, args...); + + char *msg = (char*) alloca(msgSize); + snprintf(msg, kMaxExceptionMessageBufferSize, fmt, args...); + throwNewJavaException(throwableName, msg); +} + +// Identifies any pending C++ exception and throws it as a Java exception. If the exception can't +// be thrown, it aborts the program. This is a noexcept function at C++ level. +void translatePendingCppExceptionToJavaException() noexcept; + +// For convenience, some exception names in java.lang are available here. + +const char* const gJavaLangIllegalArgumentException = "java/lang/IllegalArgumentException"; + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.cpp b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.cpp new file mode 100644 index 0000000000..ebcb778de9 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.cpp @@ -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. + */ + +#include "Hybrid.h" + +#include "Exceptions.h" +#include "Registration.h" + +namespace facebook { +namespace jni { + +namespace detail { + +void setNativePointer(alias_ref hybridData, + std::unique_ptr new_value) { + static auto pointerField = hybridData->getClass()->getField("mNativePointer"); + auto* old_value = reinterpret_cast(hybridData->getFieldValue(pointerField)); + if (new_value) { + // Modify should only ever be called once with a non-null + // new_value. If this happens again it's a programmer error, so + // blow up. + FBASSERTMSGF(old_value == 0, "Attempt to set C++ native pointer twice"); + } else if (old_value == 0) { + return; + } + // delete on a null pointer is defined to be a noop. + delete old_value; + // This releases ownership from the unique_ptr, and passes the pointer, and + // ownership of it, to HybridData which is managed by the java GC. The + // finalizer on hybridData calls resetNative which will delete the object, if + // reseetNative has not already been called. + hybridData->setFieldValue(pointerField, reinterpret_cast(new_value.release())); +} + +BaseHybridClass* getNativePointer(alias_ref hybridData) { + static auto pointerField = hybridData->getClass()->getField("mNativePointer"); + auto* value = reinterpret_cast(hybridData->getFieldValue(pointerField)); + if (!value) { + throwNewJavaException("java/lang/NullPointerException", "java.lang.NullPointerException"); + } + return value; +} + +local_ref getHybridData(alias_ref jthis, + JField field) { + auto hybridData = jthis->getFieldValue(field); + if (!hybridData) { + throwNewJavaException("java/lang/NullPointerException", "java.lang.NullPointerException"); + } + return hybridData; +} + +} + +namespace { + +void resetNative(alias_ref jthis) { + detail::setNativePointer(jthis, nullptr); +} + +} + +void HybridDataOnLoad() { + registerNatives("com/facebook/jni/HybridData", { + makeNativeMethod("resetNative", resetNative), + }); +} + +}} + diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.h new file mode 100644 index 0000000000..cfc4b03e83 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.h @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 "CoreClasses.h" + +namespace facebook { +namespace jni { + +class BaseHybridClass { +public: + virtual ~BaseHybridClass() {} +}; + +namespace detail { + +struct HybridData : public JavaClass { + constexpr static auto kJavaDescriptor = "Lcom/facebook/jni/HybridData;"; +}; + +void setNativePointer(alias_ref hybridData, + std::unique_ptr new_value); +BaseHybridClass* getNativePointer(alias_ref hybridData); +local_ref getHybridData(alias_ref jthis, + JField field); + +// Normally, pass through types unmolested. +template +struct Convert { + typedef T jniType; + static jniType fromJni(jniType t) { + return t; + } + static jniType toJniRet(jniType t) { + return t; + } + static jniType toCall(jniType t) { + return t; + } +}; + +// This is needed for return conversion +template <> +struct Convert { + typedef void jniType; +}; + +// convert to std::string from jstring +template <> +struct Convert { + typedef jstring jniType; + static std::string fromJni(jniType t) { + return wrap_alias(t)->toStdString(); + } + static jniType toJniRet(const std::string& t) { + return make_jstring(t).release(); + } + static local_ref toCall(const std::string& t) { + return make_jstring(t); + } +}; + +// convert return from const char* +template <> +struct Convert { + typedef jstring jniType; + // no automatic synthesis of const char*. (It can't be freed.) + static jniType toJniRet(const char* t) { + return make_jstring(t).release(); + } + static local_ref toCall(const char* t) { + return make_jstring(t); + } +}; + +// convert to alias_ref from T +template +struct Convert> { + typedef T jniType; + static alias_ref fromJni(jniType t) { + return wrap_alias(t); + } + static jniType toJniRet(alias_ref t) { + return t.get(); + } + static jniType toCall(alias_ref t) { + return t.get(); + } +}; + +// convert return from local_ref +template +struct Convert> { + typedef T jniType; + // No automatic synthesis of local_ref + static jniType toJniRet(local_ref t) { + return t.release(); + } + static jniType toCall(local_ref t) { + return t.get(); + } +}; + +// convert return from global_ref +template +struct Convert> { + typedef T jniType; + // No automatic synthesis of global_ref + static jniType toJniRet(global_ref t) { + return t.get(); + } + static jniType toCall(global_ref t) { + return t.get(); + } +}; + +// In order to avoid potentially filling the jni locals table, +// temporary objects (right now, this is just jstrings) need to be +// released. This is done by returning a holder which autoconverts to +// jstring. This is only relevant when the jniType is passed down, as +// in newObjectJavaArgs. + +template +inline T callToJni(T&& t) { + return t; +} + +inline jstring callToJni(local_ref&& sref) { + return sref.get(); +} + +struct jstring_holder { + local_ref s_; + jstring_holder(const char* s) : s_(make_jstring(s)) {} + operator jstring() { return s_.get(); } +}; + +} + +template +class HybridClass : public BaseHybridClass + , public JavaClass { +public: + typedef detail::HybridData::javaobject jhybriddata; + typedef typename JavaClass::javaobject jhybridobject; + + // I'm not sure why I need this, but I get errors without it. + using JavaClass::javaClassStatic; + +protected: + typedef HybridClass HybridBase; + + // This ensures that a C++ hybrid part cannot be created on its own + // by default. If a hybrid wants to enable this, it can provide its + // own public ctor, or change the accessibility of this to public. + HybridClass() = default; + + static void registerHybrid(std::initializer_list methods) { + javaClassStatic()->registerNatives(methods); + } + + static local_ref makeHybridData(std::unique_ptr cxxPart) { + static auto dataCtor = detail::HybridData::javaClassStatic()->getConstructor(); + auto hybridData = detail::HybridData::javaClassStatic()->newObject(dataCtor); + detail::setNativePointer(hybridData, std::move(cxxPart)); + return hybridData; + } + + template + static local_ref makeCxxInstance(Args&&... args) { + return makeHybridData(std::unique_ptr(new T(std::forward(args)...))); + } + +public: + // Factory method for creating a hybrid object where the arguments + // are used to initialize the C++ part directly without passing them + // through java. This method requires the Java part to have a ctor + // which takes a HybridData, and for the C++ part to have a ctor + // compatible with the arguments passed here. For safety, the ctor + // can be private, and the hybrid declared a friend of its base, so + // the hybrid can only be created from here. + // + // Exception behavior: This can throw an exception if creating the + // C++ object fails, or any JNI methods throw. + template + static local_ref newObjectCxxArgs(Args&&... args) { + auto hybridData = makeCxxInstance(std::forward(args)...); + static auto ctor = javaClassStatic()->template getConstructor(); + return javaClassStatic()->newObject(ctor, hybridData.get()); + } + + // Factory method for creating a hybrid object where the arguments + // are passed to the java ctor. + template + static local_ref newObjectJavaArgs(Args&&... args) { + static auto ctor = + javaClassStatic()->template getConstructor< + jhybridobject(typename detail::Convert::type>::jniType...)>(); + // This can't use the same impl as Convert::toJniRet because that + // function sometimes creates and then releases local_refs, which + // could potentially cause the locals table to fill. Instead, we + // use two calls, one which can return a local_ref if needed, and + // a second which extracts its value. The lifetime of the + // local_ref is the expression, after which it is destroyed and + // the local_ref is cleaned up. + auto lref = + javaClassStatic()->newObject( + ctor, detail::callToJni( + detail::Convert::type>::toCall(args))...); + return lref; + } + + // If a hybrid class throws an exception which derives from + // std::exception, it will be passed to mapException on the hybrid + // class, or nearest ancestor. This allows boilerplate exception + // translation code (for example, calling throwNewJavaException on a + // particular java class) to be hoisted to a common function. If + // mapException returns, then the std::exception will be translated + // to Java. + static void mapException(const std::exception& ex) {} +}; + +// Given a *_ref object which refers to a hybrid class, this will reach inside +// of it, find the mHybridData, extract the C++ instance pointer, cast it to +// the appropriate type, and return it. +template +inline typename std::remove_pointer::type::javaClass* cthis(T jthis) { + static auto dataField = + jthis->getClass()->template getField("mHybridData"); + // I'd like to use dynamic_cast here, but -fno-rtti is the default. + auto* value = static_cast::type::javaClass*>( + detail::getNativePointer(detail::getHybridData(jthis, dataField))); + // This would require some serious programmer error. + FBASSERTMSGF(value != 0, "Incorrect C++ type in hybrid field"); + return value; +} + +void HybridDataOnLoad(); + +} +} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta-inl.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta-inl.h new file mode 100644 index 0000000000..cc2d8383d0 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta-inl.h @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 "Common.h" +#include "Exceptions.h" + +namespace facebook { +namespace jni { + +// JMethod ///////////////////////////////////////////////////////////////////////////////////////// + +inline JMethodBase::JMethodBase(jmethodID method_id) noexcept + : method_id_{method_id} +{} + +inline JMethodBase::operator bool() const noexcept { + return method_id_ != nullptr; +} + +inline jmethodID JMethodBase::getId() const noexcept { + return method_id_; +} + +template +inline void JMethod::operator()(alias_ref self, Args... args) { + const auto env = internal::getEnv(); + env->CallVoidMethod(self.get(), getId(), args...); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); +} + +#pragma push_macro("DEFINE_PRIMITIVE_CALL") +#undef DEFINE_PRIMITIVE_CALL +#define DEFINE_PRIMITIVE_CALL(TYPE, METHOD) \ +template \ +inline TYPE JMethod::operator()(alias_ref self, Args... args) { \ + const auto env = internal::getEnv(); \ + auto result = env->Call ## METHOD ## Method(self.get(), getId(), args...); \ + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); \ + return result; \ +} + +DEFINE_PRIMITIVE_CALL(jboolean, Boolean) +DEFINE_PRIMITIVE_CALL(jbyte, Byte) +DEFINE_PRIMITIVE_CALL(jchar, Char) +DEFINE_PRIMITIVE_CALL(jshort, Short) +DEFINE_PRIMITIVE_CALL(jint, Int) +DEFINE_PRIMITIVE_CALL(jlong, Long) +DEFINE_PRIMITIVE_CALL(jfloat, Float) +DEFINE_PRIMITIVE_CALL(jdouble, Double) +#pragma pop_macro("DEFINE_PRIMITIVE_CALL") + +template +inline local_ref JMethod::operator()(alias_ref self, Args... args) { + const auto env = internal::getEnv(); + auto result = env->CallObjectMethod(self.get(), getId(), args...); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return adopt_local(static_cast(result)); +} + +template +inline void JStaticMethod::operator()(alias_ref cls, Args... args) { + const auto env = internal::getEnv(); + env->CallStaticVoidMethod(cls.get(), getId(), args...); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); +} + +#pragma push_macro("DEFINE_PRIMITIVE_STATIC_CALL") +#undef DEFINE_PRIMITIVE_STATIC_CALL +#define DEFINE_PRIMITIVE_STATIC_CALL(TYPE, METHOD) \ +template \ +inline TYPE JStaticMethod::operator()(alias_ref cls, Args... args) { \ + const auto env = internal::getEnv(); \ + auto result = env->CallStatic ## METHOD ## Method(cls.get(), getId(), args...); \ + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); \ + return result; \ +} + +DEFINE_PRIMITIVE_STATIC_CALL(jboolean, Boolean) +DEFINE_PRIMITIVE_STATIC_CALL(jbyte, Byte) +DEFINE_PRIMITIVE_STATIC_CALL(jchar, Char) +DEFINE_PRIMITIVE_STATIC_CALL(jshort, Short) +DEFINE_PRIMITIVE_STATIC_CALL(jint, Int) +DEFINE_PRIMITIVE_STATIC_CALL(jlong, Long) +DEFINE_PRIMITIVE_STATIC_CALL(jfloat, Float) +DEFINE_PRIMITIVE_STATIC_CALL(jdouble, Double) +#pragma pop_macro("DEFINE_PRIMITIVE_STATIC_CALL") + +template +inline local_ref JStaticMethod::operator()(alias_ref cls, Args... args) { + const auto env = internal::getEnv(); + auto result = env->CallStaticObjectMethod(cls.get(), getId(), args...); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return adopt_local(static_cast(result)); +} + + +template +inline void +JNonvirtualMethod::operator()(alias_ref self, jclass cls, Args... args) { + const auto env = internal::getEnv(); + env->CallNonvirtualVoidMethod(self.get(), cls, getId(), args...); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); +} + +#pragma push_macro("DEFINE_PRIMITIVE_NON_VIRTUAL_CALL") +#undef DEFINE_PRIMITIVE_NON_VIRTUAL_CALL +#define DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(TYPE, METHOD) \ +template \ +inline TYPE \ +JNonvirtualMethod::operator()(alias_ref self, jclass cls, Args... args) { \ + const auto env = internal::getEnv(); \ + auto result = env->CallNonvirtual ## METHOD ## Method(self.get(), cls, getId(), args...); \ + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); \ + return result; \ +} + +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jboolean, Boolean) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jbyte, Byte) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jchar, Char) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jshort, Short) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jint, Int) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jlong, Long) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jfloat, Float) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jdouble, Double) +#pragma pop_macro("DEFINE_PRIMITIVE_NON_VIRTUAL_CALL") + +template +inline local_ref JNonvirtualMethod::operator()( + alias_ref self, + jclass cls, + Args... args) { + const auto env = internal::getEnv(); + auto result = env->CallNonvirtualObjectMethod(self.get(), cls, getId(), args...); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return adopt_local(static_cast(result)); +} + + +// jtype_traits //////////////////////////////////////////////////////////////////////////////////// + +/// The generic way to associate a descriptor to a type is to look it up in the +/// corresponding @ref JObjectWrapper specialization. This makes it easy to add +/// support for your user defined type. +template +struct jtype_traits { + static std::string descriptor() { + if (JObjectWrapper::kJavaDescriptor != nullptr) { + return std::string{JObjectWrapper::kJavaDescriptor}; + }; + return JObjectWrapper::get_instantiated_java_descriptor(); + } + + static std::string array_descriptor() { + return '[' + std::string{JObjectWrapper::kJavaDescriptor}; + } +}; + +#pragma push_macro("DEFINE_FIELD_AND_ARRAY_TRAIT") +#undef DEFINE_FIELD_AND_ARRAY_TRAIT + +#define DEFINE_FIELD_AND_ARRAY_TRAIT(TYPE, DSC) \ +template<> \ +struct jtype_traits { \ + static std::string descriptor() { return std::string{#DSC}; } \ +}; \ +template<> \ +struct jtype_traits { \ + static std::string descriptor() { return std::string{"[" #DSC}; } \ +}; + +// There is no voidArray, handle that without the macro. +template<> +struct jtype_traits { + static std::string descriptor() { return std::string{"V"}; }; +}; + +DEFINE_FIELD_AND_ARRAY_TRAIT(jboolean, Z) +DEFINE_FIELD_AND_ARRAY_TRAIT(jbyte, B) +DEFINE_FIELD_AND_ARRAY_TRAIT(jchar, C) +DEFINE_FIELD_AND_ARRAY_TRAIT(jshort, S) +DEFINE_FIELD_AND_ARRAY_TRAIT(jint, I) +DEFINE_FIELD_AND_ARRAY_TRAIT(jlong, J) +DEFINE_FIELD_AND_ARRAY_TRAIT(jfloat, F) +DEFINE_FIELD_AND_ARRAY_TRAIT(jdouble, D) + +#pragma pop_macro("DEFINE_FIELD_AND_ARRAY_TRAIT") + + +// JField /////////////////////////////////////////////////////////////////////////////////////// + +template +inline JField::JField(jfieldID field) noexcept + : field_id_{field} +{} + +template +inline JField::operator bool() const noexcept { + return field_id_ != nullptr; +} + +template +inline jfieldID JField::getId() const noexcept { + return field_id_; +} + +#pragma push_macro("DEFINE_FIELD_PRIMITIVE_GET_SET") +#undef DEFINE_FIELD_PRIMITIVE_GET_SET +#define DEFINE_FIELD_PRIMITIVE_GET_SET(TYPE, METHOD) \ +template<> \ +inline TYPE JField::get(jobject object) const noexcept { \ + const auto env = internal::getEnv(); \ + return env->Get ## METHOD ## Field(object, field_id_); \ +} \ + \ +template<> \ +inline void JField::set(jobject object, TYPE value) noexcept { \ + const auto env = internal::getEnv(); \ + env->Set ## METHOD ## Field(object, field_id_, value); \ +} + +DEFINE_FIELD_PRIMITIVE_GET_SET(jboolean, Boolean) +DEFINE_FIELD_PRIMITIVE_GET_SET(jbyte, Byte) +DEFINE_FIELD_PRIMITIVE_GET_SET(jchar, Char) +DEFINE_FIELD_PRIMITIVE_GET_SET(jshort, Short) +DEFINE_FIELD_PRIMITIVE_GET_SET(jint, Int) +DEFINE_FIELD_PRIMITIVE_GET_SET(jlong, Long) +DEFINE_FIELD_PRIMITIVE_GET_SET(jfloat, Float) +DEFINE_FIELD_PRIMITIVE_GET_SET(jdouble, Double) +#pragma pop_macro("DEFINE_FIELD_PRIMITIVE_GET_SET") + +template +inline T JField::get(jobject object) const noexcept { + return static_cast(internal::getEnv()->GetObjectField(object, field_id_)); +} + +template +inline void JField::set(jobject object, T value) noexcept { + internal::getEnv()->SetObjectField(object, field_id_, static_cast(value)); +} + +// JStaticField ///////////////////////////////////////////////////////////////////////////////// + +template +inline JStaticField::JStaticField(jfieldID field) noexcept + : field_id_{field} +{} + +template +inline JStaticField::operator bool() const noexcept { + return field_id_ != nullptr; +} + +template +inline jfieldID JStaticField::getId() const noexcept { + return field_id_; +} + +#pragma push_macro("DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET") +#undef DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET +#define DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(TYPE, METHOD) \ +template<> \ +inline TYPE JStaticField::get(jclass jcls) const noexcept { \ + const auto env = internal::getEnv(); \ + return env->GetStatic ## METHOD ## Field(jcls, field_id_); \ +} \ + \ +template<> \ +inline void JStaticField::set(jclass jcls, TYPE value) noexcept { \ + const auto env = internal::getEnv(); \ + env->SetStatic ## METHOD ## Field(jcls, field_id_, value); \ +} + +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jboolean, Boolean) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jbyte, Byte) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jchar, Char) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jshort, Short) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jint, Int) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jlong, Long) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jfloat, Float) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jdouble, Double) +#pragma pop_macro("DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET") + +template +inline T JStaticField::get(jclass jcls) const noexcept { + const auto env = internal::getEnv(); + return static_cast(env->GetStaticObjectField(jcls, field_id_)); +} + +template +inline void JStaticField::set(jclass jcls, T value) noexcept { + internal::getEnv()->SetStaticObjectField(jcls, field_id_, value); +} + + +// jmethod_traits ////////////////////////////////////////////////////////////////////////////////// + +// TODO(T6608405) Adapt this to implement a register natives method that requires no descriptor +namespace internal { + +template +inline std::string JavaDescriptor() { + return jtype_traits::descriptor(); +} + +template +inline std::string JavaDescriptor() { + return JavaDescriptor() + JavaDescriptor(); +} + +template +inline std::string JMethodDescriptor() { + return "(" + JavaDescriptor() + ")" + JavaDescriptor(); +} + +template +inline std::string JMethodDescriptor() { + return "()" + JavaDescriptor(); +} + +} // internal + +template +inline std::string jmethod_traits::descriptor() { + return internal::JMethodDescriptor(); +} + +template +inline std::string jmethod_traits::constructor_descriptor() { + return internal::JMethodDescriptor(); +} + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta.h new file mode 100644 index 0000000000..de1bde0d54 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta.h @@ -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. + */ + +/** @file meta.h + * + * Provides wrappers for meta data such as methods and fields. + */ + +#pragma once + +#include +#include + +#include + +#include "References.h" + +namespace facebook { +namespace jni { + +/// Wrapper of a jmethodID. Provides a common base for JMethod specializations +class JMethodBase { + public: + /// Verify that the method is valid + explicit operator bool() const noexcept; + + /// Access the wrapped id + jmethodID getId() const noexcept; + + protected: + /// Create a wrapper of a method id + explicit JMethodBase(jmethodID method_id = nullptr) noexcept; + + private: + jmethodID method_id_; +}; + + +/// Representation of a jmethodID +template +class JMethod; + +/// @cond INTERNAL +#pragma push_macro("DEFINE_PRIMITIVE_METHOD_CLASS") + +#undef DEFINE_PRIMITIVE_METHOD_CLASS + +// Defining JMethod specializations based on return value +#define DEFINE_PRIMITIVE_METHOD_CLASS(TYPE) \ +template \ +class JMethod : public JMethodBase { \ + public: \ + static_assert(std::is_void::value || IsJniPrimitive(), \ + "TYPE must be primitive or void"); \ + \ + using JMethodBase::JMethodBase; \ + JMethod() noexcept {}; \ + JMethod(const JMethod& other) noexcept = default; \ + \ + TYPE operator()(alias_ref self, Args... args); \ + \ + friend class JObjectWrapper; \ +} + +DEFINE_PRIMITIVE_METHOD_CLASS(void); +DEFINE_PRIMITIVE_METHOD_CLASS(jboolean); +DEFINE_PRIMITIVE_METHOD_CLASS(jbyte); +DEFINE_PRIMITIVE_METHOD_CLASS(jchar); +DEFINE_PRIMITIVE_METHOD_CLASS(jshort); +DEFINE_PRIMITIVE_METHOD_CLASS(jint); +DEFINE_PRIMITIVE_METHOD_CLASS(jlong); +DEFINE_PRIMITIVE_METHOD_CLASS(jfloat); +DEFINE_PRIMITIVE_METHOD_CLASS(jdouble); + +#pragma pop_macro("DEFINE_PRIMITIVE_METHOD_CLASS") +/// @endcond + + +/// JMethod specialization for references that wraps the return value in a @ref local_ref +template +class JMethod : public JMethodBase { + public: + static_assert(IsPlainJniReference(), "T* must be a JNI reference"); + + using JMethodBase::JMethodBase; + JMethod() noexcept {}; + JMethod(const JMethod& other) noexcept = default; + + /// Invoke a method and return a local reference wrapping the result + local_ref operator()(alias_ref self, Args... args); + + friend class JObjectWrapper; +}; + + +/// Convenience type representing constructors +template +using JConstructor = JMethod; + +/// Representation of a jStaticMethodID +template +class JStaticMethod; + +/// @cond INTERNAL +#pragma push_macro("DEFINE_PRIMITIVE_STATIC_METHOD_CLASS") + +#undef DEFINE_PRIMITIVE_STATIC_METHOD_CLASS + +// Defining JStaticMethod specializations based on return value +#define DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(TYPE) \ +template \ +class JStaticMethod : public JMethodBase { \ + static_assert(std::is_void::value || IsJniPrimitive(), \ + "T must be a JNI primitive or void"); \ + \ + public: \ + using JMethodBase::JMethodBase; \ + JStaticMethod() noexcept {}; \ + JStaticMethod(const JStaticMethod& other) noexcept = default; \ + \ + TYPE operator()(alias_ref cls, Args... args); \ + \ + friend class JObjectWrapper; \ +} + +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(void); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jboolean); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jbyte); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jchar); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jshort); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jint); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jlong); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jfloat); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jdouble); + +#pragma pop_macro("DEFINE_PRIMITIVE_STATIC_METHOD_CLASS") +/// @endcond + + +/// JStaticMethod specialization for references that wraps the return value in a @ref local_ref +template +class JStaticMethod : public JMethodBase { + static_assert(IsPlainJniReference(), "T* must be a JNI reference"); + + public: + using JMethodBase::JMethodBase; + JStaticMethod() noexcept {}; + JStaticMethod(const JStaticMethod& other) noexcept = default; + + /// Invoke a method and return a local reference wrapping the result + local_ref operator()(alias_ref cls, Args... args); + + friend class JObjectWrapper; +}; + +/// Representation of a jNonvirtualMethodID +template +class JNonvirtualMethod; + +/// @cond INTERNAL +#pragma push_macro("DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS") + +#undef DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS + +// Defining JNonvirtualMethod specializations based on return value +#define DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(TYPE) \ +template \ +class JNonvirtualMethod : public JMethodBase { \ + static_assert(std::is_void::value || IsJniPrimitive(), \ + "T must be a JNI primitive or void"); \ + \ + public: \ + using JMethodBase::JMethodBase; \ + JNonvirtualMethod() noexcept {}; \ + JNonvirtualMethod(const JNonvirtualMethod& other) noexcept = default; \ + \ + TYPE operator()(alias_ref self, jclass cls, Args... args); \ + \ + friend class JObjectWrapper; \ +} + +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(void); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jboolean); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jbyte); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jchar); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jshort); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jint); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jlong); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jfloat); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jdouble); + +#pragma pop_macro("DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS") +/// @endcond + + +/// JNonvirtualMethod specialization for references that wraps the return value in a @ref local_ref +template +class JNonvirtualMethod : public JMethodBase { + static_assert(IsPlainJniReference(), "T* must be a JNI reference"); + + public: + using JMethodBase::JMethodBase; + JNonvirtualMethod() noexcept {}; + JNonvirtualMethod(const JNonvirtualMethod& other) noexcept = default; + + /// Invoke a method and return a local reference wrapping the result + local_ref operator()(alias_ref self, jclass cls, Args... args); + + friend class JObjectWrapper; +}; + + +/** + * JField represents typed fields and simplifies their access. Note that object types return + * raw pointers which generally should promptly get a wrap_local treatment. + */ +template +class JField { + static_assert(IsJniScalar(), "T must be a JNI scalar"); + + public: + /// Wraps an existing field id + explicit JField(jfieldID field = nullptr) noexcept; + + /// Verify that the id is valid + explicit operator bool() const noexcept; + + /// Access the wrapped id + jfieldID getId() const noexcept; + + private: + jfieldID field_id_; + + /// Get field value + /// @pre object != nullptr + T get(jobject object) const noexcept; + + /// Set field value + /// @pre object != nullptr + void set(jobject object, T value) noexcept; + + friend class JObjectWrapper; +}; + + +/** + * JStaticField represents typed fields and simplifies their access. Note that object types + * return raw pointers which generally should promptly get a wrap_local treatment. + */ +template +class JStaticField { + static_assert(IsJniScalar(), "T must be a JNI scalar"); + + public: + /// Wraps an existing field id + explicit JStaticField(jfieldID field = nullptr) noexcept; + + /// Verify that the id is valid + explicit operator bool() const noexcept; + + /// Access the wrapped id + jfieldID getId() const noexcept; + + private: + jfieldID field_id_; + + /// Get field value + /// @pre object != nullptr + T get(jclass jcls) const noexcept; + + /// Set field value + /// @pre object != nullptr + void set(jclass jcls, T value) noexcept; + + friend class JObjectWrapper; + +}; + + +/// Type traits for Java types (currently providing Java type descriptors) +template +struct jtype_traits; + + +/// Type traits for Java methods (currently providing Java type descriptors) +template +struct jmethod_traits; + +/// Template magic to provide @ref jmethod_traits +template +struct jmethod_traits { + static std::string descriptor(); + static std::string constructor_descriptor(); +}; + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators-inl.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators-inl.h new file mode 100644 index 0000000000..d60c900227 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators-inl.h @@ -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. + */ + +#pragma once + +#include +#include +#include + +#include "Exceptions.h" +#include "References.h" + +namespace facebook { +namespace jni { + +/// @cond INTERNAL +namespace internal { + +// Statistics mostly provided for test (only updated if FBJNI_DEBUG_REFS is defined) +struct ReferenceStats { + std::atomic_uint locals_deleted, globals_deleted, weaks_deleted; + + void reset() noexcept; +}; + +extern ReferenceStats g_reference_stats; +} +/// @endcond + + +// LocalReferenceAllocator ///////////////////////////////////////////////////////////////////////// + +inline jobject LocalReferenceAllocator::newReference(jobject original) const { + internal::dbglog("Local new: %p", original); + auto ref = internal::getEnv()->NewLocalRef(original); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return ref; +} + +inline void LocalReferenceAllocator::deleteReference(jobject reference) const noexcept { + internal::dbglog("Local release: %p", reference); + + if (reference) { + #ifdef FBJNI_DEBUG_REFS + ++internal::g_reference_stats.locals_deleted; + #endif + assert(verifyReference(reference)); + internal::getEnv()->DeleteLocalRef(reference); + } +} + +inline bool LocalReferenceAllocator::verifyReference(jobject reference) const noexcept { + if (!reference || !internal::doesGetObjectRefTypeWork()) { + return true; + } + return internal::getEnv()->GetObjectRefType(reference) == JNILocalRefType; +} + + +// GlobalReferenceAllocator //////////////////////////////////////////////////////////////////////// + +inline jobject GlobalReferenceAllocator::newReference(jobject original) const { + internal::dbglog("Global new: %p", original); + auto ref = internal::getEnv()->NewGlobalRef(original); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return ref; +} + +inline void GlobalReferenceAllocator::deleteReference(jobject reference) const noexcept { + internal::dbglog("Global release: %p", reference); + + if (reference) { + #ifdef FBJNI_DEBUG_REFS + ++internal::g_reference_stats.globals_deleted; + #endif + assert(verifyReference(reference)); + internal::getEnv()->DeleteGlobalRef(reference); + } +} + +inline bool GlobalReferenceAllocator::verifyReference(jobject reference) const noexcept { + if (!reference || !internal::doesGetObjectRefTypeWork()) { + return true; + } + return internal::getEnv()->GetObjectRefType(reference) == JNIGlobalRefType; +} + + +// WeakGlobalReferenceAllocator //////////////////////////////////////////////////////////////////// + +inline jobject WeakGlobalReferenceAllocator::newReference(jobject original) const { + internal::dbglog("Weak global new: %p", original); + auto ref = internal::getEnv()->NewWeakGlobalRef(original); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return ref; +} + +inline void WeakGlobalReferenceAllocator::deleteReference(jobject reference) const noexcept { + internal::dbglog("Weak Global release: %p", reference); + + if (reference) { + #ifdef FBJNI_DEBUG_REFS + ++internal::g_reference_stats.weaks_deleted; + #endif + assert(verifyReference(reference)); + internal::getEnv()->DeleteWeakGlobalRef(reference); + } +} + +inline bool WeakGlobalReferenceAllocator::verifyReference(jobject reference) const noexcept { + if (!reference || !internal::doesGetObjectRefTypeWork()) { + return true; + } + return internal::getEnv()->GetObjectRefType(reference) == JNIWeakGlobalRefType; +} + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators.h new file mode 100644 index 0000000000..ee328e071e --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators.h @@ -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. + */ + +/** + * @file ReferenceAllocators.h + * + * Reference allocators are used to create and delete various classes of JNI references (local, + * global, and weak global). + */ + +#pragma once + +#include "Common.h" + +namespace facebook { namespace jni { + +/// Allocator that handles local references +class LocalReferenceAllocator { + public: + jobject newReference(jobject original) const; + void deleteReference(jobject reference) const noexcept; + bool verifyReference(jobject reference) const noexcept; +}; + +/// Allocator that handles global references +class GlobalReferenceAllocator { + public: + jobject newReference(jobject original) const; + void deleteReference(jobject reference) const noexcept; + bool verifyReference(jobject reference) const noexcept; +}; + +/// Allocator that handles weak global references +class WeakGlobalReferenceAllocator { + public: + jobject newReference(jobject original) const; + void deleteReference(jobject reference) const noexcept; + bool verifyReference(jobject reference) const noexcept; +}; + +/// @cond INTERNAL +namespace internal { + +/** + * @return true iff env->GetObjectRefType is expected to work properly. + */ +bool doesGetObjectRefTypeWork(); + +} +/// @endcond + +}} + +#include "ReferenceAllocators-inl.h" diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/References-inl.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/References-inl.h new file mode 100644 index 0000000000..32f23493b5 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/References-inl.h @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 "CoreClasses.h" + +namespace facebook { +namespace jni { + +template +inline enable_if_t(), local_ref> adopt_local(T ref) noexcept { + return local_ref{ref}; +} + +template +inline enable_if_t(), global_ref> adopt_global(T ref) noexcept { + return global_ref{ref}; +} + +template +inline enable_if_t(), weak_ref> adopt_weak_global(T ref) noexcept { + return weak_ref{ref}; +} + + +template +inline enable_if_t(), alias_ref> wrap_alias(T ref) noexcept { + return alias_ref(ref); +} + + +template +enable_if_t(), alias_ref> wrap_alias(T ref) noexcept; + + +template +inline enable_if_t(), T> getPlainJniReference(T ref) { + return ref; +} + +template +inline T getPlainJniReference(alias_ref ref) { + return ref.get(); +} + +template +inline T getPlainJniReference(const base_owned_ref& ref) { + return ref.getPlainJniReference(); +} + + +namespace internal { + +template +enable_if_t(), plain_jni_reference_t> make_ref(const T& reference) { + auto old_reference = getPlainJniReference(reference); + if (!old_reference) { + return nullptr; + } + + auto ref = Alloc{}.newReference(old_reference); + if (!ref) { + // Note that we end up here if we pass a weak ref that refers to a collected object. + // Thus, it's hard to come up with a reason why this function should be used with + // weak references. + throw std::bad_alloc{}; + } + + return static_cast>(ref); +} + +} + +template +enable_if_t(), local_ref>> +make_local(const T& ref) { + return adopt_local(internal::make_ref(ref)); +} + +template +enable_if_t(), global_ref>> +make_global(const T& ref) { + return adopt_global(internal::make_ref(ref)); +} + +template +enable_if_t(), weak_ref>> +make_weak(const T& ref) { + return adopt_weak_global(internal::make_ref(ref)); +} + +template +inline enable_if_t() && IsNonWeakReference(), bool> +operator==(const T1& a, const T2& b) { + return isSameObject(getPlainJniReference(a), getPlainJniReference(b)); +} + +template +inline enable_if_t() && IsNonWeakReference(), bool> +operator!=(const T1& a, const T2& b) { + return !(a == b); +} + + +// base_owned_ref /////////////////////////////////////////////////////////////////////// + +template +inline constexpr base_owned_ref::base_owned_ref() noexcept + : object_{nullptr} +{} + +template +inline constexpr base_owned_ref::base_owned_ref( + std::nullptr_t t) noexcept + : object_{nullptr} +{} + +template +inline base_owned_ref::base_owned_ref( + const base_owned_ref& other) + : object_{Alloc{}.newReference(other.getPlainJniReference())} +{} + +template +inline facebook::jni::base_owned_ref::base_owned_ref( + T reference) noexcept + : object_{reference} { + assert(Alloc{}.verifyReference(reference)); + internal::dbglog("New wrapped ref=%p this=%p", getPlainJniReference(), this); +} + +template +inline base_owned_ref::base_owned_ref( + base_owned_ref&& other) noexcept + : object_{other.object_} { + internal::dbglog("New move from ref=%p other=%p", other.getPlainJniReference(), &other); + internal::dbglog("New move to ref=%p this=%p", getPlainJniReference(), this); + // JObjectWrapper is a simple type and does not support move semantics so we explicitly + // clear other + other.object_.set(nullptr); +} + +template +inline base_owned_ref::~base_owned_ref() noexcept { + reset(); + internal::dbglog("Ref destruct ref=%p this=%p", getPlainJniReference(), this); +} + +template +inline T base_owned_ref::release() noexcept { + auto value = getPlainJniReference(); + internal::dbglog("Ref release ref=%p this=%p", value, this); + object_.set(nullptr); + return value; +} + +template +inline void base_owned_ref::reset() noexcept { + reset(nullptr); +} + +template +inline void base_owned_ref::reset(T reference) noexcept { + if (getPlainJniReference()) { + assert(Alloc{}.verifyReference(reference)); + Alloc{}.deleteReference(getPlainJniReference()); + } + object_.set(reference); +} + +template +inline T base_owned_ref::getPlainJniReference() const noexcept { + return static_cast(object_.get()); +} + + +// weak_ref /////////////////////////////////////////////////////////////////////// + +template +inline weak_ref& weak_ref::operator=( + const weak_ref& other) { + auto otherCopy = other; + swap(*this, otherCopy); + return *this; +} + +template +inline weak_ref& weak_ref::operator=( + weak_ref&& other) noexcept { + internal::dbglog("Op= move ref=%p this=%p oref=%p other=%p", + getPlainJniReference(), this, other.getPlainJniReference(), &other); + reset(other.release()); + return *this; +} + +template +local_ref weak_ref::lockLocal() { + return adopt_local(static_cast(LocalReferenceAllocator{}.newReference(getPlainJniReference()))); +} + +template +global_ref weak_ref::lockGlobal() { + return adopt_global(static_cast(GlobalReferenceAllocator{}.newReference(getPlainJniReference()))); +} + +template +inline void swap( + weak_ref& a, + weak_ref& b) noexcept { + internal::dbglog("Ref swap a.ref=%p a=%p b.ref=%p b=%p", + a.getPlainJniReference(), &a, b.getPlainJniReference(), &b); + using std::swap; + swap(a.object_, b.object_); +} + + +// basic_strong_ref //////////////////////////////////////////////////////////////////////////// + +template +inline basic_strong_ref& basic_strong_ref::operator=( + const basic_strong_ref& other) { + auto otherCopy = other; + swap(*this, otherCopy); + return *this; +} + +template +inline basic_strong_ref& basic_strong_ref::operator=( + basic_strong_ref&& other) noexcept { + internal::dbglog("Op= move ref=%p this=%p oref=%p other=%p", + getPlainJniReference(), this, other.getPlainJniReference(), &other); + reset(other.release()); + return *this; +} + +template +inline alias_ref basic_strong_ref::releaseAlias() noexcept { + return wrap_alias(release()); +} + +template +inline basic_strong_ref::operator bool() const noexcept { + return get() != nullptr; +} + +template +inline T basic_strong_ref::get() const noexcept { + return getPlainJniReference(); +} + +template +inline JObjectWrapper* basic_strong_ref::operator->() noexcept { + return &object_; +} + +template +inline const JObjectWrapper* basic_strong_ref::operator->() const noexcept { + return &object_; +} + +template +inline JObjectWrapper& basic_strong_ref::operator*() noexcept { + return object_; +} + +template +inline const JObjectWrapper& basic_strong_ref::operator*() const noexcept { + return object_; +} + +template +inline void swap( + basic_strong_ref& a, + basic_strong_ref& b) noexcept { + internal::dbglog("Ref swap a.ref=%p a=%p b.ref=%p b=%p", + a.getPlainJniReference(), &a, b.getPlainJniReference(), &b); + using std::swap; + swap(a.object_, b.object_); +} + + +// alias_ref ////////////////////////////////////////////////////////////////////////////// + +template +inline constexpr alias_ref::alias_ref() noexcept + : object_{nullptr} +{} + +template +inline constexpr alias_ref::alias_ref(std::nullptr_t) noexcept + : object_{nullptr} +{} + +template +inline alias_ref::alias_ref(const alias_ref& other) noexcept + : object_{other.object_} +{} + + +template +inline alias_ref::alias_ref(T ref) noexcept + : object_{ref} { + assert( + LocalReferenceAllocator{}.verifyReference(ref) || + GlobalReferenceAllocator{}.verifyReference(ref)); +} + +template +template +inline alias_ref::alias_ref(alias_ref other) noexcept + : object_{other.get()} +{} + +template +template +inline alias_ref::alias_ref(const basic_strong_ref& other) noexcept + : object_{other.get()} +{} + +template +inline alias_ref& alias_ref::operator=(alias_ref other) noexcept { + swap(*this, other); + return *this; +} + +template +inline alias_ref::operator bool() const noexcept { + return get() != nullptr; +} + +template +inline T facebook::jni::alias_ref::get() const noexcept { + return static_cast(object_.get()); +} + +template +inline JObjectWrapper* alias_ref::operator->() noexcept { + return &object_; +} + +template +inline const JObjectWrapper* alias_ref::operator->() const noexcept { + return &object_; +} + +template +inline JObjectWrapper& alias_ref::operator*() noexcept { + return object_; +} + +template +inline const JObjectWrapper& alias_ref::operator*() const noexcept { + return object_; +} + +template +inline void swap(alias_ref& a, alias_ref& b) noexcept { + using std::swap; + swap(a.object_, b.object_); +} + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/References.cpp b/ReactAndroid/src/main/jni/first-party/jni/fbjni/References.cpp new file mode 100644 index 0000000000..0ee4c9e8f5 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/References.cpp @@ -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. + */ + +#include "References.h" + +namespace facebook { +namespace jni { + +JniLocalScope::JniLocalScope(JNIEnv* env, jint capacity) + : env_(env) { + hasFrame_ = false; + auto pushResult = env->PushLocalFrame(capacity); + FACEBOOK_JNI_THROW_EXCEPTION_IF(pushResult < 0); + hasFrame_ = true; +} + +JniLocalScope::~JniLocalScope() { + if (hasFrame_) { + env_->PopLocalFrame(nullptr); + } +} + +namespace internal { + +// Default implementation always returns true. +// Platform-specific sources can override this. +bool doesGetObjectRefTypeWork() __attribute__ ((weak)); +bool doesGetObjectRefTypeWork() { + return true; +} + +} + +} +} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/References.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/References.h new file mode 100644 index 0000000000..c7576b7d18 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/References.h @@ -0,0 +1,506 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + + +/** @file References.h + * + * Functionality similar to smart pointers, but for references into the VM. Four main reference + * types are provided: local_ref, global_ref, weak_ref, and alias_ref. All are generic + * templates that and refer to objects in the jobject hierarchy. The type of the referred objects + * are specified using the template parameter. All reference types except alias_ref own their + * underlying reference, just as a std smart pointer owns the underlying raw pointer. In the context + * of std smart pointers, these references behave like unique_ptr, and have basically the same + * interface. Thus, when the reference is destructed, the plain JNI reference, i.e. the underlying + * JNI reference (like the parameters passed directly to JNI functions), is released. The alias + * references provides no ownership and is a simple wrapper for plain JNI references. + * + * All but the weak references provides access to the underlying object using dereferencing, and a + * get() method. It is also possible to convert these references to booleans to test for nullity. + * To access the underlying object of a weak reference, the reference must either be released, or + * the weak reference can be used to create a local or global reference. + * + * An owning reference is created either by moving the reference from an existing owned reference, + * by copying an existing owned reference (which creates a new underlying reference), by using the + * default constructor which initialize the reference to nullptr, or by using a helper function. The + * helper function exist in two flavors: make_XXX or adopt_XXX. + * + * Adopting takes a plain JNI reference and wrap it in an owned reference. It takes ownership of the + * plain JNI reference so be sure that no one else owns the reference when you adopt it, and make + * sure that you know what kind of reference it is. + * + * New owned references can be created from existing plain JNI references, alias references, local + * references, and global references (i.e. non-weak references) using the make_local, make_global, + * and make_weak functions. + * + * Alias references can be implicitly initialized using global, local and plain JNI references using + * the wrap_alias function. Here, we don't assume ownership of the passed-in reference, but rather + * create a separate reference that we do own, leaving the passed-in reference to its fate. + * + * Similar rules apply for assignment. An owned reference can be copy or move assigned using a smart + * reference of the same type. In the case of copy assignment a new reference is created. Alias + * reference can also be assigned new values, but since they are simple wrappers of plain JNI + * references there is no move semantics involved. + * + * Alias references are special in that they do not own the object and can therefore safely be + * converted to and from its corresponding plain JNI reference. They are useful as parameters of + * functions that do not affect the lifetime of a reference. Usage can be compared with using plain + * JNI pointers as parameters where a function does not take ownership of the underlying object. + * + * The local, global, and alias references makes it possible to access methods in the underlying + * objects. A core set of classes are implemented in CoreClasses.h, and user defined wrappers are + * supported (see example below). The wrappers also supports inheritance so a wrapper can inherit + * from another wrapper to gain access to its functionality. As an example the jstring wrapper + * inherits from the jobject wrapper, so does the jclass wrapper. That means that you can for + * example call the toString() method using the jclass wrapper, or any other class that inherits + * from the jobject wrapper. + * + * Note that the wrappers are parameterized on the static type of your (jobject) pointer, thus if + * you have a jobject that refers to a Java String you will need to cast it to jstring to get the + * jstring wrapper. This also mean that if you make a down cast that is invalid there will be no one + * stopping you and the wrappers currently does not detect this which can cause crashes. Thus, cast + * wisely. + * + * @include WrapperSample.cpp + */ + +#pragma once + +#include +#include +#include + +#include + +#include "ReferenceAllocators.h" +#include "TypeTraits.h" + +namespace facebook { +namespace jni { + +/** + * The JObjectWrapper is specialized to provide functionality for various Java classes, some + * specializations are provided, and it is easy to add your own. See example + * @sample WrapperSample.cpp + */ +template +class JObjectWrapper; + + +template +class base_owned_ref; + +template +class basic_strong_ref; + +template +class weak_ref; + +template +class alias_ref; + + +/// A smart unique reference owning a local JNI reference +template +using local_ref = basic_strong_ref; + +/// A smart unique reference owning a global JNI reference +template +using global_ref = basic_strong_ref; + + +/// Convenience function to wrap an existing local reference +template +enable_if_t(), local_ref> adopt_local(T ref) noexcept; + +/// Convenience function to wrap an existing global reference +template +enable_if_t(), global_ref> adopt_global(T ref) noexcept; + +/// Convenience function to wrap an existing weak reference +template +enable_if_t(), weak_ref> adopt_weak_global(T ref) noexcept; + + +/** + * Create a new local reference from an existing reference + * + * @param ref a plain JNI, alias, or strong reference + * @return an owned local reference (referring to null if the input does) + * @throws std::bad_alloc if the JNI reference could not be created + */ +template +enable_if_t(), local_ref>> +make_local(const T& r); + +/** + * Create a new global reference from an existing reference + * + * @param ref a plain JNI, alias, or strong reference + * @return an owned global reference (referring to null if the input does) + * @throws std::bad_alloc if the JNI reference could not be created + */ +template +enable_if_t(), global_ref>> +make_global(const T& r); + +/** + * Create a new weak global reference from an existing reference + * + * @param ref a plain JNI, alias, or strong reference + * @return an owned weak global reference (referring to null if the input does) + * @throws std::bad_alloc if the returned reference is null + */ +template +enable_if_t(), weak_ref>> +make_weak(const T& r); + + +/// Swaps two owning references of the same type +template +void swap(weak_ref& a, weak_ref& b) noexcept; + +/// Swaps two owning references of the same type +template +void swap(basic_strong_ref& a, basic_strong_ref& b) noexcept; + +/** + * Retrieve the plain reference from a plain reference. + */ +template +enable_if_t(), T> getPlainJniReference(T ref); + +/** + * Retrieve the plain reference from an alias reference. + */ +template +T getPlainJniReference(alias_ref ref); + +/** + * Retrieve the plain JNI reference from any reference owned reference. + */ +template +T getPlainJniReference(const base_owned_ref& ref); + +/** + * Compare two references to see if they refer to the same object + */ +template +enable_if_t() && IsNonWeakReference(), bool> +operator==(const T1& a, const T2& b); + +/** + * Compare two references to see if they don't refer to the same object + */ +template +enable_if_t() && IsNonWeakReference(), bool> +operator!=(const T1& a, const T2& b); + + +template +class base_owned_ref { + + static_assert(IsPlainJniReference(), "T must be a JNI reference"); + + public: + + /** + * Release the ownership and set the reference to null. Thus no deleter is invoked. + * @return Returns the reference + */ + T release() noexcept; + + /** + * Reset the reference to refer to nullptr. + */ + void reset() noexcept; + + protected: + + JObjectWrapper object_; + + /* + * Wrap an existing reference and transfers its ownership to the newly created unique reference. + * NB! Does not create a new reference + */ + explicit base_owned_ref(T reference) noexcept; + + /// Create a null reference + constexpr base_owned_ref() noexcept; + + /// Create a null reference + constexpr explicit base_owned_ref(std::nullptr_t) noexcept; + + /// Copy constructor (note creates a new reference) + base_owned_ref(const base_owned_ref& other); + + /// Transfers ownership of an underlying reference from one unique reference to another + base_owned_ref(base_owned_ref&& other) noexcept; + + /// The delete the underlying reference if applicable + ~base_owned_ref() noexcept; + + + /// Assignment operator (note creates a new reference) + base_owned_ref& operator=(const base_owned_ref& other); + + /// Assignment by moving a reference thus not creating a new reference + base_owned_ref& operator=(base_owned_ref&& rhs) noexcept; + + + T getPlainJniReference() const noexcept; + + void reset(T reference) noexcept; + + + friend T jni::getPlainJniReference<>(const base_owned_ref& ref); +}; + + +/** + * A smart reference that owns its underlying JNI reference. The class provides basic + * functionality to handle a reference but gives no access to it unless the reference is + * released, thus no longer owned. The API is stolen with pride from unique_ptr and the + * semantics should be basically the same. This class should not be used directly, instead use + * @ref weak_ref + */ +template +class weak_ref : public base_owned_ref { + + static_assert(IsPlainJniReference(), "T must be a JNI reference"); + + public: + + using PlainJniType = T; + using Allocator = WeakGlobalReferenceAllocator; + + + /// Create a null reference + constexpr weak_ref() noexcept + : base_owned_ref{} {} + + /// Create a null reference + constexpr explicit weak_ref(std::nullptr_t) noexcept + : base_owned_ref{nullptr} {} + + /// Copy constructor (note creates a new reference) + weak_ref(const weak_ref& other) + : base_owned_ref{other} {} + + /// Transfers ownership of an underlying reference from one unique reference to another + weak_ref(weak_ref&& other) noexcept + : base_owned_ref{std::move(other)} {} + + + /// Assignment operator (note creates a new reference) + weak_ref& operator=(const weak_ref& other); + + /// Assignment by moving a reference thus not creating a new reference + weak_ref& operator=(weak_ref&& rhs) noexcept; + + + // Creates an owned local reference to the referred object or to null if the object is reclaimed + local_ref lockLocal(); + + // Creates an owned global reference to the referred object or to null if the object is reclaimed + global_ref lockGlobal(); + + private: + + using base_owned_ref::getPlainJniReference; + + /* + * Wrap an existing reference and transfers its ownership to the newly created unique reference. + * NB! Does not create a new reference + */ + explicit weak_ref(T reference) noexcept + : base_owned_ref{reference} {} + + + template friend class weak_ref; + friend weak_ref(), T>> + adopt_weak_global(T ref) noexcept; + friend void swap(weak_ref& a, weak_ref& b) noexcept; +}; + + +/** + * A class representing owned strong references to Java objects. This class + * should not be used directly, instead use @ref local_ref, or @ref global_ref. + */ +template +class basic_strong_ref : public base_owned_ref { + + static_assert(IsPlainJniReference(), "T must be a JNI reference"); + + public: + + using PlainJniType = T; + using Allocator = Alloc; + + using base_owned_ref::release; + using base_owned_ref::reset; + + + /// Create a null reference + constexpr basic_strong_ref() noexcept + : base_owned_ref{} {} + + /// Create a null reference + constexpr explicit basic_strong_ref(std::nullptr_t) noexcept + : base_owned_ref{nullptr} {} + + /// Copy constructor (note creates a new reference) + basic_strong_ref(const basic_strong_ref& other) + : base_owned_ref{other} {} + + /// Transfers ownership of an underlying reference from one unique reference to another + basic_strong_ref(basic_strong_ref&& other) noexcept + : base_owned_ref{std::move(other)} {} + + + /// Assignment operator (note creates a new reference) + basic_strong_ref& operator=(const basic_strong_ref& other); + + /// Assignment by moving a reference thus not creating a new reference + basic_strong_ref& operator=(basic_strong_ref&& rhs) noexcept; + + + /// Release the ownership of the reference and return the wrapped reference in an alias + alias_ref releaseAlias() noexcept; + + /// Checks if the reference points to a non-null object + explicit operator bool() const noexcept; + + /// Get the plain JNI reference + T get() const noexcept; + + /// Access the functionality provided by the object wrappers + JObjectWrapper* operator->() noexcept; + + /// Access the functionality provided by the object wrappers + const JObjectWrapper* operator->() const noexcept; + + /// Provide a reference to the underlying wrapper (be sure that it is non-null before invoking) + JObjectWrapper& operator*() noexcept; + + /// Provide a const reference to the underlying wrapper (be sure that it is non-null + /// before invoking) + const JObjectWrapper& operator*() const noexcept; + + private: + + using base_owned_ref::object_; + using base_owned_ref::getPlainJniReference; + + /* + * Wrap an existing reference and transfers its ownership to the newly created unique reference. + * NB! Does not create a new reference + */ + explicit basic_strong_ref(T reference) noexcept + : base_owned_ref{reference} {} + + + friend enable_if_t(), local_ref> adopt_local(T ref) noexcept; + friend enable_if_t(), global_ref> adopt_global(T ref) noexcept; + friend void swap(basic_strong_ref& a, basic_strong_ref& b) noexcept; +}; + + +template +enable_if_t(), alias_ref> wrap_alias(T ref) noexcept; + +/// Swaps to alias referencec of the same type +template +void swap(alias_ref& a, alias_ref& b) noexcept; + +/** + * A non-owning variant of the smart references (a dumb reference). These references still provide + * access to the functionality of the @ref JObjectWrapper specializations including exception + * handling and ease of use. Use this representation when you don't want to claim ownership of the + * underlying reference (compare to using raw pointers instead of smart pointers.) For symmetry use + * @ref alias_ref instead of this class. + */ +template +class alias_ref { + + static_assert(IsPlainJniReference(), "T must be a JNI reference"); + + public: + + using PlainJniType = T; + + + /// Create a null reference + constexpr alias_ref() noexcept; + + /// Create a null reference + constexpr alias_ref(std::nullptr_t) noexcept; + + /// Copy constructor + alias_ref(const alias_ref& other) noexcept; + + /// Wrap an existing plain JNI reference + alias_ref(T ref) noexcept; + + /// Wrap an existing smart reference of any type convertible to T + template(), T>> + alias_ref(alias_ref other) noexcept; + + /// Wrap an existing alias reference of a type convertible to T + template(), T>> + alias_ref(const basic_strong_ref& other) noexcept; + + + /// Assignment operator + alias_ref& operator=(alias_ref other) noexcept; + + /// Checks if the reference points to a non-null object + explicit operator bool() const noexcept; + + /// Converts back to a plain JNI reference + T get() const noexcept; + + /// Access the functionality provided by the object wrappers + JObjectWrapper* operator->() noexcept; + + /// Access the functionality provided by the object wrappers + const JObjectWrapper* operator->() const noexcept; + + /// Provide a guaranteed non-null reference (be sure that it is non-null before invoking) + JObjectWrapper& operator*() noexcept; + + /// Provide a guaranteed non-null reference (be sure that it is non-null before invoking) + const JObjectWrapper& operator*() const noexcept; + + private: + JObjectWrapper object_; + + friend void swap(alias_ref& a, alias_ref& b) noexcept; +}; + + +/** + * RAII object to create a local JNI frame, using PushLocalFrame/PopLocalFrame. + * + * This is useful when you have a call which is initiated from C++-land, and therefore + * doesn't automatically get a local JNI frame managed for you by the JNI framework. + */ +class JniLocalScope { +public: + JniLocalScope(JNIEnv* p_env, jint capacity); + ~JniLocalScope(); + +private: + JNIEnv* env_; + bool hasFrame_; +}; + +}} + +#include "References-inl.h" diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration-inl.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration-inl.h new file mode 100644 index 0000000000..d0c31579e9 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration-inl.h @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 "Exceptions.h" +#include "Hybrid.h" + +namespace facebook { +namespace jni { + +namespace detail { + +// convert to HybridClass* from jhybridobject +template +struct Convert< + T, typename std::enable_if< + std::is_base_of::type>::value>::type> { + typedef typename std::remove_pointer::type::jhybridobject jniType; + static T fromJni(jniType t) { + if (t == nullptr) { + return nullptr; + } + return facebook::jni::cthis(wrap_alias(t)); + } + // There is no automatic return conversion for objects. +}; + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(void (*)(JNIEnv*, C, Args... args)) { + struct funcWrapper { + static void call(JNIEnv* env, jobject obj, Args... args) { + // Note that if func was declared noexcept, then both gcc and clang are smart + // enough to elide the try/catch. + try { + (*func)(env, static_cast(obj), args...); + } catch (...) { + translatePendingCppExceptionToJavaException(); + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(void (*)(C, Args... args)) { + struct funcWrapper { + static void call(JNIEnv* env, jobject obj, Args... args) { + // Note that if func was declared noexcept, then both gcc and clang are smart + // enough to elide the try/catch. + try { + (void) env; + (*func)(static_cast(obj), args...); + } catch (...) { + translatePendingCppExceptionToJavaException(); + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(R (*)(JNIEnv*, C, Args... args)) { + struct funcWrapper { + static R call(JNIEnv* env, jobject obj, Args... args) { + try { + return (*func)(env, static_cast(obj), args...); + } catch (...) { + translatePendingCppExceptionToJavaException(); + return R{}; + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(void (*)(alias_ref, Args... args)) { + struct funcWrapper { + static void call(JNIEnv*, jobject obj, + typename Convert::type>::jniType... args) { + try { + (*func)(static_cast(obj), Convert::type>::fromJni(args)...); + } catch (...) { + translatePendingCppExceptionToJavaException(); + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(R (*)(alias_ref, Args... args)) { + struct funcWrapper { + typedef typename Convert::type>::jniType jniRet; + + static jniRet call(JNIEnv*, jobject obj, + typename Convert::type>::jniType... args) { + try { + return Convert::type>::toJniRet( + (*func)(static_cast(obj), Convert::type>::fromJni(args)...)); + } catch (...) { + translatePendingCppExceptionToJavaException(); + return jniRet{}; + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(void (C::*method0)(Args... args)) { + struct funcWrapper { + static void call(JNIEnv* env, jobject obj, + typename Convert::type>::jniType... args) { + try { + try { + auto aref = wrap_alias(static_cast(obj)); + // This is usually a noop, but if the hybrid object is a + // base class of other classes which register JNI methods, + // this will get the right type for the registered method. + auto cobj = static_cast(facebook::jni::cthis(aref)); + (cobj->*method)(Convert::type>::fromJni(args)...); + } catch (const std::exception& ex) { + C::mapException(ex); + throw; + } + } catch (...) { + translatePendingCppExceptionToJavaException(); + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(R (C::*method0)(Args... args)) { + struct funcWrapper { + typedef typename Convert::type>::jniType jniRet; + + static jniRet call(JNIEnv* env, jobject obj, + typename Convert::type>::jniType... args) { + try { + try { + auto aref = wrap_alias(static_cast(obj)); + // This is usually a noop, but if the hybrid object is a + // base class of other classes which register JNI methods, + // this will get the right type for the registered method. + auto cobj = static_cast(facebook::jni::cthis(aref)); + return Convert::type>::toJniRet( + (cobj->*method)(Convert::type>::fromJni(args)...)); + } catch (const std::exception& ex) { + C::mapException(ex); + throw; + } + } catch (...) { + translatePendingCppExceptionToJavaException(); + return jniRet{}; + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline std::string makeDescriptor(R (*)(JNIEnv*, C, Args... args)) { + return jmethod_traits::descriptor(); +} + +template +inline std::string makeDescriptor(R (*)(alias_ref, Args... args)) { + typedef typename Convert::type>::jniType jniRet; + return jmethod_traits::type>::jniType...)> + ::descriptor(); +} + +template +inline std::string makeDescriptor(R (C::*)(Args... args)) { + typedef typename Convert::type>::jniType jniRet; + return jmethod_traits::type>::jniType...)> + ::descriptor(); +} + +} + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration.h new file mode 100644 index 0000000000..e690f96ba9 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 "References.h" + +namespace facebook { +namespace jni { + +namespace detail { + +// This uses the real JNI function as a non-type template parameter to +// cause a (static member) function to exist with the same signature, +// but with try/catch exception translation. +template +NativeMethodWrapper* exceptionWrapJNIMethod(void (*func0)(JNIEnv*, jobject, Args... args)); + +// Same as above, but for non-void return types. +template +NativeMethodWrapper* exceptionWrapJNIMethod(R (*func0)(JNIEnv*, jobject, Args... args)); + +// Automatically wrap object argument, and don't take env explicitly. +template +NativeMethodWrapper* exceptionWrapJNIMethod(void (*func0)(alias_ref, Args... args)); + +// Automatically wrap object argument, and don't take env explicitly, +// non-void return type. +template +NativeMethodWrapper* exceptionWrapJNIMethod(R (*func0)(alias_ref, Args... args)); + +// Extract C++ instance from object, and invoke given method on it. +template +NativeMethodWrapper* exceptionWrapJNIMethod(void (C::*method0)(Args... args)); + +// Extract C++ instance from object, and invoke given method on it, +// non-void return type +template +NativeMethodWrapper* exceptionWrapJNIMethod(R (C::*method0)(Args... args)); + +// This uses deduction to figure out the descriptor name if the types +// are primitive or have JObjectWrapper specializations. +template +std::string makeDescriptor(R (*func)(JNIEnv*, C, Args... args)); + +// This uses deduction to figure out the descriptor name if the types +// are primitive or have JObjectWrapper specializations. +template +std::string makeDescriptor(R (*func)(alias_ref, Args... args)); + +// This uses deduction to figure out the descriptor name if the types +// are primitive or have JObjectWrapper specializations. +template +std::string makeDescriptor(R (C::*method0)(Args... args)); + +} + +// We have to use macros here, because the func needs to be used +// as both a decltype expression argument and as a non-type template +// parameter, since C++ provides no way for translateException +// to deduce the type of its non-type template parameter. +// The empty string in the macros below ensures that name +// is always a string literal (because that syntax is only +// valid when name is a string literal). +#define makeNativeMethod2(name, func) \ + { name "", ::facebook::jni::detail::makeDescriptor(&func), \ + ::facebook::jni::detail::exceptionWrapJNIMethod(&func) } + +#define makeNativeMethod3(name, desc, func) \ + { name "", desc, \ + ::facebook::jni::detail::exceptionWrapJNIMethod(&func) } + +// Variadic template hacks to get macros with different numbers of +// arguments. Usage instructions are in CoreClasses.h. +#define makeNativeMethodN(a, b, c, count, ...) makeNativeMethod ## count +#define makeNativeMethod(...) makeNativeMethodN(__VA_ARGS__, 3, 2)(__VA_ARGS__) + +}} + +#include "Registration-inl.h" diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/TypeTraits.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/TypeTraits.h new file mode 100644 index 0000000000..b4bdd15ea2 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/TypeTraits.h @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 + +namespace facebook { +namespace jni { + +/// Generic std::enable_if helper +template +using enable_if_t = typename std::enable_if::type; + +/// Generic std::is_convertible helper +template +constexpr bool IsConvertible() { + return std::is_convertible::value; +} + +template class TT, typename T> +struct is_instantiation_of : std::false_type {}; + +template class TT, typename... Ts> +struct is_instantiation_of> : std::true_type {}; + +template class TT, typename... Ts> +constexpr bool IsInstantiationOf() { + return is_instantiation_of::value; +} + +/// Metafunction to determine whether a type is a JNI reference or not +template +struct is_plain_jni_reference : + std::integral_constant::value && + std::is_base_of< + typename std::remove_pointer::type, + typename std::remove_pointer::type>::value> {}; + +/// Helper to simplify use of is_plain_jni_reference +template +constexpr bool IsPlainJniReference() { + return is_plain_jni_reference::value; +} + +/// Metafunction to determine whether a type is a primitive JNI type or not +template +struct is_jni_primitive : + std::integral_constant::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value> {}; + +/// Helper to simplify use of is_jni_primitive +template +constexpr bool IsJniPrimitive() { + return is_jni_primitive::value; +} + +/// Metafunction to determine if a type is a scalar (primitive or reference) JNI type +template +struct is_jni_scalar : + std::integral_constant::value || + is_jni_primitive::value> {}; + +/// Helper to simplify use of is_jni_scalar +template +constexpr bool IsJniScalar() { + return is_jni_scalar::value; +} + +// Metafunction to determine if a type is a JNI type +template +struct is_jni_type : + std::integral_constant::value || + std::is_void::value> {}; + +/// Helper to simplify use of is_jni_type +template +constexpr bool IsJniType() { + return is_jni_type::value; +} + +template +class weak_global_ref; + +template +class basic_strong_ref; + +template +class alias_ref; + +template +struct is_non_weak_reference : + std::integral_constant() || + IsInstantiationOf() || + IsInstantiationOf()> {}; + +template +constexpr bool IsNonWeakReference() { + return is_non_weak_reference::value; +} + +template +struct is_any_reference : + std::integral_constant() || + IsInstantiationOf() || + IsInstantiationOf() || + IsInstantiationOf()> {}; + +template +constexpr bool IsAnyReference() { + return is_any_reference::value; +} + +template +struct reference_traits { + static_assert(IsPlainJniReference(), "Need a plain JNI reference"); + using plain_jni_reference_t = T; +}; + +template