diff --git a/.gitignore b/.gitignore index 5326cd9cab3..60dbe841d0a 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 719cdca6630..680d5eba4cb 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.includeRequire.runModule.bundle"]; + 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 00000000000..5dfb57fd2fd --- /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 00000000000..2e12ff48800 --- /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 00000000000..3a8c184e627 --- /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 00000000000..a92fa177ee4 --- /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 00000000000..2123d078fdf --- /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 00000000000..6499cea1f9c --- /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 00000000000..a34f0dbb8fd Binary files /dev/null and b/Examples/Movies/android/app/src/main/res/drawable-hdpi/android_back_white.png differ 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 00000000000..861b40db0d4 Binary files /dev/null and b/Examples/Movies/android/app/src/main/res/drawable-hdpi/android_search_white.png differ diff --git a/Examples/Movies/android/app/src/main/res/drawable-mdpi/android_back_white.png b/Examples/Movies/android/app/src/main/res/drawable-mdpi/android_back_white.png new file mode 100644 index 00000000000..63111599a3b Binary files /dev/null and b/Examples/Movies/android/app/src/main/res/drawable-mdpi/android_back_white.png differ diff --git a/Examples/Movies/android/app/src/main/res/drawable-mdpi/android_search_white.png b/Examples/Movies/android/app/src/main/res/drawable-mdpi/android_search_white.png new file mode 100755 index 00000000000..fedd0c623f5 Binary files /dev/null and b/Examples/Movies/android/app/src/main/res/drawable-mdpi/android_search_white.png differ 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 00000000000..500892e105e Binary files /dev/null and b/Examples/Movies/android/app/src/main/res/drawable-xhdpi/android_back_white.png differ diff --git a/Examples/Movies/android/app/src/main/res/drawable-xhdpi/android_search_white.png b/Examples/Movies/android/app/src/main/res/drawable-xhdpi/android_search_white.png new file mode 100755 index 00000000000..5713a86f632 Binary files /dev/null and b/Examples/Movies/android/app/src/main/res/drawable-xhdpi/android_search_white.png differ diff --git a/Examples/Movies/android/app/src/main/res/drawable-xxhdpi/android_back_white.png b/Examples/Movies/android/app/src/main/res/drawable-xxhdpi/android_back_white.png new file mode 100644 index 00000000000..03620979ea7 Binary files /dev/null and b/Examples/Movies/android/app/src/main/res/drawable-xxhdpi/android_back_white.png differ 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 00000000000..d9f75bc794e Binary files /dev/null and b/Examples/Movies/android/app/src/main/res/drawable-xxhdpi/android_search_white.png differ 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 00000000000..395d7043130 Binary files /dev/null and b/Examples/Movies/android/app/src/main/res/drawable/rotten_tomatoes_icon.png differ diff --git a/Examples/Movies/android/app/src/main/res/layout/activity_main.xml b/Examples/Movies/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000000..d42c3b844e6 --- /dev/null +++ b/Examples/Movies/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + 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 00000000000..7c4632e0d0e --- /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 00000000000..319eb0ca100 --- /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 00000000000..aafb4a48a36 --- /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 00000000000..a92fa177ee4 --- /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 00000000000..bf3de9949b0 --- /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 00000000000..eb2e4f8b181 --- /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 00000000000..cde69bcccec Binary files /dev/null and b/Examples/SampleApp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/Examples/SampleApp/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Examples/SampleApp/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000000..c133a0cbd37 Binary files /dev/null and b/Examples/SampleApp/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ 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 00000000000..bfa42f0e7b9 Binary files /dev/null and b/Examples/SampleApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ 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 00000000000..324e72cdd74 Binary files /dev/null and b/Examples/SampleApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ 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 00000000000..18f47088131 --- /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 00000000000..319eb0ca100 --- /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 00000000000..47371fa5937 --- /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 00000000000..3df94c60317 --- /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 00000000000..040ed00fdfb --- /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 new file mode 100644 index 00000000000..af7d6863bce --- /dev/null +++ b/Examples/UIExplorer/ScrollViewSimpleExample.js @@ -0,0 +1,83 @@ +/** + * 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 { + ScrollView, + StyleSheet, + Text, + TouchableOpacity +} = React; + +var NUM_ITEMS = 20; + +var ScrollViewSimpleExample = React.createClass({ + statics: { + title: '', + description: 'Component that enables scrolling through child components.' + }, + + makeItems: function(nItems: number, styles): Array { + var items = []; + for (var i = 0; i < nItems; i++) { + items[i] = ( + + {'Item ' + i} + + ); + } + return items; + }, + + render: function() { + // One of the items is a horizontal scroll view + var items = this.makeItems(NUM_ITEMS, styles.itemWrapper); + items[4] = ( + + {this.makeItems(NUM_ITEMS, [styles.itemWrapper, styles.horizontalItemWrapper])} + + ); + + var verticalScrollView = ( + + {items} + + ); + + return verticalScrollView; + } +}); + +var styles = StyleSheet.create({ + verticalScrollView: { + margin: 10, + }, + itemWrapper: { + backgroundColor: '#dddddd', + alignItems: 'center', + borderRadius: 5, + borderWidth: 5, + borderColor: '#a52a2a', + padding: 30, + margin: 5, + }, + horizontalItemWrapper: { + padding: 50 + } +}); + +module.exports = ScrollViewSimpleExample; diff --git a/Examples/UIExplorer/SwitchAndroidExample.android.js b/Examples/UIExplorer/SwitchAndroidExample.android.js new file mode 100644 index 00000000000..ef58c5a3686 --- /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 00000000000..4159d0c18f0 --- /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 00000000000..9659e9180fd --- /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 00000000000..0e996476655 --- /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 00000000000..359ba087e87 --- /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 154796cef76..c9bd2418fe4 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 00000000000..d102d3de84a --- /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 00000000000..b0069794e74 --- /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 00000000000..e5fa61d8c78 --- /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 00000000000..a92fa177ee4 --- /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 00000000000..b69776f9a5d --- /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 00000000000..04c8e258c63 --- /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 00000000000..d3c4ccef2f5 Binary files /dev/null and b/Examples/UIExplorer/android/app/src/main/res/drawable/ic_create_black_48dp.png differ diff --git a/Examples/UIExplorer/android/app/src/main/res/drawable/ic_menu_black_24dp.png b/Examples/UIExplorer/android/app/src/main/res/drawable/ic_menu_black_24dp.png new file mode 100644 index 00000000000..ac882bca896 Binary files /dev/null and b/Examples/UIExplorer/android/app/src/main/res/drawable/ic_menu_black_24dp.png differ diff --git a/Examples/UIExplorer/android/app/src/main/res/drawable/ic_settings_black_48dp.png b/Examples/UIExplorer/android/app/src/main/res/drawable/ic_settings_black_48dp.png new file mode 100644 index 00000000000..bce161d005a Binary files /dev/null and b/Examples/UIExplorer/android/app/src/main/res/drawable/ic_settings_black_48dp.png differ 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 00000000000..a4aa3bd6d9a Binary files /dev/null and b/Examples/UIExplorer/android/app/src/main/res/drawable/launcher_icon.png differ 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 00000000000..b33726757ed Binary files /dev/null and b/Examples/UIExplorer/android/app/src/main/res/drawable/uie_comment_highlighted.png differ 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 00000000000..6491689fbbc Binary files /dev/null and b/Examples/UIExplorer/android/app/src/main/res/drawable/uie_comment_normal.png differ diff --git a/Examples/UIExplorer/android/app/src/main/res/drawable/uie_thumb_normal.png b/Examples/UIExplorer/android/app/src/main/res/drawable/uie_thumb_normal.png new file mode 100644 index 00000000000..72683dfac12 Binary files /dev/null and b/Examples/UIExplorer/android/app/src/main/res/drawable/uie_thumb_normal.png differ 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 00000000000..79eb69cf92b Binary files /dev/null and b/Examples/UIExplorer/android/app/src/main/res/drawable/uie_thumb_selected.png differ 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 00000000000..db6e0a8b812 --- /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 00000000000..0b8cb6bc3cb --- /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 00000000000..319eb0ca100 --- /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 b8c3e8def87..0f59cbea06c 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 00000000000..36ad97e51ff --- /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 00000000000..1faf93a05cc --- /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 00000000000..1510de4e8a3 --- /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 00000000000..12699e09c6a --- /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 00000000000..359e63c326b --- /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 00000000000..b3cf2758089 --- /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 00000000000..307a2bad41c --- /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 00000000000..714972f9883 --- /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 00000000000..d4ffd63f08a --- /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 00000000000..1d4d604952b --- /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 00000000000..ae06b3f039c --- /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 00000000000..961b59ac46f --- /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 00000000000..98b59c3c300 --- /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 00000000000..7b055780f0f --- /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 00000000000..8d21d81335e --- /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 00000000000..ba7a84ad96e --- /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 00000000000..d299c2eb384 --- /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 00000000000..8b2fa312ad0 --- /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 00000000000..d615b01cb46 --- /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 00000000000..2d061d4148d --- /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 00000000000..23116e61cd1 --- /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 8dc7386569b..69e10b2d4ac 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,30 @@ module.exports = MyCustomView; - `cd react-native && npm install` - `cd Examples` -Now open any example and hit run in Xcode. +Now open any example (the `.xcodeproj` file in each of the `Examples` subdirectories) and hit Run in Xcode. + +## Extending React Native + +- 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 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 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. + +## Contributing + +For more information about contributing, see our [Contribution Guidelines](https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md). + + +## License + +React is [BSD licensed](./LICENSE). We also provide an additional [patent grant](./PATENTS). + +React documentation is [Creative Commons licensed](./LICENSE-docs). Further documentation, tutorials, and more on the [React Native website](http://facebook.github.io/react-native/docs/getting-started.html). diff --git a/ReactAndroid/DevExperience.md b/ReactAndroid/DevExperience.md new file mode 100644 index 00000000000..2defb472032 --- /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 00000000000..ee19fdbde96 --- /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 00000000000..97c77d10d66 --- /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 00000000000..504653ca276 --- /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 00000000000..eaab472f378 Binary files /dev/null and b/ReactAndroid/libs/infer-annotations-1.5.jar differ diff --git a/ReactAndroid/release.gradle b/ReactAndroid/release.gradle new file mode 100644 index 00000000000..9b8cd35a8b0 --- /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 00000000000..900169cc7c5 --- /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 00000000000..7ca88e14636 --- /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 00000000000..f0441fc416b --- /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 00000000000..361a6f264ef --- /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 00000000000..4a6a492e2a2 --- /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 00000000000..bdfd6aa5afc --- /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 00000000000..5d594868da5 --- /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 00000000000..574c0bdf5f7 --- /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 00000000000..7952956089e --- /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 00000000000..96fba2f5d07 --- /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 00000000000..a25afaf6763 --- /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 00000000000..476d907c724 --- /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 00000000000..97ef4886f36 --- /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 00000000000..420a679ca3e --- /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 00000000000..a3646c84053 --- /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 00000000000..8c0190db556 --- /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 00000000000..65821d0075c --- /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 00000000000..bc13278338c --- /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 00000000000..2f3672ecdfe --- /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 00000000000..130f0c3e83b --- /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 00000000000..75892f48ca6 --- /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 00000000000..3006da53a95 --- /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 00000000000..18f754bf474 --- /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 00000000000..fcb4ca33623 --- /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 00000000000..b669e546b15 --- /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 00000000000..fa6e971f6d4 --- /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 00000000000..86a3f2c3fb6 --- /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 00000000000..11f4f32b987 --- /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 00000000000..b1ef5f7ce94 --- /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 00000000000..8f78569131d --- /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 00000000000..df524cf917b --- /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 00000000000..f8598e90884 --- /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 00000000000..3cd5cb7351b --- /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 00000000000..f11c2408cf0 --- /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 00000000000..9d6d1bfd1d5 --- /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 00000000000..0ef9cc9e6a9 --- /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 00000000000..e50bbfeaf40 --- /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 00000000000..37a6d2a1969 --- /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 00000000000..a3678ed91ff --- /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 00000000000..3cbdbf3327d --- /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 00000000000..74f0bedf47e --- /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 00000000000..da722505481 --- /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 00000000000..83dbe3d9aeb --- /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 00000000000..d1acf3da591 --- /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 00000000000..b90740c5172 --- /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 00000000000..214c84f6671 --- /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 00000000000..9eb5567557a --- /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 00000000000..3ca9429d019 --- /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 00000000000..25b02f2d0d9 --- /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 00000000000..15d498df121 --- /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 00000000000..fa574cc3e93 --- /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 00000000000..5e4760f7c74 --- /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 00000000000..ab72c46ba27 --- /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 00000000000..8b5153e5cc5 --- /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 00000000000..be3cd5d0aab --- /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 00000000000..917c1279c66 --- /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 00000000000..eea4c1d0723 --- /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 00000000000..38ad4a2f5cf --- /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 00000000000..faf123e8892 --- /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 00000000000..ee42c515312 --- /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 00000000000..d371cf5be62 --- /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 00000000000..8940ffc0860 --- /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 00000000000..2bc5e26c5c5 --- /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 00000000000..af23afcfe01 --- /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 00000000000..5e20f097053 --- /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 00000000000..fab0f231e33 --- /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 00000000000..bc75da27791 --- /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 00000000000..551ca5ac243 --- /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 00000000000..faecb9730c7 --- /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 00000000000..7efeb147663 --- /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 00000000000..1091ce0bad1 --- /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 00000000000..9b5ded014c8 --- /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 00000000000..02df6195974 --- /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 00000000000..708bdfd8d03 --- /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 00000000000..42a794ecae1 --- /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 00000000000..4d5630f9fda --- /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 00000000000..aa571141c05 --- /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 00000000000..9b374c145ea --- /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 00000000000..25db113ae0d --- /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 00000000000..08447802bf0 --- /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 00000000000..ccf9f799835 --- /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 00000000000..137ca098ec2 --- /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 00000000000..7e4376c5685 --- /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 00000000000..27363232b84 --- /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 00000000000..4d0470f46b8 --- /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 00000000000..0cc44f6e9ce --- /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 00000000000..47e5ed30cfe --- /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 00000000000..5aa5adb43bc --- /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 00000000000..3218611d388 --- /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 00000000000..2dd03c3f87f --- /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 00000000000..e2bfa848ef2 --- /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 00000000000..0c6e2a04414 --- /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 00000000000..f5d1f7a475d --- /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 00000000000..4fefe98a323 --- /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 00000000000..fee3ebbde7b --- /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 00000000000..4557d9fe674 --- /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 00000000000..6861669cb48 --- /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 00000000000..765fe39a584 --- /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 00000000000..e10a0b57d4e --- /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 00000000000..c630a59b51a --- /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 00000000000..d874f5aa2ac --- /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 00000000000..10be2a44df1 --- /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 00000000000..c5eb9ad6859 --- /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 00000000000..0090b82ad0b --- /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 00000000000..6350180fdb9 --- /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 00000000000..89673016cad --- /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 00000000000..23eb266d438 --- /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 00000000000..262f4aa5c47 --- /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 00000000000..74bced6cfa9 --- /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 00000000000..f73fbdae352 --- /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 00000000000..9023f4150e3 --- /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 00000000000..6aa627eaf02 --- /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 00000000000..0197980e7fa --- /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 00000000000..29c31b416c5 --- /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 00000000000..f9a71ab1832 --- /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 00000000000..a94a65cbc6e --- /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 00000000000..8e8524c731f --- /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 00000000000..00e075c509f --- /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 00000000000..59f80aba467 --- /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 00000000000..ca3558939d6 --- /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 00000000000..48deca9b156 --- /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 00000000000..46260652e7d --- /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 00000000000..b9a70a79d1c --- /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 00000000000..432b44ef01f --- /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 00000000000..89ae7d9bf4c --- /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 00000000000..dfefbc10c57 --- /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 00000000000..5489853b085 --- /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 00000000000..1a3973a3dd0 --- /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 00000000000..0e811e1a048 --- /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 00000000000..55c2810bb1c --- /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 00000000000..1329a5b7c6d --- /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 00000000000..87ca48b3928 --- /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 00000000000..67f5ca2312f --- /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 00000000000..ef2fcb29d01 --- /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 00000000000..326b6c58e15 --- /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 00000000000..6b914aae269 --- /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 00000000000..2ecad91c3c0 --- /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 00000000000..28144b9afaf --- /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 00000000000..1e63f525df7 --- /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 00000000000..07022a7e76c --- /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 00000000000..20f163fe364 --- /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 00000000000..b957173cbba --- /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 00000000000..fb700201302 --- /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 00000000000..7ce69c37e2d --- /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 00000000000..36340f0aa59 --- /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 00000000000..75f25617e50 --- /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 00000000000..601528a0fba --- /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 00000000000..facf52e153f --- /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 00000000000..d07eb4a5fa7 --- /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 00000000000..d401bfa1d0b --- /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 00000000000..65b7b38bcb5 --- /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 00000000000..6bc10f034a3 --- /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 00000000000..2e8ba61f229 --- /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 00000000000..299d2f4ae0c --- /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 00000000000..036badada73 --- /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 00000000000..1b7a37bdb48 --- /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 00000000000..dcf4457ebe8 --- /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 00000000000..810abd520d8 --- /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 00000000000..d8f00bfcc83 --- /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 00000000000..e61150ba96c --- /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 00000000000..80bdec21983 --- /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 00000000000..18135146eee --- /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 00000000000..7abbdae8921 --- /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 00000000000..d515ef10d07 --- /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 00000000000..3247709e619 --- /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 00000000000..01b22010275 --- /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 00000000000..94f6ccab64f --- /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 00000000000..d8c125a72a6 --- /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 00000000000..f553858bd37 --- /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 00000000000..bcb24667fc5 --- /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 00000000000..1d86fe7494c --- /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 00000000000..5b23ceda7fb --- /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 00000000000..13abb16e226 --- /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 00000000000..e219f18ccd0 --- /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 00000000000..2d99e79f569 --- /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 00000000000..e47e13e483c --- /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 00000000000..c4d5aa7b7a1 --- /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 00000000000..05a11ee9553 --- /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 00000000000..b9ee413b4a5 --- /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 00000000000..e12a76488a8 --- /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 00000000000..9ffdcd7a433 --- /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 00000000000..f338776e91e --- /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 00000000000..fbfd531c89c --- /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 00000000000..f3511bd2879 --- /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 00000000000..0902c8dd9a7 --- /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 00000000000..25b97a86397 --- /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 00000000000..3287ebf7854 --- /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 00000000000..61ca838c295 --- /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 00000000000..ef10ca1eaa5 --- /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 00000000000..60891ae2d18 --- /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 00000000000..6ceef2632d3 --- /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 00000000000..5ee3cc36ab2 --- /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 00000000000..eb0b3ee63e2 --- /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 00000000000..eaf442d232c --- /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 00000000000..2dffc8c26ca --- /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 00000000000..525a1d89261 --- /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 00000000000..859c74c99b4 --- /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 00000000000..1f4a5690b55 --- /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 00000000000..505fea79429 --- /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 00000000000..aa3315c169b --- /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 00000000000..6ef3011b2a8 --- /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 00000000000..4fa6f36770c --- /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 00000000000..62e52373fb0 --- /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 00000000000..e5783e3c5d5 --- /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 00000000000..36f32966114 --- /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 00000000000..56c6ff0ada2 --- /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 00000000000..9d0d32405f9 --- /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 00000000000..eb07905a7ee --- /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 00000000000..83bc9f50f83 --- /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 00000000000..916c301c965 --- /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 00000000000..b35bbc8d0fb --- /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 00000000000..dc6c9cd9c34 --- /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 00000000000..fd7a6f67f2b --- /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 00000000000..b7cabec7857 --- /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 00000000000..c8ba6c06143 --- /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 00000000000..b6d9f315ff2 --- /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 00000000000..296e9d40054 --- /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 00000000000..a9078c420a5 --- /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 00000000000..ec528cedf28 --- /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 00000000000..aa8c784ccf1 --- /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 00000000000..cc8098efcd1 --- /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 00000000000..840fde9ddec --- /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 00000000000..c0b72def623 --- /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 00000000000..185394151bc --- /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 00000000000..af49619366b --- /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 00000000000..79b39058bde --- /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 00000000000..4b9251dc620 --- /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 00000000000..9f62f5d0da1 --- /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 00000000000..eac5ed6db9b --- /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 00000000000..78aa65a3d1e --- /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 00000000000..1dec2e7265e --- /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 00000000000..9bdc7c03f61 --- /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 00000000000..dcd997b6c10 --- /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 00000000000..36d3923fec8 --- /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 00000000000..e78b15cefd9 --- /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 00000000000..b11af8c5725 --- /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 00000000000..863d996079b --- /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 00000000000..f7363441b92 --- /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 00000000000..2b77c314083 --- /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 00000000000..ff99d68f7f9 --- /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 00000000000..f2cbc8b9143 --- /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 00000000000..e593e851e87 --- /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 00000000000..d2f4251ebb8 --- /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 00000000000..6052b644d9d --- /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 00000000000..0b378dbb85c --- /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 00000000000..fc6c443c4c4 --- /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 00000000000..fb96b4478fb --- /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 00000000000..4e6bfa0ff50 --- /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 00000000000..a8efbd777d0 --- /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 00000000000..af73bda94c1 --- /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 00000000000..67a2e9d453a --- /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 00000000000..503d01a6cd2 --- /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 00000000000..4360c33fcfc --- /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 00000000000..2af6bb25426 --- /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 00000000000..a3decbb6efe --- /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 00000000000..eba1eeaf586 --- /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 00000000000..47cdb02320d --- /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 00000000000..a9ec0713dd1 --- /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 00000000000..a398ffe7898 --- /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 00000000000..95e2c27b295 --- /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 00000000000..35fc8599cd0 --- /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 00000000000..89f2ddbdf13 --- /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 00000000000..4f6fa44ce28 --- /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 00000000000..b6436cbcb08 --- /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 00000000000..36e8693d467 --- /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 00000000000..1520aa1c962 --- /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 00000000000..96a11f99481 --- /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 00000000000..0477ad71dc9 --- /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 00000000000..7277474d6ac --- /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 00000000000..cd5d15e48ee --- /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 00000000000..a070ed9a968 --- /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 00000000000..016013e15af --- /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 00000000000..91f28583e14 --- /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 00000000000..a7bcd49a581 --- /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 00000000000..4a832314c54 --- /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 00000000000..dd523375b46 --- /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 00000000000..3a255eb9400 --- /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 00000000000..d8f9dda846a --- /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 00000000000..3361c433d28 --- /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 00000000000..1e402a3fcb2 --- /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 00000000000..36f7737f643 --- /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 00000000000..d21fe697ea5 --- /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 00000000000..6d943972a61 --- /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 00000000000..d86a2f0deac --- /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 00000000000..db9a4315997 --- /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 00000000000..648ab2cdc41 --- /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 00000000000..4e08b558dff --- /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 00000000000..b58b7ac9452 --- /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 00000000000..7212cc4d089 --- /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 00000000000..37f006a498d --- /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 00000000000..0ed1c5fd6ab --- /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 00000000000..e77eaf1a0aa --- /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 00000000000..6ff7efe9068 --- /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 00000000000..69460d8a7de --- /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 00000000000..8b4df6a7c98 --- /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 00000000000..02b88ce73ae --- /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 00000000000..4dc6966a212 --- /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 00000000000..55f537ab57a --- /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 00000000000..b5d9c54a13b --- /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 00000000000..5fc8d0c8482 --- /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 00000000000..a85efa48ada --- /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 00000000000..a4c40409089 --- /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 00000000000..243a947889d --- /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 00000000000..d87ea33c073 --- /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 00000000000..2723155fe12 --- /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 00000000000..fb80b45539f --- /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 00000000000..7ea816b60e5 --- /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 00000000000..50111ef83e9 --- /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 00000000000..c52c3230654 --- /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 00000000000..33e83b88694 --- /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 00000000000..c5dfb3f144f --- /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 00000000000..9ba9367f67e --- /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 00000000000..ebcb778de9e --- /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 00000000000..cfc4b03e83e --- /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 00000000000..cc2d8383d07 --- /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 00000000000..de1bde0d542 --- /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 00000000000..d60c9002272 --- /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 00000000000..ee328e071e5 --- /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 00000000000..32f23493b58 --- /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 00000000000..0ee4c9e8f5d --- /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 00000000000..c7576b7d18a --- /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 00000000000..d0c31579e95 --- /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 00000000000..e690f96ba9a --- /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 00000000000..b4bdd15ea2f --- /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