diff --git a/.flowconfig b/.flowconfig index 75c8ac2ac..88093d65f 100644 --- a/.flowconfig +++ b/.flowconfig @@ -7,12 +7,19 @@ # Some modules have their own node_modules with overlap .*/node_modules/node-haste/.* -# Ignore react-tools where there are overlaps, but don't ignore anything that -# react-native relies on -.*/node_modules/react-tools/src/React.js -.*/node_modules/react-tools/src/renderers/shared/event/EventPropagators.js -.*/node_modules/react-tools/src/renderers/shared/event/eventPlugins/ResponderEventPlugin.js -.*/node_modules/react-tools/src/shared/vendor/core/ExecutionEnvironment.js +# Ignore react and fbjs where there are overlaps, but don't ignore +# anything that react-native relies on +.*/node_modules/fbjs-haste/.*/__tests__/.* +.*/node_modules/fbjs-haste/__forks__/Map.js +.*/node_modules/fbjs-haste/__forks__/Promise.js +.*/node_modules/fbjs-haste/__forks__/fetch.js +.*/node_modules/fbjs-haste/core/ExecutionEnvironment.js +.*/node_modules/fbjs-haste/core/isEmpty.js +.*/node_modules/fbjs-haste/crypto/crc32.js +.*/node_modules/fbjs-haste/stubs/ErrorUtils.js +.*/node_modules/react-haste/React.js +.*/node_modules/react-haste/renderers/dom/ReactDOM.js +.*/node_modules/react-haste/renderers/shared/event/eventPlugins/ResponderEventPlugin.js # Ignore commoner tests .*/node_modules/commoner/test/.* @@ -43,9 +50,9 @@ suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FixMe -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-7]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-7]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-8]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-8]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy [version] -0.17.0 +0.18.1 diff --git a/.travis.yml b/.travis.yml index 6efd4fbb2..ea84c8bc8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -82,3 +82,4 @@ env: branches: only: - master + - /^.*-stable$/ diff --git a/Examples/2048/2048/AppDelegate.m b/Examples/2048/2048/AppDelegate.m index b4b1769ff..9975d7df3 100644 --- a/Examples/2048/2048/AppDelegate.m +++ b/Examples/2048/2048/AppDelegate.m @@ -56,7 +56,7 @@ launchOptions:launchOptions]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [[UIViewController alloc] init]; + UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; diff --git a/Examples/Movies/Movies/AppDelegate.m b/Examples/Movies/Movies/AppDelegate.m index 6b8a069fd..d81599fb7 100644 --- a/Examples/Movies/Movies/AppDelegate.m +++ b/Examples/Movies/Movies/AppDelegate.m @@ -57,7 +57,7 @@ launchOptions:launchOptions]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [[UIViewController alloc] init]; + UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; 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 index 6499cea1f..3b2626601 100644 --- 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 @@ -69,7 +69,7 @@ public class MoviesActivity extends Activity implements DefaultHardwareBackBtnHa super.onResume(); if (mReactInstanceManager != null) { - mReactInstanceManager.onResume(this); + mReactInstanceManager.onResume(this, this); } } diff --git a/Examples/TicTacToe/TicTacToe/AppDelegate.m b/Examples/TicTacToe/TicTacToe/AppDelegate.m index f0199b6dd..4cca2217d 100644 --- a/Examples/TicTacToe/TicTacToe/AppDelegate.m +++ b/Examples/TicTacToe/TicTacToe/AppDelegate.m @@ -56,7 +56,7 @@ launchOptions:launchOptions]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [[UIViewController alloc] init]; + UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; diff --git a/Examples/UIExplorer/ActionSheetIOSExample.js b/Examples/UIExplorer/ActionSheetIOSExample.js index c8527657f..31aedca62 100644 --- a/Examples/UIExplorer/ActionSheetIOSExample.js +++ b/Examples/UIExplorer/ActionSheetIOSExample.js @@ -24,9 +24,9 @@ var { } = React; var BUTTONS = [ - 'Button Index: 0', - 'Button Index: 1', - 'Button Index: 2', + 'Option 0', + 'Option 1', + 'Option 2', 'Destruct', 'Cancel', ]; @@ -47,7 +47,7 @@ var ActionSheetExample = React.createClass({ Click to show the ActionSheet - Clicked button at index: "{this.state.clicked}" + Clicked button: {this.state.clicked} ); diff --git a/Examples/UIExplorer/AlertIOSExample.js b/Examples/UIExplorer/AlertIOSExample.js index 2d57fd5fe..e09c883c1 100644 --- a/Examples/UIExplorer/AlertIOSExample.js +++ b/Examples/UIExplorer/AlertIOSExample.js @@ -132,7 +132,7 @@ class PromptExample extends React.Component { + onPress={this.prompt.bind(this, this.title, null, null, this.promptResponse)}> @@ -143,7 +143,7 @@ class PromptExample extends React.Component { + onPress={this.prompt.bind(this, this.title, null, this.buttons, null)}> @@ -154,7 +154,7 @@ class PromptExample extends React.Component { + onPress={this.prompt.bind(this, this.title, this.defaultValue, null, this.promptResponse)}> @@ -165,7 +165,7 @@ class PromptExample extends React.Component { + onPress={this.prompt.bind(this, this.title, this.defaultValue, this.buttons, null)}> diff --git a/Examples/UIExplorer/BorderExample.js b/Examples/UIExplorer/BorderExample.js index 40c8f5992..ff3f73110 100644 --- a/Examples/UIExplorer/BorderExample.js +++ b/Examples/UIExplorer/BorderExample.js @@ -91,6 +91,13 @@ var styles = StyleSheet.create({ width: 100, height: 100 }, + border8: { + width: 60, + height: 60, + borderColor: 'black', + marginRight: 10, + backgroundColor: 'lightgrey', + }, }); exports.title = 'Border'; @@ -159,4 +166,18 @@ exports.examples = [ ); } }, + { + title: 'Single Borders', + description: 'top, left, bottom right', + render() { + return ( + + + + + + + ); + } + }, ]; diff --git a/Examples/UIExplorer/ModalExample.js b/Examples/UIExplorer/ModalExample.js index 3202cb622..eb925b5eb 100644 --- a/Examples/UIExplorer/ModalExample.js +++ b/Examples/UIExplorer/ModalExample.js @@ -143,6 +143,7 @@ var styles = StyleSheet.create({ }, innerContainer: { borderRadius: 10, + alignItems: 'center', }, row: { alignItems: 'center', @@ -158,6 +159,7 @@ var styles = StyleSheet.create({ borderRadius: 5, flex: 1, height: 44, + alignSelf: 'stretch', justifyContent: 'center', overflow: 'hidden', }, diff --git a/Examples/UIExplorer/ProgressBarAndroidExample.android.js b/Examples/UIExplorer/ProgressBarAndroidExample.android.js index 040ed00fd..b58cd73ca 100644 --- a/Examples/UIExplorer/ProgressBarAndroidExample.android.js +++ b/Examples/UIExplorer/ProgressBarAndroidExample.android.js @@ -20,6 +20,31 @@ var React = require('React'); var UIExplorerBlock = require('UIExplorerBlock'); var UIExplorerPage = require('UIExplorerPage'); +var TimerMixin = require('react-timer-mixin'); + +var MovingBar = React.createClass({ + mixins: [TimerMixin], + + getInitialState: function() { + return { + progress: 0 + }; + }, + + componentDidMount: function() { + this.setInterval( + () => { + var progress = (this.state.progress + 0.02) % 1; + this.setState({progress: progress}); + }, 50 + ); + }, + + render: function() { + return ; + }, +}); + var ProgressBarAndroidExample = React.createClass({ statics: { @@ -54,6 +79,26 @@ var ProgressBarAndroidExample = React.createClass({ + + + + + + + + + + + + + + + + + + + + ); }, diff --git a/Examples/UIExplorer/SliderIOSExample.js b/Examples/UIExplorer/SliderIOSExample.js index 2dfda6667..245fe10ad 100644 --- a/Examples/UIExplorer/SliderIOSExample.js +++ b/Examples/UIExplorer/SliderIOSExample.js @@ -37,7 +37,7 @@ var SliderExample = React.createClass({ {this.state.value} this.setState({value: value})} /> ); @@ -62,7 +62,26 @@ exports.displayName = 'SliderExample'; exports.description = 'Slider input for numeric values'; exports.examples = [ { - title: 'SliderIOS', - render(): ReactElement { return ; } + title: 'Default settings', + render(): ReactElement { + return ; + } + }, + { + title: 'minimumValue: -1, maximumValue: 2', + render(): ReactElement { + return ( + + ); + } + }, + { + title: 'step: 0.25', + render(): ReactElement { + return ; + } } ]; diff --git a/Examples/UIExplorer/TextExample.android.js b/Examples/UIExplorer/TextExample.android.js index 42d17ed07..0275863a8 100644 --- a/Examples/UIExplorer/TextExample.android.js +++ b/Examples/UIExplorer/TextExample.android.js @@ -160,6 +160,22 @@ var TextExample = React.createClass({ + + + + + NotoSerif Regular + + + NotoSerif Bold Italic + + + NotoSerif Italic (Missing Font file) + + + + + Size 23 diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index 0d6e20c10..57a2f032c 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -243,6 +243,21 @@ exports.examples = [ ) + + (opacity + + (is inherited + + (and accumulated + + (and also applies to the background) + + ) + + ) + + ) + Entity Name diff --git a/Examples/UIExplorer/TextInputExample.android.js b/Examples/UIExplorer/TextInputExample.android.js index b70f2d933..31156b50d 100644 --- a/Examples/UIExplorer/TextInputExample.android.js +++ b/Examples/UIExplorer/TextInputExample.android.js @@ -78,15 +78,79 @@ class RewriteExample extends React.Component { this.state = {text: ''}; } render() { + var limit = 20; + var remainder = limit - this.state.text.length; + var remainderColor = remainder > 5 ? 'blue' : 'red'; return ( - { - text = text.replace(/ /g, '_'); - this.setState({text}); - }} - style={styles.singleLine} - value={this.state.text} - /> + + { + text = text.replace(/ /g, '_'); + this.setState({text}); + }} + style={styles.default} + value={this.state.text} + /> + + {remainder} + + + ); + } +} + +class TokenizedTextExample extends React.Component { + constructor(props) { + super(props); + this.state = {text: 'Hello #World'}; + } + render() { + + //define delimiter + let delimiter = /\s+/; + + //split string + let _text = this.state.text; + let token, index, parts = []; + while (_text) { + delimiter.lastIndex = 0; + token = delimiter.exec(_text); + if (token === null) { + break; + } + index = token.index; + if (token[0].length === 0) { + index = 1; + } + parts.push(_text.substr(0, index)); + parts.push(token[0]); + index = index + token[0].length; + _text = _text.slice(index); + } + parts.push(_text); + + //highlight hashtags + parts = parts.map((text) => { + if (/^#/.test(text)) { + return {text}; + } else { + return text; + } + }); + + return ( + + { + this.setState({text}); + }}> + {parts} + + ); } } @@ -109,6 +173,10 @@ var styles = StyleSheet.create({ singleLineWithHeightTextInput: { height: 30, }, + hashtag: { + color: 'blue', + fontWeight: 'bold', + }, }); exports.title = ''; @@ -322,4 +390,10 @@ exports.examples = [ ); } }, + { + title: 'Attributed text', + render: function() { + return ; + } + }, ]; diff --git a/Examples/UIExplorer/TextInputExample.ios.js b/Examples/UIExplorer/TextInputExample.ios.js index d51a95e33..c7e196222 100644 --- a/Examples/UIExplorer/TextInputExample.ios.js +++ b/Examples/UIExplorer/TextInputExample.ios.js @@ -42,6 +42,7 @@ var TextEventsExample = React.createClass({ curText: '', prevText: '', prev2Text: '', + prev3Text: '', }; }, @@ -51,6 +52,7 @@ var TextEventsExample = React.createClass({ curText: text, prevText: state.curText, prev2Text: state.prevText, + prev3Text: state.prev2Text, }; }); }, @@ -73,12 +75,16 @@ var TextEventsExample = React.createClass({ onSubmitEditing={(event) => this.updateText( 'onSubmitEditing text: ' + event.nativeEvent.text )} + onKeyPress={(event) => { + this.updateText('onKeyPress key: ' + event.nativeEvent.key); + }} style={styles.default} /> {this.state.curText}{'\n'} (prev: {this.state.prevText}){'\n'} - (prev2: {this.state.prev2Text}) + (prev2: {this.state.prev2Text}){'\n'} + (prev3: {this.state.prev3Text}) ); @@ -114,6 +120,114 @@ class RewriteExample extends React.Component { } } +class TokenizedTextExample extends React.Component { + constructor(props) { + super(props); + this.state = {text: 'Hello #World'}; + } + render() { + + //define delimiter + let delimiter = /\s+/; + + //split string + let _text = this.state.text; + let token, index, parts = []; + while (_text) { + delimiter.lastIndex = 0; + token = delimiter.exec(_text); + if (token === null) { + break; + } + index = token.index; + if (token[0].length === 0) { + index = 1; + } + parts.push(_text.substr(0, index)); + parts.push(token[0]); + index = index + token[0].length; + _text = _text.slice(index); + } + parts.push(_text); + + //highlight hashtags + parts = parts.map((text) => { + if (/^#/.test(text)) { + return {text}; + } else { + return text; + } + }); + + return ( + + { + this.setState({text}); + }}> + {parts} + + + ); + } +} + +var BlurOnSubmitExample = React.createClass({ + focusNextField(nextField) { + this.refs[nextField].focus() + }, + + render: function() { + return ( + + this.focusNextField('2')} + /> + this.focusNextField('3')} + /> + this.focusNextField('4')} + /> + this.focusNextField('5')} + /> + + + ); + } +}); + var styles = StyleSheet.create({ page: { paddingBottom: 300, @@ -172,6 +286,10 @@ var styles = StyleSheet.create({ textAlign: 'right', width: 24, }, + hashtag: { + color: 'blue', + fontWeight: 'bold', + }, }); exports.displayName = (undefined: ?string); @@ -437,5 +555,15 @@ exports.examples = [ ); } - } + }, + { + title: 'Attributed text', + render: function() { + return ; + } + }, + { + title: 'Blur on submit', + render: function(): ReactElement { return ; }, + }, ]; diff --git a/Examples/UIExplorer/ToastAndroidExample.android.js b/Examples/UIExplorer/ToastAndroidExample.android.js index 61becd273..7f9cedf07 100644 --- a/Examples/UIExplorer/ToastAndroidExample.android.js +++ b/Examples/UIExplorer/ToastAndroidExample.android.js @@ -31,7 +31,7 @@ var ToastExample = React.createClass({ statics: { title: 'Toast Example', - description: 'Example that demostrates the use of an Android Toast to provide feedback.', + description: 'Example that demonstrates the use of an Android Toast to provide feedback.', }, getInitialState: function() { diff --git a/Examples/UIExplorer/ToolbarAndroidExample.android.js b/Examples/UIExplorer/ToolbarAndroidExample.android.js index c621296d9..769737a33 100644 --- a/Examples/UIExplorer/ToolbarAndroidExample.android.js +++ b/Examples/UIExplorer/ToolbarAndroidExample.android.js @@ -101,6 +101,12 @@ var ToolbarAndroidExample = React.createClass({ title="Bunny and Hawk" style={styles.toolbar} /> + + + ); }, diff --git a/Examples/UIExplorer/TransparentHitTestExample.js b/Examples/UIExplorer/TransparentHitTestExample.js new file mode 100644 index 000000000..755582da3 --- /dev/null +++ b/Examples/UIExplorer/TransparentHitTestExample.js @@ -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. + * + * @flow + */ + +'use strict'; + +var React = require('react-native'); +var { + Text, + View, + TouchableOpacity, +} = React; + +var TransparentHitTestExample = React.createClass({ + render: function() { + return ( + + alert('Hi!')}> + HELLO! + + + + + ); + }, +}); + +exports.title = ''; +exports.displayName = 'TransparentHitTestExample'; +exports.description = 'Transparent view receiving touch events'; +exports.examples = [ + { + title: 'TransparentHitTestExample', + render(): ReactElement { return ; } + } +]; diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 4c0bf2b19..40e9ef5f1 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -51,6 +51,7 @@ 14D6D7291B2222EF001FB087 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14AADF041AC3DB95002390C9 /* libReact.a */; }; 14DC67F41AB71881001358AB /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14DC67F11AB71876001358AB /* libRCTPushNotification.a */; }; 3578590A1B28D2CF00341EDB /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 357859011B28D2C500341EDB /* libRCTLinking.a */; }; + 3D36915B1BDA8CBB007B22D8 /* uie_thumb_big.png in Resources */ = {isa = PBXBuildFile; fileRef = 3D36915A1BDA8CBB007B22D8 /* uie_thumb_big.png */; }; 3DB99D0C1BA0340600302749 /* UIExplorerIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DB99D0B1BA0340600302749 /* UIExplorerIntegrationTests.m */; }; 834C36EC1AF8DED70019C93C /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 834C36D21AF8DA610019C93C /* libRCTSettings.a */; }; 83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */; }; @@ -222,6 +223,7 @@ 14DC67E71AB71876001358AB /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = ../../Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj; sourceTree = ""; }; 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../../Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; 357858F81B28D2C400341EDB /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = ../../Libraries/LinkingIOS/RCTLinking.xcodeproj; sourceTree = ""; }; + 3D36915A1BDA8CBB007B22D8 /* uie_thumb_big.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = uie_thumb_big.png; path = UIExplorer/Images.xcassets/uie_thumb_big.imageset/uie_thumb_big.png; sourceTree = SOURCE_ROOT; }; 3DB99D0B1BA0340600302749 /* UIExplorerIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIExplorerIntegrationTests.m; sourceTree = ""; }; 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTTest.xcodeproj; path = ../../Libraries/RCTTest/RCTTest.xcodeproj; sourceTree = ""; }; 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIManagerScenarioTests.m; sourceTree = ""; }; @@ -436,6 +438,7 @@ 143BC5971B21E3E100462512 /* Supporting Files */ = { isa = PBXGroup; children = ( + 3D36915A1BDA8CBB007B22D8 /* uie_thumb_big.png */, 143BC5981B21E3E100462512 /* Info.plist */, ); name = "Supporting Files"; @@ -817,6 +820,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3D36915B1BDA8CBB007B22D8 /* uie_thumb_big.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -966,6 +970,7 @@ OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = com.facebook.internal.uiexplorer.local; PRODUCT_NAME = UIExplorer; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -985,6 +990,7 @@ OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = com.facebook.internal.uiexplorer.local; PRODUCT_NAME = UIExplorer; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -1076,6 +1082,7 @@ "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; @@ -1103,6 +1110,7 @@ WARNING_CFLAGS = ( "-Wextra", "-Wall", + "-Wincompatible-pointer-types", ); }; name = Debug; @@ -1133,6 +1141,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; @@ -1160,6 +1169,7 @@ WARNING_CFLAGS = ( "-Wextra", "-Wall", + "-Wincompatible-pointer-types", ); }; name = Release; diff --git a/Examples/UIExplorer/UIExplorerApp.ios.js b/Examples/UIExplorer/UIExplorerApp.ios.js index c2f4734e8..8daecbd42 100644 --- a/Examples/UIExplorer/UIExplorerApp.ios.js +++ b/Examples/UIExplorer/UIExplorerApp.ios.js @@ -73,5 +73,6 @@ var styles = StyleSheet.create({ }); AppRegistry.registerComponent('UIExplorerApp', () => UIExplorerApp); +UIExplorerList.registerComponents(); module.exports = UIExplorerApp; diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testLayoutExample_1@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testLayoutExample_1@2x.png index ff9eaeae1..5786896ee 100644 Binary files a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testLayoutExample_1@2x.png and b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testLayoutExample_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testSliderExample_1@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testSliderExample_1@2x.png index 239761ed2..c38d68b52 100644 Binary files a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testSliderExample_1@2x.png and b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testSliderExample_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testSwitchExample_1@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testSwitchExample_1@2x.png index efdc8fe05..bfd587267 100644 Binary files a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testSwitchExample_1@2x.png and b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testSwitchExample_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testTabBarExample_1@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testTabBarExample_1@2x.png index 90874b135..cfde34a4a 100644 Binary files a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testTabBarExample_1@2x.png and b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testTabBarExample_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testTextExample_1@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testTextExample_1@2x.png index 4055e789b..43718fb49 100644 Binary files a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testTextExample_1@2x.png and b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testTextExample_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png index cc17b0dd2..8ec70eb0c 100644 Binary files a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png and b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m index 1bbd981ce..fc9afeaf1 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m @@ -36,7 +36,7 @@ #endif NSOperatingSystemVersion version = [NSProcessInfo processInfo].operatingSystemVersion; - RCTAssert(version.majorVersion == 8 || version.minorVersion >= 3, @"Tests should be run on iOS 8.3+, found %zd.%zd.%zd", version.majorVersion, version.minorVersion, version.patchVersion); + RCTAssert((version.majorVersion == 8 && version.minorVersion >= 3) || version.majorVersion >= 9, @"Tests should be run on iOS 8.3+, found %zd.%zd.%zd", version.majorVersion, version.minorVersion, version.patchVersion); _runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp", nil); } @@ -63,7 +63,7 @@ RCT_TEST(IntegrationTestHarnessTest) RCT_TEST(TimersTest) RCT_TEST(AsyncStorageTest) RCT_TEST(AppEventsTest) -RCT_TEST(ImageSnapshotTest) +//RCT_TEST(ImageSnapshotTest) // Disabled: #8985988 RCT_TEST(SimpleSnapshotTest) // Disable due to flakiness: #8686784 diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerSnapshotTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerSnapshotTests.m index 1281a4796..2f3639cdc 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerSnapshotTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerSnapshotTests.m @@ -37,7 +37,7 @@ #endif NSOperatingSystemVersion version = [NSProcessInfo processInfo].operatingSystemVersion; - RCTAssert(version.majorVersion == 8 || version.minorVersion >= 3, @"Snapshot tests should be run on iOS 8.3+, found %zd.%zd.%zd", version.majorVersion, version.minorVersion, version.patchVersion); + RCTAssert((version.majorVersion == 8 && version.minorVersion >= 3) || version.majorVersion >= 9, @"Tests should be run on iOS 8.3+, found %zd.%zd.%zd", version.majorVersion, version.minorVersion, version.patchVersion); _runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerApp.ios", nil); _runner.recordMode = NO; } @@ -53,7 +53,7 @@ RCT_TEST(LayoutExample) RCT_TEST(TextExample) RCT_TEST(SwitchExample) RCT_TEST(SliderExample) -RCT_TEST(TabBarExample) +//RCT_TEST(TabBarExample) // Disabled: #8985988 - (void)testZZZNotInRecordMode { diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index 7dab3bf07..c8f07d971 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -50,6 +50,7 @@ var COMPONENTS = [ require('./TextExample.ios'), require('./TextInputExample.ios'), require('./TouchableExample'), + require('./TransparentHitTestExample'), require('./ViewExample'), require('./WebViewExample'), ]; @@ -78,23 +79,6 @@ var APIS = [ require('./ImageEditingExample'), ]; -// Register suitable examples for snapshot tests -COMPONENTS.concat(APIS).forEach((Example) => { - if (Example.displayName) { - var Snapshotter = React.createClass({ - render: function() { - var Renderable = UIExplorerListBase.makeRenderable(Example); - return ( - - - - ); - }, - }); - AppRegistry.registerComponent(Example.displayName, () => Snapshotter); - } -}); - type Props = { navigator: { navigationContext: NavigationContext, @@ -143,6 +127,25 @@ class UIExplorerList extends React.Component { onPressRow(example: any) { this._openExample(example); } + + // Register suitable examples for snapshot tests + static registerComponents() { + COMPONENTS.concat(APIS).forEach((Example) => { + if (Example.displayName) { + var Snapshotter = React.createClass({ + render: function() { + var Renderable = UIExplorerListBase.makeRenderable(Example); + return ( + + + + ); + }, + }); + AppRegistry.registerComponent(Example.displayName, () => Snapshotter); + } + }); + } } var styles = StyleSheet.create({ diff --git a/Examples/UIExplorer/UIExplorerUnitTests/LayoutSubviewsOrderingTest.m b/Examples/UIExplorer/UIExplorerUnitTests/LayoutSubviewsOrderingTest.m index c00eedcfd..fcd796114 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/LayoutSubviewsOrderingTest.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/LayoutSubviewsOrderingTest.m @@ -20,10 +20,10 @@ - (void)testLayoutSubviewsOrdering { // create some Views and ViewControllers - UIViewController *parentVC = [[UIViewController alloc] init]; - UIView *parentView = [[UIView alloc] init]; - UIViewController *childVC = [[UIViewController alloc] init]; - UIView *childView = [[UIView alloc] init]; + UIViewController *parentVC = [UIViewController new]; + UIView *parentView = [UIView new]; + UIViewController *childVC = [UIViewController new]; + UIView *childView = [UIView new]; // The ordering we expect is: // parentView::layoutSubviews diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m index a3c4c4354..7802f4ad1 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m @@ -136,19 +136,25 @@ _Pragma("clang diagnostic pop") NSString *injectedStuff; RUN_RUNLOOP_WHILE(!(injectedStuff = executor.injectedStuff[@"__fbBatchedBridgeConfig"])); - NSDictionary *moduleConfig = RCTJSONParse(injectedStuff, NULL); - NSDictionary *remoteModuleConfig = moduleConfig[@"remoteModuleConfig"]; - NSDictionary *testModuleConfig = remoteModuleConfig[@"TestModule"]; - NSDictionary *constants = testModuleConfig[@"constants"]; - NSDictionary *methods = testModuleConfig[@"methods"]; + __block NSNumber *testModuleID = nil; + __block NSDictionary *testConstants = nil; + __block NSNumber *testMethodID = nil; + + NSArray *remoteModuleConfig = RCTJSONParse(injectedStuff, NULL)[@"remoteModuleConfig"]; + [remoteModuleConfig enumerateObjectsUsingBlock:^(id moduleConfig, NSUInteger i, BOOL *stop) { + if ([moduleConfig isKindOfClass:[NSArray class]] && [moduleConfig[0] isEqualToString:@"TestModule"]) { + testModuleID = @(i); + testConstants = moduleConfig[1]; + testMethodID = @([moduleConfig[2] indexOfObject:@"testMethod"]); + *stop = YES; + } + }]; - XCTAssertNotNil(moduleConfig); XCTAssertNotNil(remoteModuleConfig); - XCTAssertNotNil(testModuleConfig); - XCTAssertNotNil(constants); - XCTAssertEqualObjects(constants[@"eleventyMillion"], @42); - XCTAssertNotNil(methods); - XCTAssertNotNil(methods[@"testMethod"]); + XCTAssertNotNil(testModuleID); + XCTAssertNotNil(testConstants); + XCTAssertEqualObjects(testConstants[@"eleventyMillion"], @42); + XCTAssertNotNil(testMethodID); } - (void)testCallNativeMethod @@ -158,13 +164,19 @@ _Pragma("clang diagnostic pop") NSString *injectedStuff; RUN_RUNLOOP_WHILE(!(injectedStuff = executor.injectedStuff[@"__fbBatchedBridgeConfig"])); - NSDictionary *moduleConfig = RCTJSONParse(injectedStuff, NULL); - NSDictionary *remoteModuleConfig = moduleConfig[@"remoteModuleConfig"]; - NSDictionary *testModuleConfig = remoteModuleConfig[@"TestModule"]; - NSNumber *testModuleID = testModuleConfig[@"moduleID"]; - NSDictionary *methods = testModuleConfig[@"methods"]; - NSDictionary *testMethod = methods[@"testMethod"]; - NSNumber *testMethodID = testMethod[@"methodID"]; + __block NSNumber *testModuleID = nil; + __block NSDictionary *testConstants = nil; + __block NSNumber *testMethodID = nil; + + NSArray *remoteModuleConfig = RCTJSONParse(injectedStuff, NULL)[@"remoteModuleConfig"]; + [remoteModuleConfig enumerateObjectsUsingBlock:^(id moduleConfig, NSUInteger i, __unused BOOL *stop) { + if ([moduleConfig isKindOfClass:[NSArray class]] && [moduleConfig[0] isEqualToString:@"TestModule"]) { + testModuleID = @(i); + testConstants = moduleConfig[1]; + testMethodID = @([moduleConfig[2] indexOfObject:@"testMethod"]); + *stop = YES; + } + }]; NSArray *args = @[@1234, @5678, @"stringy", @{@"a": @1}, @42]; NSArray *buffer = @[@[testModuleID], @[testMethodID], @[args], @[], @1234567]; diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_NSURLTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_NSURLTests.m index 0de322a88..a42d587b0 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_NSURLTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_NSURLTests.m @@ -36,7 +36,7 @@ } \ #define TEST_BUNDLE_PATH(name, _input, _expectedPath) \ -TEST_PATH(name, _input, [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:_expectedPath]) +TEST_PATH(name, _input, [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:_expectedPath]) // Basic tests TEST_URL(basic, @"http://example.com", @"http://example.com") diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m index b11bffa26..a51319bb3 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m @@ -64,6 +64,8 @@ static BOOL RCTLogsError(void (^block)(void)) - (void)doFooWithInteger:(__unused NSInteger)n { } - (void)doFooWithCGRect:(CGRect)s { _s = s; } +- (void)doFoo : (__unused NSString *)foo { } + - (void)testNumbersNonnull { { @@ -121,4 +123,22 @@ static BOOL RCTLogsError(void (^block)(void)) XCTAssertTrue(CGRectEqualToRect(r, _s)); } +- (void)testWhitespaceTolerance +{ + NSString *methodName = @"doFoo : \t (NSString *)foo"; + + __block RCTModuleMethod *method; + XCTAssertFalse(RCTLogsError(^{ + method = [[RCTModuleMethod alloc] initWithObjCMethodName:methodName + JSMethodName:nil + moduleClass:[self class]]; + })); + + XCTAssertEqualObjects(method.JSMethodName, @"doFoo"); + + XCTAssertFalse(RCTLogsError(^{ + [method invokeWithBridge:nil module:self arguments:@[@"bar"]]; + })); +} + @end diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m index 9bdda8a8d..b94622c8c 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m @@ -83,7 +83,8 @@ [parentView insertReactSubview:mainView atIndex:1]; [parentView insertReactSubview:footerView atIndex:2]; - [parentView collectRootUpdatedFrames:nil parentConstraint:CGSizeZero]; + parentView.reactTag = @1; // must be valid rootView tag + [parentView collectRootUpdatedFrames:nil]; XCTAssertTrue(CGRectEqualToRect([parentView measureLayoutRelativeToAncestor:parentView], CGRectMake(0, 0, 440, 440))); XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets([parentView paddingAsInsets], UIEdgeInsetsMake(10, 10, 10, 10))); diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js index 41f6b4d1a..f8183b12b 100644 --- a/Examples/UIExplorer/WebViewExample.js +++ b/Examples/UIExplorer/WebViewExample.js @@ -96,6 +96,7 @@ var WebViewExample = React.createClass({ url={this.state.url} javaScriptEnabledAndroid={true} onNavigationStateChange={this.onNavigationStateChange} + onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest} startInLoadingState={true} scalesPageToFit={this.state.scalesPageToFit} /> @@ -118,6 +119,11 @@ var WebViewExample = React.createClass({ this.refs[WEBVIEW_REF].reload(); }, + onShouldStartLoadWithRequest: function(event) { + // Implement any custom loading logic here, don't forget to return! + return true; + }, + onNavigationStateChange: function(navState) { this.setState({ backButtonEnabled: navState.canGoBack, diff --git a/Examples/UIExplorer/XHRExample.android.js b/Examples/UIExplorer/XHRExample.android.js index 92344e72d..3696a2bfb 100644 --- a/Examples/UIExplorer/XHRExample.android.js +++ b/Examples/UIExplorer/XHRExample.android.js @@ -25,6 +25,8 @@ var { View, } = React; +var XHRExampleHeaders = require('./XHRExampleHeaders'); + // 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. @@ -259,10 +261,9 @@ class FormUploader extends React.Component { } } - exports.framework = 'React'; exports.title = 'XMLHttpRequest'; -exports.description = 'Example that demostrates upload and download requests ' + +exports.description = 'Example that demonstrates upload and download requests ' + 'using XMLHttpRequest.'; exports.examples = [{ title: 'File Download', @@ -274,6 +275,11 @@ exports.examples = [{ render() { return ; } +}, { + title: 'Headers', + render() { + return ; + } }]; var styles = StyleSheet.create({ diff --git a/Examples/UIExplorer/XHRExample.ios.js b/Examples/UIExplorer/XHRExample.ios.js index 57f7fc31e..cc83ccf58 100644 --- a/Examples/UIExplorer/XHRExample.ios.js +++ b/Examples/UIExplorer/XHRExample.ios.js @@ -30,6 +30,8 @@ var { View, } = React; +var XHRExampleHeaders = require('./XHRExampleHeaders'); + class Downloader extends React.Component { xhr: XMLHttpRequest; @@ -368,6 +370,11 @@ exports.examples = [{ render() { return ; } +}, { + title: 'Headers', + render() { + return ; + } }]; var styles = StyleSheet.create({ diff --git a/Examples/UIExplorer/XHRExampleHeaders.js b/Examples/UIExplorer/XHRExampleHeaders.js new file mode 100644 index 000000000..e9f5e2c4c --- /dev/null +++ b/Examples/UIExplorer/XHRExampleHeaders.js @@ -0,0 +1,116 @@ +/** + * 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, + TouchableHighlight, + View, +} = React; + +class XHRExampleHeaders extends React.Component { + + xhr: XMLHttpRequest; + cancelled: boolean; + + constructor(props) { + super(props); + this.cancelled = false; + this.state = { + status: '', + headers: '', + contentSize: 1, + downloaded: 0, + }; + } + + download() { + this.xhr && this.xhr.abort(); + + var xhr = this.xhr || new XMLHttpRequest(); + xhr.onreadystatechange = () => { + if (xhr.readyState === xhr.DONE) { + if (this.cancelled) { + this.cancelled = false; + return; + } + if (xhr.status === 200) { + this.setState({ + status: 'Download complete!', + headers: xhr.getAllResponseHeaders() + }); + } 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', 'https://httpbin.org/response-headers?header1=value&header2=value1&header2=value2'); + 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...' ? ( + + + ... + + + ) : ( + + + Get headers + + + ); + + return ( + + {button} + {this.state.headers} + + ); + } +} + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + backgroundColor: '#eeeeee', + padding: 8, + }, +}); + +module.exports = XHRExampleHeaders; \ No newline at end of file diff --git a/Examples/UIExplorer/android/app/src/main/assets/fonts/notoserif.ttf b/Examples/UIExplorer/android/app/src/main/assets/fonts/notoserif.ttf new file mode 100755 index 000000000..a1c6f1059 Binary files /dev/null and b/Examples/UIExplorer/android/app/src/main/assets/fonts/notoserif.ttf differ diff --git a/Examples/UIExplorer/android/app/src/main/assets/fonts/notoserif_bold_italic.ttf b/Examples/UIExplorer/android/app/src/main/assets/fonts/notoserif_bold_italic.ttf new file mode 100755 index 000000000..32d38afee Binary files /dev/null and b/Examples/UIExplorer/android/app/src/main/assets/fonts/notoserif_bold_italic.ttf differ diff --git a/Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java b/Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java index 04c8e258c..c1dd73f6d 100644 --- a/Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java +++ b/Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java @@ -69,7 +69,7 @@ public class UIExplorerActivity extends Activity implements DefaultHardwareBackB super.onResume(); if (mReactInstanceManager != null) { - mReactInstanceManager.onResume(this); + mReactInstanceManager.onResume(this, this); } } diff --git a/Libraries/ART/Brushes/ARTLinearGradient.m b/Libraries/ART/Brushes/ARTLinearGradient.m index 8793ff07b..e34fd59fd 100644 --- a/Libraries/ART/Brushes/ARTLinearGradient.m +++ b/Libraries/ART/Brushes/ARTLinearGradient.m @@ -19,7 +19,7 @@ CGPoint _endPoint; } -- (instancetype)initWithArray:(NSArray *)array +- (instancetype)initWithArray:(NSArray *)array { if ((self = [super initWithArray:array])) { if (array.count < 5) { diff --git a/Libraries/ART/Brushes/ARTPattern.m b/Libraries/ART/Brushes/ARTPattern.m index 07dd86700..a0e6d5a5d 100644 --- a/Libraries/ART/Brushes/ARTPattern.m +++ b/Libraries/ART/Brushes/ARTPattern.m @@ -18,7 +18,7 @@ CGRect _rect; } -- (instancetype)initWithArray:(NSArray *)array +- (instancetype)initWithArray:(NSArray *)array { if ((self = [super initWithArray:array])) { if (array.count < 6) { diff --git a/Libraries/ART/Brushes/ARTRadialGradient.m b/Libraries/ART/Brushes/ARTRadialGradient.m index b59b17369..cb3ae65a2 100644 --- a/Libraries/ART/Brushes/ARTRadialGradient.m +++ b/Libraries/ART/Brushes/ARTRadialGradient.m @@ -21,7 +21,7 @@ CGFloat _radiusRatio; } -- (instancetype)initWithArray:(NSArray *)array +- (instancetype)initWithArray:(NSArray *)array { if ((self = [super initWithArray:array])) { if (array.count < 7) { diff --git a/Libraries/ART/Brushes/ARTSolidColor.m b/Libraries/ART/Brushes/ARTSolidColor.m index 229942dde..bfeff00bd 100644 --- a/Libraries/ART/Brushes/ARTSolidColor.m +++ b/Libraries/ART/Brushes/ARTSolidColor.m @@ -17,7 +17,7 @@ CGColorRef _color; } -- (instancetype)initWithArray:(NSArray *)array +- (instancetype)initWithArray:(NSArray *)array { if ((self = [super initWithArray:array])) { _color = CGColorRetain([RCTConvert CGColor:array offset:1]); diff --git a/Libraries/ART/ReactNativeART.js b/Libraries/ART/ReactNativeART.js index 3b5801d00..9bf9bc0b6 100644 --- a/Libraries/ART/ReactNativeART.js +++ b/Libraries/ART/ReactNativeART.js @@ -566,8 +566,14 @@ function Pattern(url, width, height, left, top) { this._brush = [PATTERN, url, +left || 0, +top || 0, +width, +height]; } -var ReactART = { +// This doesn't work on iOS and is just a placeholder to get Spectrum running. +// I will try to eliminate this dependency in Spectrum and remove it from +// ReactART proper. +function CSSBackgroundPattern() { + return new Color('rgba(0,0,0,0)'); +} +var ReactART = { LinearGradient: LinearGradient, RadialGradient: RadialGradient, Pattern: Pattern, @@ -578,7 +584,7 @@ var ReactART = { ClippingRectangle: ClippingRectangle, Shape: Shape, Text: Text, - + CSSBackgroundPattern: CSSBackgroundPattern }; module.exports = ReactART; diff --git a/Libraries/ART/ViewManagers/ARTGroupManager.m b/Libraries/ART/ViewManagers/ARTGroupManager.m index 15f55d4df..b958d11f9 100644 --- a/Libraries/ART/ViewManagers/ARTGroupManager.m +++ b/Libraries/ART/ViewManagers/ARTGroupManager.m @@ -17,7 +17,7 @@ RCT_EXPORT_MODULE() - (ARTNode *)node { - return [[ARTGroup alloc] init]; + return [ARTGroup new]; } @end diff --git a/Libraries/ART/ViewManagers/ARTNodeManager.m b/Libraries/ART/ViewManagers/ARTNodeManager.m index c2f0dba35..3c697c129 100644 --- a/Libraries/ART/ViewManagers/ARTNodeManager.m +++ b/Libraries/ART/ViewManagers/ARTNodeManager.m @@ -17,7 +17,7 @@ RCT_EXPORT_MODULE() - (ARTNode *)node { - return [[ARTNode alloc] init]; + return [ARTNode new]; } - (UIView *)view diff --git a/Libraries/ART/ViewManagers/ARTRenderableManager.m b/Libraries/ART/ViewManagers/ARTRenderableManager.m index 01b579dca..aaed2e31d 100644 --- a/Libraries/ART/ViewManagers/ARTRenderableManager.m +++ b/Libraries/ART/ViewManagers/ARTRenderableManager.m @@ -17,7 +17,7 @@ RCT_EXPORT_MODULE() - (ARTRenderable *)node { - return [[ARTRenderable alloc] init]; + return [ARTRenderable new]; } RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat) diff --git a/Libraries/ART/ViewManagers/ARTShapeManager.m b/Libraries/ART/ViewManagers/ARTShapeManager.m index 426237fa7..3997586d1 100644 --- a/Libraries/ART/ViewManagers/ARTShapeManager.m +++ b/Libraries/ART/ViewManagers/ARTShapeManager.m @@ -18,7 +18,7 @@ RCT_EXPORT_MODULE() - (ARTRenderable *)node { - return [[ARTShape alloc] init]; + return [ARTShape new]; } RCT_EXPORT_VIEW_PROPERTY(d, CGPath) diff --git a/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m index ddfba6697..10772b72c 100644 --- a/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m +++ b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m @@ -17,7 +17,7 @@ RCT_EXPORT_MODULE() - (UIView *)view { - return [[ARTSurfaceView alloc] init]; + return [ARTSurfaceView new]; } @end diff --git a/Libraries/ART/ViewManagers/ARTTextManager.m b/Libraries/ART/ViewManagers/ARTTextManager.m index 473d0cf4f..430f26db5 100644 --- a/Libraries/ART/ViewManagers/ARTTextManager.m +++ b/Libraries/ART/ViewManagers/ARTTextManager.m @@ -18,7 +18,7 @@ RCT_EXPORT_MODULE() - (ARTRenderable *)node { - return [[ARTText alloc] init]; + return [ARTText new]; } RCT_EXPORT_VIEW_PROPERTY(alignment, CTTextAlignment) diff --git a/Libraries/ActionSheetIOS/ActionSheetIOS.js b/Libraries/ActionSheetIOS/ActionSheetIOS.js index 20150d6f6..155b7d74e 100644 --- a/Libraries/ActionSheetIOS/ActionSheetIOS.js +++ b/Libraries/ActionSheetIOS/ActionSheetIOS.js @@ -27,7 +27,6 @@ var ActionSheetIOS = { ); RCTActionSheetManager.showActionSheetWithOptions( options, - () => {}, // RKActionSheet compatibility hack callback ); }, diff --git a/Libraries/ActionSheetIOS/RCTActionSheetManager.m b/Libraries/ActionSheetIOS/RCTActionSheetManager.m index dbaa9affd..626da966c 100644 --- a/Libraries/ActionSheetIOS/RCTActionSheetManager.m +++ b/Libraries/ActionSheetIOS/RCTActionSheetManager.m @@ -21,65 +21,137 @@ @implementation RCTActionSheetManager { - NSMutableDictionary *_callbacks; + // Use NSMapTable, as UIAlertViews do not implement + // which is required for NSDictionary keys + NSMapTable *_callbacks; } RCT_EXPORT_MODULE() -- (instancetype)init -{ - if ((self = [super init])) { - _callbacks = [NSMutableDictionary new]; - } - return self; -} - - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } +/* + * The `anchor` option takes a view to set as the anchor for the share + * popup to point to, on iPads running iOS 8. If it is not passed, it + * defaults to centering the share popup on screen without any arrows. + */ +- (CGRect)sourceRectInView:(UIView *)sourceView + anchorViewTag:(NSNumber *)anchorViewTag +{ + if (anchorViewTag) { + UIView *anchorView = [self.bridge.uiManager viewForReactTag:anchorViewTag]; + return [anchorView convertRect:anchorView.bounds toView:sourceView]; + } else { + return (CGRect){sourceView.center, {1, 1}}; + } +} + RCT_EXPORT_METHOD(showActionSheetWithOptions:(NSDictionary *)options - failureCallback:(__unused RCTResponseSenderBlock)failureCallback + callback:(RCTResponseSenderBlock)callback) +{ + if (RCTRunningInAppExtension()) { + RCTLogError(@"Unable to show action sheet from app extension"); + return; + } + + if (!_callbacks) { + _callbacks = [NSMapTable strongToStrongObjectsMapTable]; + } + + NSString *title = [RCTConvert NSString:options[@"title"]]; + NSArray *buttons = [RCTConvert NSStringArray:options[@"options"]]; + NSInteger destructiveButtonIndex = options[@"destructiveButtonIndex"] ? [RCTConvert NSInteger:options[@"destructiveButtonIndex"]] : -1; + NSInteger cancelButtonIndex = options[@"cancelButtonIndex"] ? [RCTConvert NSInteger:options[@"cancelButtonIndex"]] : -1; + + UIViewController *controller = RCTKeyWindow().rootViewController; + if (controller == nil) { + RCTLogError(@"Tried to display action sheet but there is no application window. options: %@", options); + return; + } + + /* + * The `anchor` option takes a view to set as the anchor for the share + * popup to point to, on iPads running iOS 8. If it is not passed, it + * defaults to centering the share popup on screen without any arrows. + */ + NSNumber *anchorViewTag = [RCTConvert NSNumber:options[@"anchor"]]; + UIView *sourceView = controller.view; + CGRect sourceRect = [self sourceRectInView:sourceView anchorViewTag:anchorViewTag]; + +#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 + + if ([UIAlertController class] == nil) { + + UIActionSheet *actionSheet = [UIActionSheet new]; + + actionSheet.title = title; + for (NSString *option in buttons) { + [actionSheet addButtonWithTitle:option]; + } + actionSheet.destructiveButtonIndex = destructiveButtonIndex; + actionSheet.cancelButtonIndex = cancelButtonIndex; + actionSheet.delegate = self; + + [_callbacks setObject:callback forKey:actionSheet]; + + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + [actionSheet showFromRect:sourceRect inView:sourceView animated:YES]; + } else { + [actionSheet showInView:sourceView]; + } + + } else + +#endif + + { + UIAlertController *alertController = + [UIAlertController alertControllerWithTitle:title + message:nil + preferredStyle:UIAlertControllerStyleActionSheet]; + + NSInteger index = 0; + for (NSString *option in buttons) { + UIAlertActionStyle style = UIAlertActionStyleDefault; + if (index == destructiveButtonIndex) { + style = UIAlertActionStyleDestructive; + } else if (index == cancelButtonIndex) { + style = UIAlertActionStyleCancel; + } + + NSInteger localIndex = index; + [alertController addAction:[UIAlertAction actionWithTitle:option + style:style + handler:^(__unused UIAlertAction *action){ + callback(@[@(localIndex)]); + }]]; + + index++; + } + + alertController.modalPresentationStyle = UIModalPresentationPopover; + alertController.popoverPresentationController.sourceView = sourceView; + alertController.popoverPresentationController.sourceRect = sourceRect; + if (!anchorViewTag) { + alertController.popoverPresentationController.permittedArrowDirections = 0; + } + [controller presentViewController:alertController animated:YES completion:nil]; + } +} + +RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options + failureCallback:(RCTResponseErrorBlock)failureCallback successCallback:(RCTResponseSenderBlock)successCallback) { if (RCTRunningInAppExtension()) { RCTLogError(@"Unable to show action sheet from app extension"); return; } - - UIActionSheet *actionSheet = [UIActionSheet new]; - actionSheet.title = options[@"title"]; - - for (NSString *option in options[@"options"]) { - [actionSheet addButtonWithTitle:option]; - } - - if (options[@"destructiveButtonIndex"]) { - actionSheet.destructiveButtonIndex = [options[@"destructiveButtonIndex"] integerValue]; - } - if (options[@"cancelButtonIndex"]) { - actionSheet.cancelButtonIndex = [options[@"cancelButtonIndex"] integerValue]; - } - - actionSheet.delegate = self; - - _callbacks[RCTKeyForInstance(actionSheet)] = successCallback; - - UIWindow *appWindow = RCTSharedApplication().delegate.window; - if (appWindow == nil) { - RCTLogError(@"Tried to display action sheet but there is no application window. options: %@", options); - return; - } - [actionSheet showInView:appWindow]; -} - -RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options - failureCallback:(RCTResponseSenderBlock)failureCallback - successCallback:(RCTResponseSenderBlock)successCallback) -{ - NSMutableArray *items = [NSMutableArray array]; + NSMutableArray *items = [NSMutableArray array]; NSString *message = [RCTConvert NSString:options[@"message"]]; if (message) { [items addObject:message]; @@ -89,22 +161,18 @@ RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options [items addObject:URL]; } if (items.count == 0) { - failureCallback(@[@"No `url` or `message` to share"]); - return; - } - if (RCTRunningInAppExtension()) { - failureCallback(@[@"Unable to show action sheet from app extension"]); + RCTLogError(@"No `url` or `message` to share"); return; } - UIActivityViewController *share = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; - UIViewController *ctrl = RCTSharedApplication().delegate.window.rootViewController; + UIActivityViewController *shareController = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; + UIViewController *controller = RCTKeyWindow().rootViewController; #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 if (![UIActivityViewController instancesRespondToSelector:@selector(setCompletionWithItemsHandler:)]) { // Legacy iOS 7 implementation - share.completionHandler = ^(NSString *activityType, BOOL completed) { + shareController.completionHandler = ^(NSString *activityType, BOOL completed) { successCallback(@[@(completed), RCTNullIfNil(activityType)]); }; } else @@ -113,57 +181,37 @@ RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options { // iOS 8 version - share.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, __unused NSArray *returnedItems, NSError *activityError) { + shareController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, __unused NSArray *returnedItems, NSError *activityError) { if (activityError) { - failureCallback(@[RCTNullIfNil(activityError.localizedDescription)]); + failureCallback(activityError); } else { successCallback(@[@(completed), RCTNullIfNil(activityType)]); } }; - } - /* - * The `anchor` option takes a view to set as the anchor for the share - * popup to point to, on iPads running iOS 8. If it is not passed, it - * defaults to centering the share popup on screen without any arrows. - */ - if ([share respondsToSelector:@selector(popoverPresentationController)]) { - share.popoverPresentationController.sourceView = ctrl.view; + shareController.modalPresentationStyle = UIModalPresentationPopover; NSNumber *anchorViewTag = [RCTConvert NSNumber:options[@"anchor"]]; - if (anchorViewTag) { - UIView *anchorView = [self.bridge.uiManager viewForReactTag:anchorViewTag]; - share.popoverPresentationController.sourceRect = [anchorView convertRect:anchorView.bounds toView:ctrl.view]; - } else { - CGRect sourceRect = CGRectMake(ctrl.view.center.x, ctrl.view.center.y, 1, 1); - share.popoverPresentationController.sourceRect = sourceRect; - share.popoverPresentationController.permittedArrowDirections = 0; + if (!anchorViewTag) { + shareController.popoverPresentationController.permittedArrowDirections = 0; } + shareController.popoverPresentationController.sourceView = controller.view; + shareController.popoverPresentationController.sourceRect = [self sourceRectInView:controller.view anchorViewTag:anchorViewTag]; } - [ctrl presentViewController:share animated:YES completion:nil]; + [controller presentViewController:shareController animated:YES completion:nil]; } #pragma mark UIActionSheetDelegate Methods - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { - NSString *key = RCTKeyForInstance(actionSheet); - RCTResponseSenderBlock callback = _callbacks[key]; + RCTResponseSenderBlock callback = [_callbacks objectForKey:actionSheet]; if (callback) { callback(@[@(buttonIndex)]); - [_callbacks removeObjectForKey:key]; + [_callbacks removeObjectForKey:actionSheet]; } else { RCTLogWarn(@"No callback registered for action sheet: %@", actionSheet.title); } - - [RCTSharedApplication().delegate.window makeKeyWindow]; -} - -#pragma mark Private - -static NSString *RCTKeyForInstance(id instance) -{ - return [NSString stringWithFormat:@"%p", instance]; } @end diff --git a/Libraries/Animated/src/Interpolation.js b/Libraries/Animated/src/Interpolation.js index dcb62ab70..da9671316 100644 --- a/Libraries/Animated/src/Interpolation.js +++ b/Libraries/Animated/src/Interpolation.js @@ -202,13 +202,22 @@ function createInterpolationFromStringOutputRange( // [200, 250], // [0, 0.5], // ] + /* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to + * guard against this possibility. + */ var outputRanges = outputRange[0].match(stringShapeRegex).map(() => []); outputRange.forEach(value => { + /* $FlowFixMe(>=0.18.0): `value.match()` can return `null`. Need to guard + * against this possibility. + */ value.match(stringShapeRegex).forEach((number, i) => { outputRanges[i].push(+number); }); }); + /* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to + * guard against this possibility. + */ var interpolations = outputRange[0].match(stringShapeRegex).map((value, i) => { return Interpolation.create({ ...config, diff --git a/Libraries/CameraRoll/CameraRoll.js b/Libraries/CameraRoll/CameraRoll.js index a710e7ec1..1eee46a3e 100644 --- a/Libraries/CameraRoll/CameraRoll.js +++ b/Libraries/CameraRoll/CameraRoll.js @@ -119,6 +119,8 @@ class CameraRoll { /** * Saves the image to the camera roll / gallery. * + * The CameraRoll API is not yet implemented for Android. + * * @param {string} tag On Android, this is a local URI, such * as `"file:///sdcard/img.png"`. * diff --git a/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.h b/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.h index f075325e1..0cc1c5353 100644 --- a/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.h +++ b/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.h @@ -15,11 +15,6 @@ @interface RCTBridge (RCTAssetsLibraryImageLoader) -/** - * The shared Assets Library image loader - */ -@property (nonatomic, readonly) RCTAssetsLibraryImageLoader *assetsLibraryImageLoader; - /** * The shared asset library instance. */ diff --git a/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.m b/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.m index 3ccbc34b4..ebec0764e 100644 --- a/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.m +++ b/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.m @@ -41,10 +41,15 @@ RCT_EXPORT_MODULE() - (BOOL)canLoadImageURL:(NSURL *)requestURL { - return [requestURL.scheme.lowercaseString isEqualToString:@"assets-library"]; + return [requestURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame; } -- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler +- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(UIViewContentMode)resizeMode + progressHandler:(RCTImageLoaderProgressBlock)progressHandler + completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { __block volatile uint32_t cancelled = 0; @@ -69,7 +74,8 @@ RCT_EXPORT_MODULE() BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero); ALAssetRepresentation *representation = [asset defaultRepresentation]; - #if RCT_DEV +#if RCT_DEV + CGSize sizeBeingLoaded = size; if (useMaximumSize) { CGSize pointSize = representation.dimensions; @@ -78,7 +84,7 @@ RCT_EXPORT_MODULE() CGSize screenSize; if ([[[UIDevice currentDevice] systemVersion] compare:@"8.0" options:NSNumericSearch] == NSOrderedDescending) { - screenSize = UIScreen.mainScreen.nativeBounds.size; + screenSize = [UIScreen mainScreen].nativeBounds.size; } else { CGSize mainScreenSize = [UIScreen mainScreen].bounds.size; CGFloat mainScreenScale = [[UIScreen mainScreen] scale]; @@ -87,9 +93,11 @@ RCT_EXPORT_MODULE() CGFloat maximumPixelDimension = fmax(screenSize.width, screenSize.height); if (sizeBeingLoaded.width > maximumPixelDimension || sizeBeingLoaded.height > maximumPixelDimension) { - RCTLogInfo(@"[PERF ASSETS] Loading %@ at size %@, which is larger than screen size %@", representation.filename, NSStringFromCGSize(sizeBeingLoaded), NSStringFromCGSize(screenSize)); + RCTLogInfo(@"[PERF ASSETS] Loading %@ at size %@, which is larger than screen size %@", + representation.filename, NSStringFromCGSize(sizeBeingLoaded), NSStringFromCGSize(screenSize)); } - #endif + +#endif UIImage *image; NSError *error = nil; @@ -106,8 +114,7 @@ RCT_EXPORT_MODULE() }); } else { NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@ with no error message.", imageURL]; - NSError *error = RCTErrorWithMessage(errorText); - completionHandler(error, nil); + completionHandler(RCTErrorWithMessage(errorText), nil); } } failureBlock:^(NSError *loadError) { if (cancelled) { @@ -115,8 +122,7 @@ RCT_EXPORT_MODULE() } NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@.\niOS Error: %@", imageURL, loadError]; - NSError *error = RCTErrorWithMessage(errorText); - completionHandler(error, nil); + completionHandler(RCTErrorWithMessage(errorText), nil); }]; return ^{ @@ -128,14 +134,9 @@ RCT_EXPORT_MODULE() @implementation RCTBridge (RCTAssetsLibraryImageLoader) -- (RCTAssetsLibraryImageLoader *)assetsLibraryImageLoader -{ - return self.modules[RCTBridgeModuleNameForClass([RCTAssetsLibraryImageLoader class])]; -} - - (ALAssetsLibrary *)assetsLibrary { - return [self.assetsLibraryImageLoader assetsLibrary]; + return [self.modules[RCTBridgeModuleNameForClass([RCTAssetsLibraryImageLoader class])] assetsLibrary]; } @end @@ -154,7 +155,11 @@ static dispatch_queue_t RCTAssetsLibraryImageLoaderQueue(void) // Why use a custom scaling method? Greater efficiency, reduced memory overhead: // http://www.mindsea.com/2012/12/downscaling-huge-alassets-without-fear-of-sigkill -static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation, CGSize size, CGFloat scale, UIViewContentMode resizeMode, NSError **error) +static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation, + CGSize size, + CGFloat scale, + UIViewContentMode resizeMode, + NSError **error) { NSUInteger length = (NSUInteger)representation.size; NSMutableData *data = [NSMutableData dataWithLength:length]; diff --git a/Libraries/CameraRoll/RCTCameraRollManager.h b/Libraries/CameraRoll/RCTCameraRollManager.h index 51921b7ed..30407c9f6 100644 --- a/Libraries/CameraRoll/RCTCameraRollManager.h +++ b/Libraries/CameraRoll/RCTCameraRollManager.h @@ -7,7 +7,17 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + #import "RCTBridgeModule.h" +#import "RCTConvert.h" + +@interface RCTConvert (ALAssetGroup) + ++ (ALAssetsGroupType)ALAssetsGroupType:(id)json; ++ (ALAssetsFilter *)ALAssetsFilter:(id)json; + +@end @interface RCTCameraRollManager : NSObject diff --git a/Libraries/CameraRoll/RCTCameraRollManager.m b/Libraries/CameraRoll/RCTCameraRollManager.m index 3d719f4ab..e2044f03c 100644 --- a/Libraries/CameraRoll/RCTCameraRollManager.m +++ b/Libraries/CameraRoll/RCTCameraRollManager.m @@ -9,17 +9,70 @@ #import "RCTCameraRollManager.h" -#import #import #import #import #import "RCTAssetsLibraryImageLoader.h" #import "RCTBridge.h" +#import "RCTConvert.h" #import "RCTImageLoader.h" #import "RCTLog.h" #import "RCTUtils.h" +@implementation RCTConvert (ALAssetGroup) + +RCT_ENUM_CONVERTER(ALAssetsGroupType, (@{ + + // New values + @"album": @(ALAssetsGroupAlbum), + @"all": @(ALAssetsGroupAll), + @"event": @(ALAssetsGroupEvent), + @"faces": @(ALAssetsGroupFaces), + @"library": @(ALAssetsGroupLibrary), + @"photo-stream": @(ALAssetsGroupPhotoStream), + @"saved-photos": @(ALAssetsGroupSavedPhotos), + + // Legacy values + @"Album": @(ALAssetsGroupAlbum), + @"All": @(ALAssetsGroupAll), + @"Event": @(ALAssetsGroupEvent), + @"Faces": @(ALAssetsGroupFaces), + @"Library": @(ALAssetsGroupLibrary), + @"PhotoStream": @(ALAssetsGroupPhotoStream), + @"SavedPhotos": @(ALAssetsGroupSavedPhotos), + +}), ALAssetsGroupSavedPhotos, integerValue) + ++ (ALAssetsFilter *)ALAssetsFilter:(id)json +{ + static NSDictionary *options; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + options = @{ + + // New values + @"photos": [ALAssetsFilter allPhotos], + @"videos": [ALAssetsFilter allVideos], + @"all": [ALAssetsFilter allAssets], + + // Legacy values + @"Photos": [ALAssetsFilter allPhotos], + @"Videos": [ALAssetsFilter allVideos], + @"All": [ALAssetsFilter allAssets], + }; + }); + + ALAssetsFilter *filter = options[json ?: @"photos"]; + if (!filter) { + RCTLogError(@"Invalid filter option: '%@'. Expected one of 'photos'," + "'videos' or 'all'.", json); + } + return filter ?: [ALAssetsFilter allPhotos]; +} + +@end + @implementation RCTCameraRollManager RCT_EXPORT_MODULE() @@ -35,18 +88,23 @@ RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag errorCallback(loadError); return; } - [_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) { - if (saveError) { - RCTLogWarn(@"Error saving cropped image: %@", saveError); - errorCallback(saveError); - } else { - successCallback(@[assetURL.absoluteString]); - } - }]; + // It's unclear if writeImageToSavedPhotosAlbum is thread-safe + dispatch_async(dispatch_get_main_queue(), ^{ + [_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) { + if (saveError) { + RCTLogWarn(@"Error saving cropped image: %@", saveError); + errorCallback(saveError); + } else { + successCallback(@[assetURL.absoluteString]); + } + }]; + }); }]; } -- (void)callCallback:(RCTResponseSenderBlock)callback withAssets:(NSArray *)assets hasNextPage:(BOOL)hasNextPage +- (void)callCallback:(RCTResponseSenderBlock)callback + withAssets:(NSArray *)assets + hasNextPage:(BOOL)hasNextPage { if (!assets.count) { callback(@[@{ @@ -69,45 +127,21 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params callback:(RCTResponseSenderBlock)callback errorCallback:(RCTResponseErrorBlock)errorCallback) { - NSUInteger first = [params[@"first"] integerValue]; - NSString *afterCursor = params[@"after"]; - NSString *groupTypesStr = params[@"groupTypes"]; - NSString *groupName = params[@"groupName"]; - NSString *assetType = params[@"assetType"]; - ALAssetsGroupType groupTypes; - - if ([groupTypesStr isEqualToString:@"Album"]) { - groupTypes = ALAssetsGroupAlbum; - } else if ([groupTypesStr isEqualToString:@"All"]) { - groupTypes = ALAssetsGroupAll; - } else if ([groupTypesStr isEqualToString:@"Event"]) { - groupTypes = ALAssetsGroupEvent; - } else if ([groupTypesStr isEqualToString:@"Faces"]) { - groupTypes = ALAssetsGroupFaces; - } else if ([groupTypesStr isEqualToString:@"Library"]) { - groupTypes = ALAssetsGroupLibrary; - } else if ([groupTypesStr isEqualToString:@"PhotoStream"]) { - groupTypes = ALAssetsGroupPhotoStream; - } else { - groupTypes = ALAssetsGroupSavedPhotos; - } + NSUInteger first = [RCTConvert NSInteger:params[@"first"]]; + NSString *afterCursor = [RCTConvert NSString:params[@"after"]]; + NSString *groupName = [RCTConvert NSString:params[@"groupName"]]; + ALAssetsFilter *assetType = [RCTConvert ALAssetsFilter:params[@"assetType"]]; + ALAssetsGroupType groupTypes = [RCTConvert ALAssetsGroupType:params[@"groupTypes"]]; BOOL __block foundAfter = NO; BOOL __block hasNextPage = NO; BOOL __block calledCallback = NO; - NSMutableArray *assets = [NSMutableArray new]; + NSMutableArray *assets = [NSMutableArray new]; [_bridge.assetsLibrary enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) { if (group && (groupName == nil || [groupName isEqualToString:[group valueForProperty:ALAssetsGroupPropertyName]])) { - if (assetType == nil || [assetType isEqualToString:@"Photos"]) { - [group setAssetsFilter:ALAssetsFilter.allPhotos]; - } else if ([assetType isEqualToString:@"Videos"]) { - [group setAssetsFilter:ALAssetsFilter.allVideos]; - } else if ([assetType isEqualToString:@"All"]) { - [group setAssetsFilter:ALAssetsFilter.allAssets]; - } - + [group setAssetsFilter:assetType]; [group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stopAssets) { if (result) { NSString *uri = ((NSURL *)[result valueForProperty:ALAssetPropertyAssetURL]).absoluteString; diff --git a/Libraries/CameraRoll/RCTImagePickerManager.m b/Libraries/CameraRoll/RCTImagePickerManager.m index 281149196..d6f8846c0 100644 --- a/Libraries/CameraRoll/RCTImagePickerManager.m +++ b/Libraries/CameraRoll/RCTImagePickerManager.m @@ -23,9 +23,9 @@ @implementation RCTImagePickerManager { - NSMutableArray *_pickers; - NSMutableArray *_pickerCallbacks; - NSMutableArray *_pickerCancelCallbacks; + NSMutableArray *_pickers; + NSMutableArray *_pickerCallbacks; + NSMutableArray *_pickerCancelCallbacks; } RCT_EXPORT_MODULE(ImagePickerIOS); @@ -42,7 +42,7 @@ RCT_EXPORT_MODULE(ImagePickerIOS); RCT_EXPORT_METHOD(canRecordVideos:(RCTResponseSenderBlock)callback) { - NSArray *availableMediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; + NSArray *availableMediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; callback(@[@([availableMediaTypes containsObject:(NSString *)kUTTypeMovie])]); } @@ -59,9 +59,8 @@ RCT_EXPORT_METHOD(openCameraDialog:(NSDictionary *)config cancelCallback(@[@"Camera access is unavailable in an app extension"]); return; } - - UIWindow *keyWindow = RCTSharedApplication().keyWindow; - UIViewController *rootViewController = keyWindow.rootViewController; + + UIViewController *rootViewController = RCTKeyWindow().rootViewController; UIImagePickerController *imagePicker = [UIImagePickerController new]; imagePicker.delegate = self; @@ -86,15 +85,14 @@ RCT_EXPORT_METHOD(openSelectDialog:(NSDictionary *)config cancelCallback(@[@"Image picker is currently unavailable in an app extension"]); return; } - - UIWindow *keyWindow = RCTSharedApplication().keyWindow; - UIViewController *rootViewController = keyWindow.rootViewController; + + UIViewController *rootViewController = RCTKeyWindow().rootViewController; UIImagePickerController *imagePicker = [UIImagePickerController new]; imagePicker.delegate = self; imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; - NSMutableArray *allowedTypes = [NSMutableArray new]; + NSMutableArray *allowedTypes = [NSMutableArray new]; if ([config[@"showImages"] boolValue]) { [allowedTypes addObject:(NSString *)kUTTypeImage]; } @@ -121,8 +119,7 @@ didFinishPickingMediaWithInfo:(NSDictionary *)info [_pickerCallbacks removeObjectAtIndex:index]; [_pickerCancelCallbacks removeObjectAtIndex:index]; - UIWindow *keyWindow = RCTSharedApplication().keyWindow; - UIViewController *rootViewController = keyWindow.rootViewController; + UIViewController *rootViewController = RCTKeyWindow().rootViewController; [rootViewController dismissViewControllerAnimated:YES completion:nil]; callback(@[[info[UIImagePickerControllerReferenceURL] absoluteString]]); @@ -137,8 +134,7 @@ didFinishPickingMediaWithInfo:(NSDictionary *)info [_pickerCallbacks removeObjectAtIndex:index]; [_pickerCancelCallbacks removeObjectAtIndex:index]; - UIWindow *keyWindow = RCTSharedApplication().keyWindow; - UIViewController *rootViewController = keyWindow.rootViewController; + UIViewController *rootViewController = RCTKeyWindow().rootViewController; [rootViewController dismissViewControllerAnimated:YES completion:nil]; callback(@[]); diff --git a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m index 144f80805..d0e5b6459 100644 --- a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m +++ b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m @@ -24,35 +24,44 @@ RCT_EXPORT_MODULE() - (BOOL)canLoadImageURL:(NSURL *)requestURL { - return [requestURL.scheme.lowercaseString isEqualToString:@"ph"]; + return [requestURL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame; } -- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler +- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(UIViewContentMode)resizeMode + progressHandler:(RCTImageLoaderProgressBlock)progressHandler + completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { // Using PhotoKit for iOS 8+ // The 'ph://' prefix is used by FBMediaKit to differentiate between // assets-library. It is prepended to the local ID so that it is in the // form of an, NSURL which is what assets-library uses. - NSString *phAssetID = [imageURL.absoluteString substringFromIndex:[@"ph://" length]]; + NSString *phAssetID = [imageURL.absoluteString substringFromIndex:@"ph://".length]; PHFetchResult *results = [PHAsset fetchAssetsWithLocalIdentifiers:@[phAssetID] options:nil]; if (results.count == 0) { NSString *errorText = [NSString stringWithFormat:@"Failed to fetch PHAsset with local identifier %@ with no error message.", phAssetID]; - NSError *error = RCTErrorWithMessage(errorText); - completionHandler(error, nil); + completionHandler(RCTErrorWithMessage(errorText), nil); return ^{}; } PHAsset *asset = [results firstObject]; - PHImageRequestOptions *imageOptions = [PHImageRequestOptions new]; - imageOptions.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { - static const double multiplier = 1e6; - progressHandler(progress * multiplier, multiplier); - }; + + if (progressHandler) { + imageOptions.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { + static const double multiplier = 1e6; + progressHandler(progress * multiplier, multiplier); + }; + } + + // Note: PhotoKit defaults to a deliveryMode of PHImageRequestOptionsDeliveryModeOpportunistic + // which means it may call back multiple times - we probably don't want that + imageOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero); CGSize targetSize; - if (useMaximumSize) { targetSize = PHImageManagerMaximumSize; imageOptions.resizeMode = PHImageRequestOptionsResizeModeNone; @@ -66,7 +75,12 @@ RCT_EXPORT_MODULE() contentMode = PHImageContentModeAspectFit; } - PHImageRequestID requestID = [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:targetSize contentMode:contentMode options:imageOptions resultHandler:^(UIImage *result, NSDictionary *info) { + PHImageRequestID requestID = + [[PHImageManager defaultManager] requestImageForAsset:asset + targetSize:targetSize + contentMode:contentMode + options:imageOptions + resultHandler:^(UIImage *result, NSDictionary *info) { if (result) { completionHandler(nil, result); } else { diff --git a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js index 7e0ea6936..ed1744a1c 100644 --- a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js +++ b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js @@ -74,8 +74,8 @@ var ActivityIndicatorIOS = React.createClass({ return ( - + style={[styles.container, style]}> + ); } diff --git a/Libraries/Components/MapView/MapView.js b/Libraries/Components/MapView/MapView.js index ac715e896..37c11187e 100644 --- a/Libraries/Components/MapView/MapView.js +++ b/Libraries/Components/MapView/MapView.js @@ -80,6 +80,13 @@ var MapView = React.createClass({ */ showsUserLocation: React.PropTypes.bool, + /** + * If `false` points of interest won't be displayed on the map. + * Default value is `true`. + * @platform ios + */ + showsPointsOfInterest: React.PropTypes.bool, + /** * If `false` the user won't be able to pinch/zoom the map. * Default value is `true`. diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js index 307a2bad4..1588d21eb 100644 --- a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js @@ -15,7 +15,7 @@ var React = require('React'); var ReactPropTypes = require('ReactPropTypes'); var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); -var createReactNativeComponentClass = require('createReactNativeComponentClass'); +var requireNativeComponent = require('requireNativeComponent'); var STYLE_ATTRIBUTES = [ 'Horizontal', @@ -26,6 +26,18 @@ var STYLE_ATTRIBUTES = [ 'LargeInverse' ]; +var indeterminateType = function(props, propName, componentName) { + var checker = function() { + var indeterminate = props[propName]; + var styleAttr = props.styleAttr; + if (!indeterminate && styleAttr !== 'Horizontal') { + return new Error('indeterminate=false is only valid for styleAttr=Horizontal'); + } + }; + + return ReactPropTypes.bool(props, propName, componentName) || checker(); +}; + /** * 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. @@ -62,6 +74,19 @@ var ProgressBarAndroid = React.createClass({ * - LargeInverse */ styleAttr: ReactPropTypes.oneOf(STYLE_ATTRIBUTES), + /** + * If the progress bar will show indeterminate progress. Note that this + * can only be false if styleAttr is Horizontal. + */ + indeterminate: indeterminateType, + /** + * The progress value (between 0 and 1). + */ + progress: ReactPropTypes.number, + /** + * Color of the progress bar. + */ + color: ReactPropTypes.string, /** * Used to locate this view in end-to-end tests. */ @@ -71,6 +96,7 @@ var ProgressBarAndroid = React.createClass({ getDefaultProps: function() { return { styleAttr: 'Large', + indeterminate: true }; }, @@ -81,12 +107,6 @@ var ProgressBarAndroid = React.createClass({ }, }); -var AndroidProgressBar = createReactNativeComponentClass({ - validAttributes: { - ...ReactNativeViewAttributes.UIView, - styleAttr: true, - }, - uiViewClassName: 'AndroidProgressBar', -}); +var AndroidProgressBar = requireNativeComponent('AndroidProgressBar', ProgressBarAndroid); module.exports = ProgressBarAndroid; diff --git a/Libraries/Components/SliderIOS/SliderIOS.ios.js b/Libraries/Components/SliderIOS/SliderIOS.ios.js index 0c06fbf6f..acc0ea842 100644 --- a/Libraries/Components/SliderIOS/SliderIOS.ios.js +++ b/Libraries/Components/SliderIOS/SliderIOS.ios.js @@ -41,6 +41,13 @@ var SliderIOS = React.createClass({ */ value: PropTypes.number, + /** + * Step value of the slider. The value should be + * between 0 and (maximumValue - minimumValue). + * Default value is 0. + */ + step: PropTypes.number, + /** * Initial minimum value of the slider. Default value is 0. */ @@ -63,6 +70,12 @@ var SliderIOS = React.createClass({ */ maximumTrackTintColor: PropTypes.string, + /** + * If true the user won't be able to move the slider. + * Default value is false. + */ + disabled: PropTypes.bool, + /** * Callback continuously called while the user is dragging the slider. */ @@ -75,27 +88,33 @@ var SliderIOS = React.createClass({ onSlidingComplete: PropTypes.func, }, - _onValueChange: function(event: Event) { - this.props.onChange && this.props.onChange(event); - if (event.nativeEvent.continuous) { - this.props.onValueChange && - this.props.onValueChange(event.nativeEvent.value); - } else { - this.props.onSlidingComplete && event.nativeEvent.value !== undefined && - this.props.onSlidingComplete(event.nativeEvent.value); - } + getDefaultProps: function() : any { + return { + disabled: false, + }; }, render: function() { + + let onValueChange = this.props.onValueChange && ((event: Event) => { + this.props.onValueChange && + this.props.onValueChange(event.nativeEvent.value); + }); + + let onSlidingComplete = this.props.onSlidingComplete && ((event: Event) => { + this.props.onSlidingComplete && + this.props.onSlidingComplete(event.nativeEvent.value); + }); + + let {style, ...props} = this.props; + style = [styles.slider, this.props.style]; + return ( ); } @@ -107,8 +126,6 @@ var styles = StyleSheet.create({ }, }); -var RCTSlider = requireNativeComponent('RCTSlider', SliderIOS, { - nativeOnly: { onChange: true }, -}); +var RCTSlider = requireNativeComponent('RCTSlider', SliderIOS); module.exports = SliderIOS; diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 52dc55222..f511a3804 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -47,6 +47,10 @@ if (Platform.OS === 'android') { var RCTTextField = requireNativeComponent('RCTTextField', null); } +type DefaultProps = { + blurOnSubmit: boolean; +}; + type Event = Object; /** @@ -82,6 +86,11 @@ type Event = Object; * ``` */ var TextInput = React.createClass({ + statics: { + /* TODO(brentvatne) docs are needed for this */ + State: TextInputState, + }, + propTypes: { /** * Can tell TextInput to automatically capitalize certain characters. @@ -172,7 +181,6 @@ var TextInput = React.createClass({ /** * Limits the maximum number of characters that can be entered. Use this * instead of implementing the logic in JS to avoid flicker. - * @platform ios */ maxLength: PropTypes.number, /** @@ -217,6 +225,13 @@ var TextInput = React.createClass({ * Callback that is called when the text input's submit button is pressed. */ onSubmitEditing: PropTypes.func, + /** + * Callback that is called when a key is pressed. + * Pressed key value is passed as an argument to the callback handler. + * Fires before onChange callbacks. + * @platform ios + */ + onKeyPress: PropTypes.func, /** * Invoked on mount and layout changes with `{x, y, width, height}`. */ @@ -276,6 +291,12 @@ var TextInput = React.createClass({ * @platform ios */ selectTextOnFocus: PropTypes.bool, + /** + * If true, the text field will blur when submitted. + * The default value is true. + * @platform ios + */ + blurOnSubmit: PropTypes.bool, /** * Styles */ @@ -291,6 +312,12 @@ var TextInput = React.createClass({ underlineColorAndroid: PropTypes.string, }, + getDefaultProps: function(): DefaultProps { + return { + blurOnSubmit: true, + }; + }, + /** * `NativeMethodsMixin` will look for this when invoking `setNativeProps`. We * make `this` look like an actual native component class. @@ -473,6 +500,7 @@ var TextInput = React.createClass({ mostRecentEventCount={this.state.mostRecentEventCount} multiline={this.props.multiline} numberOfLines={this.props.numberOfLines} + maxLength={this.props.maxLength} onFocus={this._onFocus} onBlur={this._onBlur} onChange={this._onChange} @@ -522,13 +550,16 @@ var TextInput = React.createClass({ this.props.onChange && this.props.onChange(event); this.props.onChangeText && this.props.onChangeText(text); this.setState({mostRecentEventCount: eventCount}, () => { - // This is a controlled component, so make sure to force the native value - // to match. Most usage shouldn't need this, but if it does this will be - // more correct but might flicker a bit and/or cause the cursor to jump. - if (text !== this.props.value && typeof this.props.value === 'string') { - this.refs.input.setNativeProps({ - text: this.props.value, - }); + // NOTE: this doesn't seem to be needed on iOS - keeping for now in case it's required on Android + if (Platform.OS === 'android') { + // This is a controlled component, so make sure to force the native value + // to match. Most usage shouldn't need this, but if it does this will be + // more correct but might flicker a bit and/or cause the cursor to jump. + if (text !== this.props.value && typeof this.props.value === 'string') { + this.refs.input.setNativeProps({ + text: this.props.value, + }); + } } }); }, diff --git a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js index 5692c9771..dcaed6b0c 100644 --- a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js +++ b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js @@ -103,6 +103,10 @@ var ToolbarAndroid = React.createClass({ * Callback called when the icon is selected. */ onIconClicked: ReactPropTypes.func, + /** + * Sets the overflow icon. + */ + overflowIcon: optionalImageSource, /** * Sets the toolbar subtitle. */ @@ -135,6 +139,9 @@ var ToolbarAndroid = React.createClass({ if (this.props.navIcon) { nativeProps.navIcon = resolveAssetSource(this.props.navIcon); } + if (this.props.overflowIcon) { + nativeProps.overflowIcon = resolveAssetSource(this.props.overflowIcon); + } if (this.props.actions) { nativeProps.actions = []; for (var i = 0; i < this.props.actions.length; i++) { @@ -169,6 +176,7 @@ var toolbarAttributes = { actions: true, logo: true, navIcon: true, + overflowIcon: true, subtitle: true, subtitleColor: true, title: true, diff --git a/Libraries/Components/Touchable/BoundingDimensions.js b/Libraries/Components/Touchable/BoundingDimensions.js index 38934ea04..f4acffc02 100644 --- a/Libraries/Components/Touchable/BoundingDimensions.js +++ b/Libraries/Components/Touchable/BoundingDimensions.js @@ -20,6 +20,11 @@ function BoundingDimensions(width, height) { this.height = height; } +BoundingDimensions.prototype.destructor = function() { + this.width = null; + this.height = null; +}; + /** * @param {HTMLElement} element Element to return `BoundingDimensions` for. * @return {BoundingDimensions} Bounding dimensions of `element`. diff --git a/Libraries/Components/Touchable/Position.js b/Libraries/Components/Touchable/Position.js index d6ae0b490..6175b6b9e 100644 --- a/Libraries/Components/Touchable/Position.js +++ b/Libraries/Components/Touchable/Position.js @@ -21,6 +21,11 @@ function Position(left, top) { this.top = top; } +Position.prototype.destructor = function() { + this.left = null; + this.top = null; +}; + PooledClass.addPoolingTo(Position, twoArgumentPooler); module.exports = Position; diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index 7cb8d35f6..3f62f027c 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -178,7 +178,6 @@ var View = React.createClass({ * `TouchableHighlight` or `TouchableOpacity`. Check out `Touchable.js`, * `ScrollResponder.js` and `ResponderEventPlugin.js` for more discussion. */ - onMoveShouldSetResponder: PropTypes.func, onResponderGrant: PropTypes.func, onResponderMove: PropTypes.func, onResponderReject: PropTypes.func, @@ -187,6 +186,8 @@ var View = React.createClass({ onResponderTerminationRequest: PropTypes.func, onStartShouldSetResponder: PropTypes.func, onStartShouldSetResponderCapture: PropTypes.func, + onMoveShouldSetResponder: PropTypes.func, + onMoveShouldSetResponderCapture: PropTypes.func, /** * Invoked on mount and layout changes with diff --git a/Libraries/Components/ViewPager/ViewPagerAndroid.android.js b/Libraries/Components/ViewPager/ViewPagerAndroid.android.js index 36c60820c..45b109134 100644 --- a/Libraries/Components/ViewPager/ViewPagerAndroid.android.js +++ b/Libraries/Components/ViewPager/ViewPagerAndroid.android.js @@ -125,7 +125,10 @@ var ViewPagerAndroid = React.createClass({ }], collapsable: false, }; - if (child.type && child.type.displayName && (child.type.displayName !== 'View')) { + if (child.type && + child.type.displayName && + (child.type.displayName !== 'RCTView') && + (child.type.displayName !== 'View')) { console.warn('Each ViewPager child must be a . Was ' + child.type.displayName); } return ReactElement.createElement(child.type, newProps); diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index 274d76728..7975653a3 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -31,6 +31,10 @@ var WebViewState = keyMirror({ ERROR: null, }); +/** + * Note that WebView is only supported on iOS for now, + * see https://facebook.github.io/react-native/docs/known-issues.html + */ var WebView = React.createClass({ propTypes: { diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index a3d457bfd..9256c3c86 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -75,6 +75,12 @@ var defaultRenderError = (errorDomain, errorCode, errorDesc) => ( ); +/** + * Renders a native WebView. + * + * Note that WebView is only supported on iOS for now, + * see https://facebook.github.io/react-native/docs/known-issues.html + */ var WebView = React.createClass({ statics: { JSNavigationScheme: JSNavigationScheme, @@ -107,6 +113,12 @@ var WebView = React.createClass({ * user can change the scale */ scalesPageToFit: PropTypes.bool, + + /** + * Allows custom handling of any webview requests by a JS handler. Return true + * or false from this method to continue loading the request. + */ + onShouldStartLoadWithRequest: PropTypes.func, }, getInitialState: function() { @@ -152,6 +164,12 @@ var WebView = React.createClass({ webViewStyles.push(styles.hidden); } + var onShouldStartLoadWithRequest = this.props.onShouldStartLoadWithRequest && ((event: Event) => { + var shouldStart = this.props.onShouldStartLoadWithRequest && + this.props.onShouldStartLoadWithRequest(event.nativeEvent); + RCTWebViewManager.startLoadWithResult(!!shouldStart, event.nativeEvent.lockIdentifier); + }); + var webView = ; diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 98cf211d0..d8edc9dd6 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -277,6 +277,7 @@ var ListView = React.createClass({ componentWillReceiveProps: function(nextProps) { if (this.props.dataSource !== nextProps.dataSource) { + this._sentEndForContentLength = null; this.setState((state, props) => { var rowsToRender = Math.min( state.curRenderedRowsCount + props.pageSize, @@ -500,9 +501,11 @@ var ListView = React.createClass({ }, _getDistanceFromEnd: function(scrollProperties) { - return scrollProperties.contentLength - - scrollProperties.visibleLength - - scrollProperties.offset; + var maxLength = Math.max( + scrollProperties.contentLength, + scrollProperties.visibleLength + ); + return maxLength - scrollProperties.visibleLength - scrollProperties.offset; }, _updateVisibleRows: function(updatedFrames) { @@ -590,6 +593,12 @@ var ListView = React.createClass({ this._renderMoreRowsIfNeeded(); } + if (this.props.onEndReached && + this._getDistanceFromEnd(this.scrollProperties) > this.props.onEndReachedThreshold) { + // Scrolled out of the end zone, so it should be able to trigger again. + this._sentEndForContentLength = null; + } + this.props.onScroll && this.props.onScroll(e); }, }); diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js index dfce209bf..5a0c35f37 100644 --- a/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js +++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js @@ -30,6 +30,8 @@ var NavigationEvent = require('NavigationEvent'); var NavigationEventEmitter = require('NavigationEventEmitter'); var NavigationTreeNode = require('NavigationTreeNode'); +var Set = require('Set'); + var emptyFunction = require('emptyFunction'); var invariant = require('invariant'); @@ -41,6 +43,13 @@ var { CAPTURING_PHASE, } = NavigationEvent; +// Event types that do not support event bubbling, capturing and +// reconciliation API (e.g event.preventDefault(), event.stopPropagation()). +var LegacyEventTypes = new Set([ + 'willfocus', + 'didfocus', +]); + /** * Class that contains the info and methods for app navigation. */ @@ -63,8 +72,8 @@ class NavigationContext { this._emitCounter = 0; this._emitQueue = []; - this.addListener('willfocus', this._onFocus, this); - this.addListener('didfocus', this._onFocus, this); + this.addListener('willfocus', this._onFocus); + this.addListener('didfocus', this._onFocus); } /* $FlowFixMe - get/set properties not yet supported */ @@ -73,6 +82,17 @@ class NavigationContext { return parent ? parent.getValue() : null; } + /* $FlowFixMe - get/set properties not yet supported */ + get top(): ?NavigationContext { + var result = null; + var parentNode = this.__node.getParent(); + while (parentNode) { + result = parentNode.getValue(); + parentNode = parentNode.getParent(); + } + return result; + } + /* $FlowFixMe - get/set properties not yet supported */ get currentRoute(): any { return this._currentRoute; @@ -85,14 +105,18 @@ class NavigationContext { addListener( eventType: string, listener: Function, - context: ?Object, useCapture: ?boolean ): EventSubscription { + if (LegacyEventTypes.has(eventType)) { + useCapture = false; + } + var emitter = useCapture ? this._captureEventEmitter : this._bubbleEventEmitter; + if (emitter) { - return emitter.addListener(eventType, listener, context); + return emitter.addListener(eventType, listener, this); } else { return {remove: emptyFunction}; } @@ -109,50 +133,65 @@ class NavigationContext { this._emitCounter++; - var targets = [this]; - var parentTarget = this.parent; - while (parentTarget) { - targets.unshift(parentTarget); - parentTarget = parentTarget.parent; + if (LegacyEventTypes.has(eventType)) { + // Legacy events does not support event bubbling and reconciliation. + this.__emit( + eventType, + data, + null, + { + defaultPrevented: false, + eventPhase: AT_TARGET, + propagationStopped: true, + target: this, + } + ); + } else { + var targets = [this]; + var parentTarget = this.parent; + while (parentTarget) { + targets.unshift(parentTarget); + parentTarget = parentTarget.parent; + } + + var propagationStopped = false; + var defaultPrevented = false; + var callback = (event) => { + propagationStopped = propagationStopped || event.isPropagationStopped(); + defaultPrevented = defaultPrevented || event.defaultPrevented; + }; + + // Capture phase + targets.some((currentTarget) => { + if (propagationStopped) { + return true; + } + + var extraInfo = { + defaultPrevented, + eventPhase: CAPTURING_PHASE, + propagationStopped, + target: this, + }; + + currentTarget.__emit(eventType, data, callback, extraInfo); + }, this); + + // bubble phase + targets.reverse().some((currentTarget) => { + if (propagationStopped) { + return true; + } + var extraInfo = { + defaultPrevented, + eventPhase: BUBBLING_PHASE, + propagationStopped, + target: this, + }; + currentTarget.__emit(eventType, data, callback, extraInfo); + }, this); } - var propagationStopped = false; - var defaultPrevented = false; - var callback = (event) => { - propagationStopped = propagationStopped || event.isPropagationStopped(); - defaultPrevented = defaultPrevented || event.defaultPrevented; - }; - - // capture phase - targets.some((currentTarget) => { - if (propagationStopped) { - return true; - } - - var extraInfo = { - defaultPrevented, - eventPhase: CAPTURING_PHASE, - propagationStopped, - target: this, - }; - - currentTarget.__emit(eventType, data, callback, extraInfo); - }, this); - - // bubble phase - targets.reverse().some((currentTarget) => { - if (propagationStopped) { - return true; - } - var extraInfo = { - defaultPrevented, - eventPhase: BUBBLING_PHASE, - propagationStopped, - target: this, - }; - currentTarget.__emit(eventType, data, callback, extraInfo); - }, this); - if (didEmitCallback) { var event = NavigationEvent.pool(eventType, this, data); propagationStopped && event.stopPropagation(); @@ -189,9 +228,15 @@ class NavigationContext { case CAPTURING_PHASE: // phase = 1 emitter = this._captureEventEmitter; break; + + case AT_TARGET: // phase = 2 + emitter = this._bubbleEventEmitter; + break; + case BUBBLING_PHASE: // phase = 3 emitter = this._bubbleEventEmitter; break; + default: invariant(false, 'invalid event phase %s', extraInfo.eventPhase); } @@ -214,8 +259,10 @@ class NavigationContext { _onFocus(event: NavigationEvent): void { invariant( event.data && event.data.hasOwnProperty('route'), - 'didfocus event should provide route' + 'event type "%s" should provide route', + event.type ); + this._currentRoute = event.data.route; } } diff --git a/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationContext-test.js b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationContext-test.js index 37eb820f8..5d5ea4813 100644 --- a/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationContext-test.js +++ b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationContext-test.js @@ -50,6 +50,15 @@ describe('NavigationContext', () => { expect(child.parent).toBe(parent); }); + it('has `top`', () => { + var top = new NavigationContext(); + var parent = new NavigationContext(); + var child = new NavigationContext(); + top.appendChild(parent); + parent.appendChild(child); + expect(child.top).toBe(top); + }); + it('captures event', () => { var parent = new NavigationContext(); var child = new NavigationContext(); @@ -67,8 +76,8 @@ describe('NavigationContext', () => { }); }; - parent.addListener('yo', listener, null, true); - child.addListener('yo', listener, null, true); + parent.addListener('yo', listener, true); + child.addListener('yo', listener, true); child.emit('yo'); @@ -133,8 +142,8 @@ describe('NavigationContext', () => { var counter = 0; - parent.addListener('yo', event => event.stopPropagation(), null, true); - child.addListener('yo', event => counter++, null, true); + parent.addListener('yo', event => event.stopPropagation(), true); + child.addListener('yo', event => counter++, true); child.emit('yo'); @@ -162,8 +171,8 @@ describe('NavigationContext', () => { parent.appendChild(child); var val; - parent.addListener('yo', event => event.preventDefault(), null, true); - child.addListener('yo', event => val = event.defaultPrevented, null, true); + parent.addListener('yo', event => event.preventDefault(), true); + child.addListener('yo', event => val = event.defaultPrevented, true); child.emit('yo'); @@ -205,9 +214,9 @@ describe('NavigationContext', () => { child.emit('didyo'); }); - parent.addListener('yo', listener, null, true); - parent.addListener('didyo', listener, null, true); - child.addListener('yo', listener, null, true); + parent.addListener('yo', listener, true); + parent.addListener('didyo', listener, true); + child.addListener('yo', listener, true); child.emit('yo'); diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index b00d99a5d..8bfd55457 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -273,6 +273,8 @@ var Navigator = React.createClass({ }, getInitialState: function() { + this._navigationBarNavigator = this.props.navigationBarNavigator || this; + this._renderedSceneMap = new Map(); var routeStack = this.props.initialRouteStack || [this.props.initialRoute]; @@ -346,6 +348,12 @@ var Navigator = React.createClass({ this._navigationContext.dispose(); this._navigationContext = null; } + + this.spring.destroy(); + + if (this._interactionHandle) { + this.clearInteractionHandle(this._interactionHandle); + } }, /** @@ -411,6 +419,9 @@ var Navigator = React.createClass({ * happening, we only set values for the transition and the gesture will catch up later */ _handleSpringUpdate: function() { + if (!this.isMounted()) { + return; + } // Prioritize handling transition in progress over a gesture: if (this.state.transitionFromIndex != null) { this._transitionBetween( @@ -432,6 +443,10 @@ var Navigator = React.createClass({ * This happens at the end of a transition started by transitionTo, and when the spring catches up to a pending gesture */ _completeTransition: function() { + if (!this.isMounted()) { + return; + } + if (this.spring.getCurrentValue() !== 1 && this.spring.getCurrentValue() !== 0) { // The spring has finished catching up to a gesture in progress. Remove the pending progress // and we will be in a normal activeGesture state @@ -591,8 +606,11 @@ var Navigator = React.createClass({ _handleMoveShouldSetPanResponder: function(e, gestureState) { var sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex]; + if (!sceneConfig) { + return false; + } this._expectingGestureGrant = this._matchGestureAction(this._eligibleGestures, sceneConfig.gestures, gestureState); - return !! this._expectingGestureGrant; + return !!this._expectingGestureGrant; }, _doesGestureOverswipe: function(gestureName) { @@ -1070,7 +1088,7 @@ var Navigator = React.createClass({ } return React.cloneElement(this.props.navigationBar, { ref: (navBar) => { this._navBar = navBar; }, - navigator: this, + navigator: this._navigationBarNavigator, navState: this.state, }); }, diff --git a/Libraries/Devtools/setupDevtools.js b/Libraries/Devtools/setupDevtools.js index 93ba87555..ed85e79d9 100644 --- a/Libraries/Devtools/setupDevtools.js +++ b/Libraries/Devtools/setupDevtools.js @@ -14,7 +14,7 @@ function setupDevtools() { var messageListeners = []; var closeListeners = []; - var ws = new window.WebSocket('ws://localhost:8081/devtools'); + var ws = new window.WebSocket('ws://localhost:8097/devtools'); // this is accessed by the eval'd backend code var FOR_BACKEND = { // eslint-disable-line no-unused-vars resolveRNStyle: require('flattenStyle'), @@ -31,11 +31,11 @@ function setupDevtools() { }, }; ws.onclose = () => { - console.warn('devtools socket closed'); + setTimeout(setupDevtools, 200); closeListeners.forEach(fn => fn()); }; ws.onerror = error => { - console.warn('devtools socket errored', error); + setTimeout(setupDevtools, 200); closeListeners.forEach(fn => fn()); }; ws.onopen = function () { @@ -58,7 +58,7 @@ function setupDevtools() { // FOR_BACKEND is used by the eval'd code eval(text); // eslint-disable-line no-eval } catch (e) { - console.error('Failed to eval' + e.message); + console.error('Failed to eval: ' + e.message); return; } window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject({ diff --git a/Libraries/Fetch/fetch.js b/Libraries/Fetch/fetch.js index 68c98a90d..ff5ef29bb 100644 --- a/Libraries/Fetch/fetch.js +++ b/Libraries/Fetch/fetch.js @@ -278,6 +278,10 @@ var self = {}; } this._initBody(body) } + + Request.prototype.clone = function() { + return new Request(this) + } function decode(body) { var form = new FormData() @@ -320,6 +324,15 @@ var self = {}; this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers) this.url = options.url || '' } + + Response.prototype.clone = function() { + return new Response(this._bodyInit, { + status: this.status, + statusText: this.statusText, + headers: new Headers(this.headers), + url: this.url + }) + } Body.call(Response.prototype) diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m index ebb66f4e1..b97011630 100644 --- a/Libraries/Geolocation/RCTLocationObserver.m +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -100,7 +100,7 @@ static NSDictionary *RCTPositionError(RCTPositionErrorCode code, NSString *msg / { CLLocationManager *_locationManager; NSDictionary *_lastLocationEvent; - NSMutableArray *_pendingRequests; + NSMutableArray *_pendingRequests; BOOL _observingLocation; RCTLocationOptions _observerOptions; } @@ -249,7 +249,8 @@ RCT_EXPORT_METHOD(getCurrentPosition:(RCTLocationOptions)options #pragma mark - CLLocationManagerDelegate -- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations +- (void)locationManager:(CLLocationManager *)manager + didUpdateLocations:(NSArray *)locations { // Create event CLLocation *location = locations.lastObject; diff --git a/Libraries/Image/RCTGIFImageDecoder.m b/Libraries/Image/RCTGIFImageDecoder.m index 899a266e8..473210025 100644 --- a/Libraries/Image/RCTGIFImageDecoder.m +++ b/Libraries/Image/RCTGIFImageDecoder.m @@ -42,8 +42,8 @@ RCT_EXPORT_MODULE() if (imageCount > 1) { NSTimeInterval duration = 0; - NSMutableArray *delays = [NSMutableArray arrayWithCapacity:imageCount]; - NSMutableArray *images = [NSMutableArray arrayWithCapacity:imageCount]; + NSMutableArray *delays = [NSMutableArray arrayWithCapacity:imageCount]; + NSMutableArray *images = [NSMutableArray arrayWithCapacity:imageCount]; for (size_t i = 0; i < imageCount; i++) { CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, NULL); @@ -75,7 +75,7 @@ RCT_EXPORT_MODULE() } CFRelease(imageSource); - NSMutableArray *keyTimes = [NSMutableArray arrayWithCapacity:delays.count]; + NSMutableArray *keyTimes = [NSMutableArray arrayWithCapacity:delays.count]; NSTimeInterval runningDuration = 0; for (NSNumber *delayNumber in delays) { [keyTimes addObject:@(runningDuration / duration)]; diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index f6bf9b3e8..770747452 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -28,7 +28,7 @@ typedef void (^RCTImageLoaderCancellationBlock)(void); /** * Loads the specified image at the highest available resolution. - * Can be called from any thread, will always call callback on main thread. + * Can be called from any thread, will call back on an unspecified thread. */ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag callback:(RCTImageLoaderCompletionBlock)callback; diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index ca1092dce..753f58b35 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -9,6 +9,7 @@ #import "RCTImageLoader.h" +#import #import #import "RCTConvert.h" @@ -18,17 +19,6 @@ #import "RCTNetworking.h" #import "RCTUtils.h" -static void RCTDispatchCallbackOnMainQueue(void (^callback)(NSError *, id), NSError *error, UIImage *image) -{ - if ([NSThread isMainThread]) { - callback(error, image); - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - callback(error, image); - }); - } -} - @implementation UIImage (React) - (CAKeyframeAnimation *)reactKeyframeAnimation @@ -45,8 +35,8 @@ static void RCTDispatchCallbackOnMainQueue(void (^callback)(NSError *, id), NSEr @implementation RCTImageLoader { - NSArray *_loaders; - NSArray *_decoders; + NSArray> *_loaders; + NSArray> *_decoders; NSURLCache *_cache; } @@ -57,14 +47,14 @@ RCT_EXPORT_MODULE() - (void)setBridge:(RCTBridge *)bridge { // Get image loaders and decoders - NSMutableArray *loaders = [NSMutableArray array]; - NSMutableArray *decoders = [NSMutableArray array]; + NSMutableArray> *loaders = [NSMutableArray array]; + NSMutableArray> *decoders = [NSMutableArray array]; for (id module in bridge.modules.allValues) { if ([module conformsToProtocol:@protocol(RCTImageURLLoader)]) { - [loaders addObject:module]; + [loaders addObject:(id)module]; } if ([module conformsToProtocol:@protocol(RCTImageDataDecoder)]) { - [decoders addObject:module]; + [decoders addObject:(id)module]; } } @@ -184,7 +174,7 @@ RCT_EXPORT_MODULE() size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode - progressBlock:(RCTImageLoaderProgressBlock)progressBlock + progressBlock:(RCTImageLoaderProgressBlock)progressHandler completionBlock:(RCTImageLoaderCompletionBlock)completionBlock { if (imageTag.length == 0) { @@ -192,23 +182,20 @@ RCT_EXPORT_MODULE() return ^{}; } - // Ensure progress is dispatched on main thread - RCTImageLoaderProgressBlock progressHandler = nil; - if (progressBlock) { - progressHandler = ^(int64_t progress, int64_t total) { - if ([NSThread isMainThread]) { - progressBlock(progress, total); - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - progressBlock(progress, total); - }); - } - }; - } - - // Ensure completion is dispatched on main thread + __block volatile uint32_t cancelled = 0; RCTImageLoaderCompletionBlock completionHandler = ^(NSError *error, UIImage *image) { - RCTDispatchCallbackOnMainQueue(completionBlock, error, image); + if ([NSThread isMainThread]) { + + // Most loaders do not return on the main thread, so caller is probably not + // expecting it, and may do expensive post-processing in the callback + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (!cancelled) { + completionBlock(error, image); + } + }); + } else if (!cancelled) { + completionBlock(error, image); + } }; // Find suitable image URL loader @@ -220,7 +207,7 @@ RCT_EXPORT_MODULE() scale:scale resizeMode:resizeMode progressHandler:progressHandler - completionHandler:completionHandler]; + completionHandler:completionHandler] ?: ^{}; } // Check if networking module is available @@ -237,7 +224,16 @@ RCT_EXPORT_MODULE() __weak RCTImageLoader *weakSelf = self; __block RCTImageLoaderCancellationBlock decodeCancel = nil; RCTURLRequestCompletionBlock processResponse = - ^(NSURLResponse *response, NSData *data, __unused NSError *error) { + ^(NSURLResponse *response, NSData *data, NSError *error) { + + // Check for system errors + if (error) { + completionHandler(error, nil); + return; + } else if (!data) { + completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil); + return; + } // Check for http errors if ([response isKindOfClass:[NSHTTPURLResponse class]]) { @@ -296,9 +292,7 @@ RCT_EXPORT_MODULE() processResponse(response, data, nil); }]; - if (progressBlock) { - task.downloadProgressBlock = progressBlock; - } + task.downloadProgressBlock = progressHandler; [task start]; return ^{ @@ -306,6 +300,7 @@ RCT_EXPORT_MODULE() if (decodeCancel) { decodeCancel(); } + OSAtomicOr32Barrier(1, &cancelled); }; } @@ -317,27 +312,36 @@ RCT_EXPORT_MODULE() size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode - completionBlock:(RCTImageLoaderCompletionBlock)completionBlock + completionBlock:(RCTImageLoaderCompletionBlock)completionHandler { id imageDecoder = [self imageDataDecoderForData:data]; if (imageDecoder) { + return [imageDecoder decodeImageData:data size:size scale:scale resizeMode:resizeMode - completionHandler:completionBlock]; + completionHandler:completionHandler]; } else { + + __block volatile uint32_t cancelled = 0; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (cancelled) { + return; + } UIImage *image = [UIImage imageWithData:data scale:scale]; if (image) { - completionBlock(nil, image); + completionHandler(nil, image); } else { NSString *errorMessage = [NSString stringWithFormat:@"Error decoding image data ", data, data.length]; NSError *finalError = RCTErrorWithMessage(errorMessage); - completionBlock(finalError, nil); + completionHandler(finalError, nil); } }); - return ^{}; + + return ^{ + OSAtomicOr32Barrier(1, &cancelled); + }; } } diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index 049eb36ac..4f4a65d33 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -183,24 +183,26 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) resizeMode:self.contentMode progressBlock:progressHandler completionBlock:^(NSError *error, UIImage *image) { - if (image.reactKeyframeAnimation) { - [self.layer addAnimation:image.reactKeyframeAnimation forKey:@"contents"]; - } else { - [self.layer removeAnimationForKey:@"contents"]; - self.image = image; - } - if (error) { - if (_onError) { - _onError(@{ @"error": error.localizedDescription }); + dispatch_async(dispatch_get_main_queue(), ^{ + if (image.reactKeyframeAnimation) { + [self.layer addAnimation:image.reactKeyframeAnimation forKey:@"contents"]; + } else { + [self.layer removeAnimationForKey:@"contents"]; + self.image = image; } - } else { - if (_onLoad) { - _onLoad(nil); + if (error) { + if (_onError) { + _onError(@{ @"error": error.localizedDescription }); + } + } else { + if (_onLoad) { + _onLoad(nil); + } } - } - if (_onLoadEnd) { - _onLoadEnd(nil); - } + if (_onLoadEnd) { + _onLoadEnd(nil); + } + }); }]; } else { [self clearImage]; diff --git a/Libraries/Image/RCTShadowVirtualImage.m b/Libraries/Image/RCTShadowVirtualImage.m index c908b62df..431fbac07 100644 --- a/Libraries/Image/RCTShadowVirtualImage.m +++ b/Libraries/Image/RCTShadowVirtualImage.m @@ -38,7 +38,13 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init) CGFloat scale = [RCTConvert CGFloat:_source[@"scale"]] ?: 1; __weak RCTShadowVirtualImage *weakSelf = self; - [_bridge.imageLoader loadImageWithTag:imageTag size:CGSizeZero scale:scale resizeMode:UIViewContentModeScaleToFill progressBlock:nil completionBlock:^(NSError *error, UIImage *image) { + [_bridge.imageLoader loadImageWithTag:imageTag + size:CGSizeZero + scale:scale + resizeMode:UIViewContentModeScaleToFill + progressBlock:nil + completionBlock:^(NSError *error, UIImage *image) { + dispatch_async(_bridge.uiManager.methodQueue, ^{ RCTShadowVirtualImage *strongSelf = weakSelf; strongSelf->_image = image; diff --git a/Libraries/Image/RCTXCAssetImageLoader.m b/Libraries/Image/RCTXCAssetImageLoader.m index 93a02bce3..799798a36 100644 --- a/Libraries/Image/RCTXCAssetImageLoader.m +++ b/Libraries/Image/RCTXCAssetImageLoader.m @@ -9,6 +9,8 @@ #import "RCTXCAssetImageLoader.h" +#import + #import "RCTUtils.h" @implementation RCTXCAssetImageLoader @@ -20,34 +22,34 @@ RCT_EXPORT_MODULE() return RCTIsXCAssetURL(requestURL); } - - (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler + - (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(UIViewContentMode)resizeMode + progressHandler:(RCTImageLoaderProgressBlock)progressHandler + completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { - __block BOOL cancelled = NO; + __block volatile uint32_t cancelled = 0; dispatch_async(dispatch_get_main_queue(), ^{ + if (cancelled) { return; } - NSString *imageName = RCTBundlePathForURL(imageURL); UIImage *image = [UIImage imageNamed:imageName]; if (image) { if (progressHandler) { progressHandler(1, 1); } - - if (completionHandler) { - completionHandler(nil, image); - } + completionHandler(nil, image); } else { - if (completionHandler) { - NSString *message = [NSString stringWithFormat:@"Could not find image named %@", imageName]; - completionHandler(RCTErrorWithMessage(message), nil); - } + NSString *message = [NSString stringWithFormat:@"Could not find image named %@", imageName]; + completionHandler(RCTErrorWithMessage(message), nil); } }); return ^{ - cancelled = YES; + OSAtomicOr32Barrier(1, &cancelled); }; } diff --git a/Libraries/Image/__tests__/resolveAssetSource-test.js b/Libraries/Image/__tests__/resolveAssetSource-test.js index 6c1029ed7..81fdb2b08 100644 --- a/Libraries/Image/__tests__/resolveAssetSource-test.js +++ b/Libraries/Image/__tests__/resolveAssetSource-test.js @@ -107,7 +107,7 @@ describe('resolveAssetSource', () => { describe('bundle was loaded from file on iOS', () => { beforeEach(() => { NativeModules.SourceCode.scriptURL = - 'file:///Path/To/Simulator/main.bundle'; + 'file:///Path/To/Sample.app/main.bundle'; Platform.OS = 'ios'; }); @@ -126,7 +126,7 @@ describe('resolveAssetSource', () => { __packager_asset: true, width: 100, height: 200, - uri: 'assets/module/a/logo.png', + uri: '/Path/To/Sample.app/assets/module/a/logo.png', scale: 1, }); }); diff --git a/Libraries/Image/resolveAssetSource.js b/Libraries/Image/resolveAssetSource.js index a278a708f..638b72ce0 100644 --- a/Libraries/Image/resolveAssetSource.js +++ b/Libraries/Image/resolveAssetSource.js @@ -26,7 +26,7 @@ var PixelRatio = require('PixelRatio'); var Platform = require('Platform'); var SourceCode = require('NativeModules').SourceCode; -var _serverURL; +var _serverURL, _offlinePath; function getDevServerURL() { if (_serverURL === undefined) { @@ -44,6 +44,20 @@ function getDevServerURL() { return _serverURL; } +function getOfflinePath() { + if (_offlinePath === undefined) { + var scriptURL = SourceCode.scriptURL; + var match = scriptURL && scriptURL.match(/^file:\/\/(\/.*\/)/); + if (match) { + _offlinePath = match[1]; + } else { + _offlinePath = ''; + } + } + + return _offlinePath; +} + /** * Returns the path at which the asset can be found in the archive */ @@ -59,7 +73,7 @@ function getPathInArchive(asset) { .replace(/^assets_/, ''); // Remove "assets_" prefix } else { // E.g. 'assets/AwesomeModule/icon@2x.png' - return getScaledAssetPath(asset); + return getOfflinePath() + getScaledAssetPath(asset); } } diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js index 41b94be0b..8e652caf0 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -21,12 +21,13 @@ var sourceMapPromise; var exceptionID = 0; -function reportException(e: Error, isFatal: bool, stack?: any) { +/** + * Handles the developer-visible aspect of errors and exceptions + */ +function reportException(e: Error, isFatal: bool) { var currentExceptionID = ++exceptionID; if (RCTExceptionsManager) { - if (!stack) { - stack = parseErrorStack(e); - } + var stack = parseErrorStack(e); if (isFatal) { RCTExceptionsManager.reportFatalException(e.message, stack, currentExceptionID); } else { @@ -47,6 +48,9 @@ function reportException(e: Error, isFatal: bool, stack?: any) { } } +/** + * Logs exceptions to the (native) console and displays them + */ function handleException(e: Error, isFatal: boolean) { // Workaround for reporting errors caused by `throw 'some string'` // Unfortunately there is no way to figure out the stacktrace in this @@ -55,19 +59,9 @@ function handleException(e: Error, isFatal: boolean) { if (!e.message) { e = new Error(e); } - var stack = parseErrorStack(e); - var msg = - 'Error: ' + e.message + - '\n stack: \n' + stackToString(stack) + - '\n URL: ' + (e: any).sourceURL + - '\n line: ' + (e: any).line + - '\n message: ' + e.message; - if (console.errorOriginal) { - console.errorOriginal(msg); - } else { - console.error(msg); - } - reportException(e, isFatal, stack); + + (console._errorOriginal || console.error)(e.message); + reportException(e, isFatal); } /** @@ -75,54 +69,35 @@ function handleException(e: Error, isFatal: boolean) { * setting `console.reportErrorsAsExceptions = false;` in your app. */ function installConsoleErrorReporter() { - if (console.reportException) { + // Enable reportErrorsAsExceptions + if (console._errorOriginal) { return; // already installed } - console.reportException = reportException; - console.errorOriginal = console.error.bind(console); + console._errorOriginal = console.error.bind(console); console.error = function reactConsoleError() { - // Note that when using the built-in context executor on iOS (i.e., not - // Chrome debugging), console.error is already stubbed out to cause a - // redbox via RCTNativeLoggingHook. - console.errorOriginal.apply(null, arguments); + console._errorOriginal.apply(null, arguments); if (!console.reportErrorsAsExceptions) { return; } - var str = Array.prototype.map.call(arguments, stringifySafe).join(', '); - if (str.slice(0, 10) === '"Warning: ') { - // React warnings use console.error so that a stack trace is shown, but - // we don't (currently) want these to show a redbox - // (Note: Logic duplicated in polyfills/console.js.) - return; + + if (arguments[0] && arguments[0].stack) { + reportException(arguments[0], /* isFatal */ false); + } else { + var str = Array.prototype.map.call(arguments, stringifySafe).join(', '); + if (str.slice(0, 10) === '"Warning: ') { + // React warnings use console.error so that a stack trace is shown, but + // we don't (currently) want these to show a redbox + // (Note: Logic duplicated in polyfills/console.js.) + return; + } + var error : any = new Error('console.error: ' + str); + error.framesToPop = 1; + reportException(error, /* isFatal */ false); } - var error: any = new Error('console.error: ' + str); - error.framesToPop = 1; - reportException(error, /* isFatal */ false); }; if (console.reportErrorsAsExceptions === undefined) { console.reportErrorsAsExceptions = true; // Individual apps can disable this } } -function stackToString(stack) { - var maxLength = Math.max.apply(null, stack.map(frame => frame.methodName.length)); - return stack.map(frame => stackFrameToString(frame, maxLength)).join('\n'); -} - -function stackFrameToString(stackFrame, maxLength) { - var fileNameParts = stackFrame.file.split('/'); - var fileName = fileNameParts[fileNameParts.length - 1]; - - if (fileName.length > 18) { - fileName = fileName.substr(0, 17) + '\u2026'; /* ... */ - } - - var spaces = fillSpaces(maxLength - stackFrame.methodName.length); - return ' ' + stackFrame.methodName + spaces + ' ' + fileName + ':' + stackFrame.lineNumber; -} - -function fillSpaces(n) { - return new Array(n + 1).join(' '); -} - module.exports = { handleException, installConsoleErrorReporter }; diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index bc10123ac..a38350e86 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -22,10 +22,6 @@ /* eslint strict: 0 */ /* globals GLOBAL: true, window: true */ -// Just to make sure the JS gets packaged up. -require('RCTDebugComponentOwnership'); -require('RCTDeviceEventEmitter'); -require('PerformanceLogger'); require('regenerator/runtime'); if (typeof GLOBAL === 'undefined') { @@ -36,12 +32,10 @@ if (typeof window === 'undefined') { window = GLOBAL; } -function handleError(e, isFatal) { - try { - require('ExceptionsManager').handleException(e, isFatal); - } catch(ee) { - console.log('Failed to print error: ', ee.message); - } +function setUpConsole() { + // ExceptionsManager transitively requires Promise so we install it after + var ExceptionsManager = require('ExceptionsManager'); + ExceptionsManager.installConsoleErrorReporter(); } /** @@ -76,21 +70,19 @@ function polyfillGlobal(name, newValue, scope=GLOBAL) { Object.defineProperty(scope, name, {...descriptor, value: newValue}); } -function setUpRedBoxErrorHandler() { +function setUpErrorHandler() { + function handleError(e, isFatal) { + try { + require('ExceptionsManager').handleException(e, isFatal); + } catch(ee) { + console.log('Failed to print error: ', ee.message); + } + } + var ErrorUtils = require('ErrorUtils'); ErrorUtils.setGlobalHandler(handleError); } -function setUpRedBoxConsoleErrorHandler() { - // ExceptionsManager transitively requires Promise so we install it after - var ExceptionsManager = require('ExceptionsManager'); - var Platform = require('Platform'); - // TODO (#6925182): Enable console.error redbox on Android - if (__DEV__ && Platform.OS === 'ios') { - ExceptionsManager.installConsoleErrorReporter(); - } -} - function setUpFlowChecker() { if (__DEV__) { var checkFlowAtRuntime = require('checkFlowAtRuntime'); @@ -163,8 +155,6 @@ function setUpWebSockets() { } function setUpProfile() { - console.profile = console.profile || GLOBAL.nativeTraceBeginSection || function () {}; - console.profileEnd = console.profileEnd || GLOBAL.nativeTraceEndSection || function () {}; if (__DEV__) { require('BridgeProfiling').swizzleReactPerf(); } @@ -184,15 +174,30 @@ function setUpNumber() { Number.MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER || -(Math.pow(2, 53) - 1); } -setUpRedBoxErrorHandler(); +function setUpDevTools() { + // not when debugging in chrome + if (__DEV__ && !window.document && require('Platform').OS === 'ios') { + var setupDevtools = require('setupDevtools'); + setupDevtools(); + } +} + +setUpProcessEnv(); +setUpConsole(); setUpTimers(); setUpAlert(); setUpPromise(); +setUpErrorHandler(); setUpXHR(); -setUpRedBoxConsoleErrorHandler(); setUpGeolocation(); setUpWebSockets(); setUpProfile(); -setUpProcessEnv(); setUpFlowChecker(); setUpNumber(); +setUpDevTools(); + +// Just to make sure the JS gets packaged up. Wait until the JS environment has +// been initialized before requiring them. +require('RCTDebugComponentOwnership'); +require('RCTDeviceEventEmitter'); +require('PerformanceLogger'); diff --git a/Libraries/JavaScriptAppEngine/Initialization/__tests__/parseErrorStack-test.js b/Libraries/JavaScriptAppEngine/Initialization/__tests__/parseErrorStack-test.js index b32c5acfa..6a0855a2c 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/__tests__/parseErrorStack-test.js +++ b/Libraries/JavaScriptAppEngine/Initialization/__tests__/parseErrorStack-test.js @@ -9,7 +9,7 @@ 'use strict'; -require('mock-modules').autoMockOff(); +jest.autoMockOff(); var parseErrorStack = require('parseErrorStack'); diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js index 1124b904a..dfbfdb685 100644 --- a/Libraries/Modal/Modal.js +++ b/Libraries/Modal/Modal.js @@ -29,7 +29,8 @@ var RCTModalHostView = requireNativeComponent('RCTModalHostView', null); * * In apps written with React Native from the root view down, you should use * Navigator instead of Modal. With a top-level Navigator, you have more control - * over how to present the modal scene over the rest of your app. + * over how to present the modal scene over the rest of your app by using the + * configureScene property. */ class Modal extends React.Component { render(): ?ReactElement { @@ -58,9 +59,14 @@ class Modal extends React.Component { Modal.propTypes = { animated: PropTypes.bool, transparent: PropTypes.bool, + visible: PropTypes.bool, onDismiss: PropTypes.func, }; +Modal.defaultProps = { + visible: true, +}; + var styles = StyleSheet.create({ modal: { position: 'absolute', diff --git a/Libraries/Network/FormData.js b/Libraries/Network/FormData.js index ea5d8cc29..b2c4eda5a 100644 --- a/Libraries/Network/FormData.js +++ b/Libraries/Network/FormData.js @@ -47,26 +47,18 @@ type FormDataPart = { */ class FormData { _parts: Array; - _partsByKey: {[key: string]: FormDataNameValuePair}; constructor() { this._parts = []; - this._partsByKey = {}; } append(key: string, value: FormDataValue) { - var parts = this._partsByKey[key]; - if (parts) { - // It's a bit unclear what the behaviour should be in this case. - // The XMLHttpRequest spec doesn't specify it, while MDN says that - // the any new values should appended to existing values. We're not - // doing that for now -- it's tedious and doesn't seem worth the effort. - parts[1] = value; - return; - } - parts = [key, value]; - this._parts.push(parts); - this._partsByKey[key] = parts; + // The XMLHttpRequest spec doesn't specify if duplicate keys are allowed. + // MDN says that any new values should be appended to existing values. + // In any case, major browsers allow duplicate keys, so that's what we'll do + // too. They'll simply get appended as additional form data parts in the + // request body, leaving the server to deal with them. + this._parts.push([key, value]); } getParts(): Array { diff --git a/Libraries/Network/RCTDataRequestHandler.m b/Libraries/Network/RCTDataRequestHandler.m index e80bcfc63..c71ce79a8 100644 --- a/Libraries/Network/RCTDataRequestHandler.m +++ b/Libraries/Network/RCTDataRequestHandler.m @@ -36,6 +36,7 @@ RCT_EXPORT_MODULE() _queue.maxConcurrentOperationCount = 2; } + __weak __block NSBlockOperation *weakOp; __block NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ // Get mime type @@ -48,7 +49,7 @@ RCT_EXPORT_MODULE() expectedContentLength:-1 textEncodingName:nil]; - [delegate URLRequest:op didReceiveResponse:response]; + [delegate URLRequest:weakOp didReceiveResponse:response]; // Load data NSError *error; @@ -56,11 +57,12 @@ RCT_EXPORT_MODULE() options:NSDataReadingMappedIfSafe error:&error]; if (data) { - [delegate URLRequest:op didReceiveData:data]; + [delegate URLRequest:weakOp didReceiveData:data]; } - [delegate URLRequest:op didCompleteWithError:error]; + [delegate URLRequest:weakOp didCompleteWithError:error]; }]; + weakOp = op; [_queue addOperation:op]; return op; } diff --git a/Libraries/Network/RCTFileRequestHandler.m b/Libraries/Network/RCTFileRequestHandler.m index 797fab9e2..ad55ba0f4 100644 --- a/Libraries/Network/RCTFileRequestHandler.m +++ b/Libraries/Network/RCTFileRequestHandler.m @@ -42,6 +42,7 @@ RCT_EXPORT_MODULE() _fileQueue.maxConcurrentOperationCount = 4; } + __weak __block NSBlockOperation *weakOp; __block NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ // Get content length @@ -49,7 +50,7 @@ RCT_EXPORT_MODULE() NSFileManager *fileManager = [NSFileManager new]; NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:request.URL.path error:&error]; if (error) { - [delegate URLRequest:op didCompleteWithError:error]; + [delegate URLRequest:weakOp didCompleteWithError:error]; return; } @@ -66,18 +67,19 @@ RCT_EXPORT_MODULE() expectedContentLength:[fileAttributes[NSFileSize] ?: @-1 integerValue] textEncodingName:nil]; - [delegate URLRequest:op didReceiveResponse:response]; + [delegate URLRequest:weakOp didReceiveResponse:response]; // Load data NSData *data = [NSData dataWithContentsOfURL:request.URL options:NSDataReadingMappedIfSafe error:&error]; if (data) { - [delegate URLRequest:op didReceiveData:data]; + [delegate URLRequest:weakOp didReceiveData:data]; } - [delegate URLRequest:op didCompleteWithError:error]; + [delegate URLRequest:weakOp didCompleteWithError:error]; }]; + weakOp = op; [_fileQueue addOperation:op]; return op; } diff --git a/Libraries/Network/RCTHTTPRequestHandler.m b/Libraries/Network/RCTHTTPRequestHandler.m index 961bedb22..f52a20426 100644 --- a/Libraries/Network/RCTHTTPRequestHandler.m +++ b/Libraries/Network/RCTHTTPRequestHandler.m @@ -47,7 +47,7 @@ RCT_EXPORT_MODULE() - (BOOL)canHandleRequest:(NSURLRequest *)request { - static NSSet *schemes = nil; + static NSSet *schemes = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // technically, RCTHTTPRequestHandler can handle file:// as well, diff --git a/Libraries/Network/RCTNetworkTask.h b/Libraries/Network/RCTNetworkTask.h index 268483670..5265d8ef3 100644 --- a/Libraries/Network/RCTNetworkTask.h +++ b/Libraries/Network/RCTNetworkTask.h @@ -22,7 +22,7 @@ typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response); @property (nonatomic, readonly) NSURLRequest *request; @property (nonatomic, readonly) NSNumber *requestID; -@property (nonatomic, readonly) id requestToken; +@property (nonatomic, readonly, weak) id requestToken; @property (nonatomic, readonly) NSURLResponse *response; @property (nonatomic, readonly) RCTURLRequestCompletionBlock completionBlock; diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m index 7d5e1fd2c..b1450a631 100644 --- a/Libraries/Network/RCTNetworking.m +++ b/Libraries/Network/RCTNetworking.m @@ -37,7 +37,7 @@ typedef RCTURLRequestCancellationBlock (^RCTHTTPQueryResult)(NSError *error, NSD @implementation RCTHTTPFormDataHelper { - NSMutableArray *_parts; + NSMutableArray *_parts; NSMutableData *_multipartBody; RCTHTTPQueryResult _callback; NSString *_boundary; @@ -122,7 +122,7 @@ static NSString *RCTGenerateFormBoundary() @implementation RCTNetworking { NSMutableDictionary *_tasksByRequestID; - NSArray *_handlers; + NSArray> *_handlers; } @synthesize bridge = _bridge; @@ -133,10 +133,10 @@ RCT_EXPORT_MODULE() - (void)setBridge:(RCTBridge *)bridge { // get handlers - NSMutableArray *handlers = [NSMutableArray array]; + NSMutableArray> *handlers = [NSMutableArray array]; for (id module in bridge.modules.allValues) { if ([module conformsToProtocol:@protocol(RCTURLRequestHandler)]) { - [handlers addObject:module]; + [handlers addObject:(id)module]; } } diff --git a/Libraries/RCTTest/RCTTestRunner.h b/Libraries/RCTTest/RCTTestRunner.h index 72a2b3127..097bef6bf 100644 --- a/Libraries/RCTTest/RCTTestRunner.h +++ b/Libraries/RCTTest/RCTTestRunner.h @@ -9,18 +9,21 @@ #import +#ifndef FB_REFERENCE_IMAGE_DIR +#define FB_REFERENCE_IMAGE_DIR "" +#endif + /** - * Use the RCTInitRunnerForApp macro for typical usage. - * - * Add this to your test target's gcc preprocessor macros: - * - * FB_REFERENCE_IMAGE_DIR="\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\"" + * Use the RCTInitRunnerForApp macro for typical usage. See FBSnapshotTestCase.h for more information + * on how to configure the snapshotting system. */ #define RCTInitRunnerForApp(app__, moduleProvider__) \ [[RCTTestRunner alloc] initWithApp:(app__) \ referenceDirectory:@FB_REFERENCE_IMAGE_DIR \ moduleProvider:(moduleProvider__)] +@protocol RCTBridgeModule; + @interface RCTTestRunner : NSObject @property (nonatomic, assign) BOOL recordMode; @@ -31,12 +34,12 @@ * macro instead of calling this directly. * * @param app The path to the app bundle without suffixes, e.g. IntegrationTests/IntegrationTestsApp - * @param referenceDirectory The path for snapshot references images. The RCTInitRunnerForApp macro uses FB_REFERENCE_IMAGE_DIR for this automatically. + * @param referenceDirectory The path for snapshot references images. * @param block A block that returns an array of extra modules to be used by the test runner. */ - (instancetype)initWithApp:(NSString *)app referenceDirectory:(NSString *)referenceDirectory - moduleProvider:(NSArray *(^)(void))block NS_DESIGNATED_INITIALIZER; + moduleProvider:(NSArray> *(^)(void))block NS_DESIGNATED_INITIALIZER; /** * Simplest runTest function simply mounts the specified JS module with no diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 0da2c8388..13d38247b 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -34,6 +34,9 @@ static const NSTimeInterval kTestTeardownTimeoutSeconds = 30; RCTAssertParam(referenceDirectory); if ((self = [super init])) { + if (!referenceDirectory.length) { + referenceDirectory = [[NSBundle bundleForClass:self.class].resourcePath stringByAppendingPathComponent:@"ReferenceImages"]; + } NSString *sanitizedAppName = [app stringByReplacingOccurrencesOfString:@"/" withString:@"-"]; sanitizedAppName = [sanitizedAppName stringByReplacingOccurrencesOfString:@"\\" withString:@"-"]; @@ -123,7 +126,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) RCTSetLogFunction(RCTDefaultLogFunction); - NSArray *nonLayoutSubviews = [vc.view.subviews filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id subview, NSDictionary *bindings) { + NSArray *nonLayoutSubviews = [vc.view.subviews filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id subview, NSDictionary *bindings) { return ![NSStringFromClass([subview class]) isEqualToString:@"_UILayoutGuide"]; }]]; RCTAssert(nonLayoutSubviews.count == 0, @"There shouldn't be any other views: %@", nonLayoutSubviews); diff --git a/Libraries/ReactIOS/renderApplication.ios.js b/Libraries/ReactIOS/renderApplication.ios.js index 6a4bb5f68..aca645324 100644 --- a/Libraries/ReactIOS/renderApplication.ios.js +++ b/Libraries/ReactIOS/renderApplication.ios.js @@ -69,11 +69,6 @@ function renderApplication( rootTag, 'Expect to have a valid rootTag, instead got ', rootTag ); - // not when debugging in chrome - if (__DEV__ && !window.document) { - var setupDevtools = require('setupDevtools'); - setupDevtools(); - } React.render( ( var styles = StyleSheet.create({ appContainer: { - position: 'absolute', - left: 0, - top: 0, - right: 0, - bottom: 0, + flex: 1, }, }); diff --git a/Libraries/ReactNative/ReactDOM.js b/Libraries/ReactNative/ReactDOM.js new file mode 100644 index 000000000..7ac737c13 --- /dev/null +++ b/Libraries/ReactNative/ReactDOM.js @@ -0,0 +1,16 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule ReactDOM + */ + +'use strict'; + +var ReactUpdates = require('ReactUpdates'); + +// Temporary shim required for ReactTestUtils and Relay. +var ReactDOM = { + unstable_batchedUpdates: ReactUpdates.batchedUpdates, +}; + +module.exports = ReactDOM; diff --git a/Libraries/ReactNative/ReactNative.js b/Libraries/ReactNative/ReactNative.js index 4b09bb667..a5554c0f4 100644 --- a/Libraries/ReactNative/ReactNative.js +++ b/Libraries/ReactNative/ReactNative.js @@ -11,6 +11,10 @@ */ 'use strict'; +// Require ReactNativeDefaultInjection first for its side effects of setting up +// the JS environment +var ReactNativeDefaultInjection = require('ReactNativeDefaultInjection'); + var ReactChildren = require('ReactChildren'); var ReactClass = require('ReactClass'); var ReactComponent = require('ReactComponent'); @@ -18,7 +22,6 @@ var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactElement = require('ReactElement'); var ReactElementValidator = require('ReactElementValidator'); var ReactInstanceHandles = require('ReactInstanceHandles'); -var ReactNativeDefaultInjection = require('ReactNativeDefaultInjection'); var ReactNativeMount = require('ReactNativeMount'); var ReactPropTypes = require('ReactPropTypes'); var ReactUpdates = require('ReactUpdates'); diff --git a/Libraries/ReactNative/ReactNativeDefaultInjection.js b/Libraries/ReactNative/ReactNativeDefaultInjection.js index c95992402..5185af953 100644 --- a/Libraries/ReactNative/ReactNativeDefaultInjection.js +++ b/Libraries/ReactNative/ReactNativeDefaultInjection.js @@ -12,9 +12,12 @@ 'use strict'; /** - * Make sure `setTimeout`/`setInterval` are patched correctly. + * Make sure essential globals are available and are patched correctly. Please don't remove this + * line. Bundles created by react-packager `require` it before executing any application code. This + * ensures it exists in the dependency graph and can be `require`d. */ require('InitializeJavaScriptAppEngine'); + var EventPluginHub = require('EventPluginHub'); var EventPluginUtils = require('EventPluginUtils'); var IOSDefaultEventPluginOrder = require('IOSDefaultEventPluginOrder'); diff --git a/Libraries/ReactNative/ReactNativeMount.js b/Libraries/ReactNative/ReactNativeMount.js index 60e48def1..02dbcb40f 100644 --- a/Libraries/ReactNative/ReactNativeMount.js +++ b/Libraries/ReactNative/ReactNativeMount.js @@ -34,6 +34,10 @@ function instanceNumberToChildRootID(rootNodeID, instanceNumber) { * here. */ var TopLevelWrapper = function() {}; +TopLevelWrapper.prototype.isReactComponent = {}; +if (__DEV__) { + TopLevelWrapper.displayName = 'TopLevelWrapper'; +} TopLevelWrapper.prototype.render = function() { // this.props is actually a ReactElement return this.props; @@ -111,6 +115,8 @@ var ReactNativeMount = { null, null, null, + null, + null, nextElement ); diff --git a/Libraries/ReactNative/ReactNativeTextComponent.js b/Libraries/ReactNative/ReactNativeTextComponent.js index bb93b9c34..a7694c139 100644 --- a/Libraries/ReactNative/ReactNativeTextComponent.js +++ b/Libraries/ReactNative/ReactNativeTextComponent.js @@ -15,6 +15,7 @@ var ReactNativeTagHandles = require('ReactNativeTagHandles'); var RCTUIManager = require('NativeModules').UIManager; var assign = require('Object.assign'); +var invariant = require('invariant'); var ReactNativeTextComponent = function(props) { // This constructor and its argument is currently used by mocks. @@ -30,6 +31,11 @@ assign(ReactNativeTextComponent.prototype, { }, mountComponent: function(rootID, transaction, context) { + invariant( + context.isInAParentText, + 'RawText "' + this._stringText + '" must be wrapped in an explicit ' + + ' component.' + ); this._rootNodeID = rootID; var tag = ReactNativeTagHandles.allocateTag(); var nativeTopRootID = ReactNativeTagHandles.getNativeTopRootIDFromNodeID(rootID); diff --git a/Libraries/Storage/AsyncStorage.android.js b/Libraries/Storage/AsyncStorage.android.js deleted file mode 100644 index 8b2fa312a..000000000 --- a/Libraries/Storage/AsyncStorage.android.js +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in 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/Storage/AsyncStorage.ios.js b/Libraries/Storage/AsyncStorage.js similarity index 83% rename from Libraries/Storage/AsyncStorage.ios.js rename to Libraries/Storage/AsyncStorage.js index ed028db4b..c5be992e2 100644 --- a/Libraries/Storage/AsyncStorage.ios.js +++ b/Libraries/Storage/AsyncStorage.js @@ -12,11 +12,12 @@ 'use strict'; var NativeModules = require('NativeModules'); -var RCTAsyncLocalStorage = NativeModules.AsyncLocalStorage; +var RCTAsyncSQLiteStorage = NativeModules.AsyncSQLiteDBStorage; var RCTAsyncRocksDBStorage = NativeModules.AsyncRocksDBStorage; +var RCTAsyncFileStorage = NativeModules.AsyncLocalStorage; -// We use RocksDB if available. -var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncLocalStorage; +// Use RocksDB if available, then SQLite, then file storage. +var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncSQLiteStorage || RCTAsyncFileStorage; /** * AsyncStorage is a simple, asynchronous, persistent, key-value storage @@ -43,9 +44,10 @@ var AsyncStorage = { RCTAsyncStorage.multiGet([key], function(errors, result) { // Unpack result to get value from [[key,value]] var value = (result && result[0] && result[0][1]) ? result[0][1] : null; - callback && callback((errors && convertError(errors[0])) || null, value); - if (errors) { - reject(convertError(errors[0])); + var errs = convertErrors(errors); + callback && callback(errs && errs[0], value); + if (errs) { + reject(errs[0]); } else { resolve(value); } @@ -64,15 +66,17 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiSet([[key,value]], function(errors) { - callback && callback((errors && convertError(errors[0])) || null); - if (errors) { - reject(convertError(errors[0])); + var errs = convertErrors(errors); + callback && callback(errs && errs[0]); + if (errs) { + reject(errs[0]); } else { resolve(null); } }); }); }, + /** * Returns a `Promise` object. */ @@ -82,9 +86,10 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiRemove([key], function(errors) { - callback && callback((errors && convertError(errors[0])) || null); - if (errors) { - reject(convertError(errors[0])); + var errs = convertErrors(errors); + callback && callback(errs && errs[0]); + if (errs) { + reject(errs[0]); } else { resolve(null); } @@ -93,9 +98,8 @@ var AsyncStorage = { }, /** - * Merges existing value with input value, assuming they are stringified json. Returns a `Promise` object. - * - * Not supported by all native implementations. + * Merges existing value with input value, assuming they are stringified json. + * Returns a `Promise` object. Not supported by all native implementations. */ mergeItem: function( key: string, @@ -104,9 +108,10 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiMerge([[key,value]], function(errors) { - callback && callback((errors && convertError(errors[0])) || null); - if (errors) { - reject(convertError(errors[0])); + var errs = convertErrors(errors); + callback && callback(errs && errs[0]); + if (errs) { + reject(errs[0]); } else { resolve(null); } @@ -170,9 +175,9 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiGet(keys, function(errors, result) { - var error = (errors && errors.map((error) => convertError(error))) || null; + var error = convertErrors(errors); callback && callback(error, result); - if (errors) { + if (error) { reject(error); } else { resolve(result); @@ -193,9 +198,9 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiSet(keyValuePairs, function(errors) { - var error = (errors && errors.map((error) => convertError(error))) || null; + var error = convertErrors(errors); callback && callback(error); - if (errors) { + if (error) { reject(error); } else { resolve(null); @@ -213,9 +218,9 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiRemove(keys, function(errors) { - var error = (errors && errors.map((error) => convertError(error))) || null; + var error = convertErrors(errors); callback && callback(error); - if (errors) { + if (error) { reject(error); } else { resolve(null); @@ -236,9 +241,9 @@ var AsyncStorage = { ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) { - var error = (errors && errors.map((error) => convertError(error))) || null; + var error = convertErrors(errors); callback && callback(error); - if (errors) { + if (error) { reject(error); } else { resolve(null); @@ -254,6 +259,13 @@ if (!RCTAsyncStorage.multiMerge) { delete AsyncStorage.multiMerge; } +function convertErrors(errs) { + if (!errs) { + return null; + } + return (Array.isArray(errs) ? errs : [errs]).map((e) => convertError(e)); +} + function convertError(error) { if (!error) { return null; diff --git a/Libraries/StyleSheet/StyleSheet.js b/Libraries/StyleSheet/StyleSheet.js index 796470ced..aa997b0a4 100644 --- a/Libraries/StyleSheet/StyleSheet.js +++ b/Libraries/StyleSheet/StyleSheet.js @@ -13,6 +13,7 @@ var StyleSheetRegistry = require('StyleSheetRegistry'); var StyleSheetValidation = require('StyleSheetValidation'); +var flattenStyle = require('flattenStyle'); /** * A StyleSheet is an abstraction similar to CSS StyleSheets @@ -59,6 +60,8 @@ var StyleSheetValidation = require('StyleSheetValidation'); * subsequent uses are going to refer an id (not implemented yet). */ class StyleSheet { + static flatten: typeof flattenStyle; + static create(obj: {[key: string]: any}): {[key: string]: number} { var result = {}; for (var key in obj) { @@ -69,4 +72,7 @@ class StyleSheet { } } +/* TODO(brentvatne) docs are needed for this */ +StyleSheet.flatten = flattenStyle; + module.exports = StyleSheet; diff --git a/Libraries/Text/RCTShadowRawText.m b/Libraries/Text/RCTShadowRawText.m index a76cc288b..6d1dd6d25 100644 --- a/Libraries/Text/RCTShadowRawText.m +++ b/Libraries/Text/RCTShadowRawText.m @@ -37,7 +37,7 @@ - (void)setText:(NSString *)text { - if (_text != text) { + if (_text != text && ![_text isEqualToString:text]) { _text = [text copy]; [self dirtyLayout]; [self dirtyText]; diff --git a/Libraries/Text/RCTShadowText.h b/Libraries/Text/RCTShadowText.h index abb111879..1ded47c93 100644 --- a/Libraries/Text/RCTShadowText.h +++ b/Libraries/Text/RCTShadowText.h @@ -32,6 +32,7 @@ extern NSString *const RCTReactTagAttributeName; @property (nonatomic, assign) RCTTextDecorationLineType textDecorationLine; @property (nonatomic, assign) CGFloat fontSizeMultiplier; @property (nonatomic, assign) BOOL allowFontScaling; +@property (nonatomic, assign) CGFloat opacity; - (void)recomputeText; diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index 33e8c8485..d6d6298ae 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -55,6 +55,7 @@ static css_dim_t RCTMeasure(void *context, float width) _letterSpacing = NAN; _isHighlighted = NO; _textDecorationStyle = NSUnderlineStyleSingle; + _opacity = 1.0; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentSizeMultiplierDidChange:) name:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification @@ -80,7 +81,7 @@ static css_dim_t RCTMeasure(void *context, float width) [self dirtyText]; } -- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks +- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties { parentProperties = [super processUpdatedProperties:applierBlocks @@ -99,7 +100,7 @@ static css_dim_t RCTMeasure(void *context, float width) } - (void)applyLayoutNode:(css_node_t *)node - viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame absolutePosition:(CGPoint)absolutePosition { [super applyLayoutNode:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; @@ -152,7 +153,10 @@ static css_dim_t RCTMeasure(void *context, float width) fontWeight:nil fontStyle:nil letterSpacing:nil - useBackgroundColor:NO]; + useBackgroundColor:NO + foregroundColor:self.color ?: [UIColor blackColor] + backgroundColor:self.backgroundColor + opacity:self.opacity]; } - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily @@ -161,6 +165,9 @@ static css_dim_t RCTMeasure(void *context, float width) fontStyle:(NSString *)fontStyle letterSpacing:(NSNumber *)letterSpacing useBackgroundColor:(BOOL)useBackgroundColor + foregroundColor:(UIColor *)foregroundColor + backgroundColor:(UIColor *)backgroundColor + opacity:(CGFloat)opacity { if (![self isTextDirty] && _cachedAttributedString) { return _cachedAttributedString; @@ -188,7 +195,16 @@ static css_dim_t RCTMeasure(void *context, float width) for (RCTShadowView *child in [self reactSubviews]) { if ([child isKindOfClass:[RCTShadowText class]]) { RCTShadowText *shadowText = (RCTShadowText *)child; - [attributedString appendAttributedString:[shadowText _attributedStringWithFontFamily:fontFamily fontSize:fontSize fontWeight:fontWeight fontStyle:fontStyle letterSpacing:letterSpacing useBackgroundColor:YES]]; + [attributedString appendAttributedString: + [shadowText _attributedStringWithFontFamily:fontFamily + fontSize:fontSize + fontWeight:fontWeight + fontStyle:fontStyle + letterSpacing:letterSpacing + useBackgroundColor:YES + foregroundColor:shadowText.color ?: foregroundColor + backgroundColor:shadowText.backgroundColor ?: backgroundColor + opacity:opacity * shadowText.opacity]]; } else if ([child isKindOfClass:[RCTShadowRawText class]]) { RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child; [attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:shadowRawText.text ?: @""]]; @@ -208,14 +224,17 @@ static css_dim_t RCTMeasure(void *context, float width) [child setTextComputed]; } - if (_color) { - [self _addAttribute:NSForegroundColorAttributeName withValue:_color toAttributedString:attributedString]; - } + [self _addAttribute:NSForegroundColorAttributeName + withValue:[foregroundColor colorWithAlphaComponent:CGColorGetAlpha(foregroundColor.CGColor) * opacity] + toAttributedString:attributedString]; + if (_isHighlighted) { [self _addAttribute:RCTIsHighlightedAttributeName withValue:@YES toAttributedString:attributedString]; } - if (useBackgroundColor && self.backgroundColor) { - [self _addAttribute:NSBackgroundColorAttributeName withValue:self.backgroundColor toAttributedString:attributedString]; + if (useBackgroundColor && backgroundColor) { + [self _addAttribute:NSBackgroundColorAttributeName + withValue:[backgroundColor colorWithAlphaComponent:CGColorGetAlpha(backgroundColor.CGColor) * opacity] + toAttributedString:attributedString]; } UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily @@ -278,7 +297,7 @@ static css_dim_t RCTMeasure(void *context, float width) NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; paragraphStyle.alignment = _textAlign; paragraphStyle.baseWritingDirection = _writingDirection; - CGFloat lineHeight = round(_lineHeight * self.fontSizeMultiplier); + CGFloat lineHeight = round(_lineHeight * (_allowFontScaling && self.fontSizeMultiplier > 0.0 ? self.fontSizeMultiplier : 1.0)); paragraphStyle.minimumLineHeight = lineHeight; paragraphStyle.maximumLineHeight = lineHeight; [attributedString addAttribute:NSParagraphStyleAttributeName @@ -352,6 +371,7 @@ RCT_TEXT_PROPERTY(TextDecorationColor, _textDecorationColor, UIColor *); RCT_TEXT_PROPERTY(TextDecorationLine, _textDecorationLine, RCTTextDecorationLineType); RCT_TEXT_PROPERTY(TextDecorationStyle, _textDecorationStyle, NSUnderlineStyle); RCT_TEXT_PROPERTY(WritingDirection, _writingDirection, NSWritingDirection) +RCT_TEXT_PROPERTY(Opacity, _opacity, CGFloat) - (void)setAllowFontScaling:(BOOL)allowFontScaling { diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index 47b7de6a5..c45287917 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -16,7 +16,7 @@ @implementation RCTText { NSTextStorage *_textStorage; - NSMutableArray *_reactSubviews; + NSMutableArray *_reactSubviews; CAShapeLayer *_highlightLayer; } @@ -62,7 +62,7 @@ [_reactSubviews removeObject:subview]; } -- (NSArray *)reactSubviews +- (NSArray *)reactSubviews { return _reactSubviews; } diff --git a/Libraries/Text/RCTTextField.h b/Libraries/Text/RCTTextField.h index 3aba72bba..99eadce98 100644 --- a/Libraries/Text/RCTTextField.h +++ b/Libraries/Text/RCTTextField.h @@ -16,12 +16,17 @@ @property (nonatomic, assign) BOOL caretHidden; @property (nonatomic, assign) BOOL autoCorrect; @property (nonatomic, assign) BOOL selectTextOnFocus; +@property (nonatomic, assign) BOOL blurOnSubmit; @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, strong) UIColor *placeholderTextColor; @property (nonatomic, assign) NSInteger mostRecentEventCount; @property (nonatomic, strong) NSNumber *maxLength; +@property (nonatomic, assign) BOOL textWasPasted; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + - (void)textFieldDidChange; +- (void)sendKeyValueForString:(NSString *)string; +- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField; @end diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m index a75a268e9..6b11bdae5 100644 --- a/Libraries/Text/RCTTextField.m +++ b/Libraries/Text/RCTTextField.m @@ -17,9 +17,10 @@ @implementation RCTTextField { RCTEventDispatcher *_eventDispatcher; - NSMutableArray *_reactSubviews; + NSMutableArray *_reactSubviews; BOOL _jsRequestingFirstResponder; NSInteger _nativeEventCount; + BOOL _submitted; } - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher @@ -39,6 +40,23 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) +- (void)sendKeyValueForString:(NSString *)string +{ + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress + reactTag:self.reactTag + text:nil + key:string + eventCount:_nativeEventCount]; +} + +// This method is overriden for `onKeyPress`. The manager +// will not send a keyPress for text that was pasted. +- (void)paste:(id)sender +{ + _textWasPasted = YES; + [super paste:sender]; +} + - (void)setText:(NSString *)text { NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; @@ -75,7 +93,7 @@ static void RCTUpdatePlaceholder(RCTTextField *self) RCTUpdatePlaceholder(self); } -- (NSArray *)reactSubviews +- (NSArray *)reactSubviews { // TODO: do we support subviews of textfield in React? // In any case, we should have a better approach than manually @@ -134,6 +152,7 @@ static void RCTUpdatePlaceholder(RCTTextField *self) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange reactTag:self.reactTag text:self.text + key:nil eventCount:_nativeEventCount]; } @@ -142,13 +161,16 @@ static void RCTUpdatePlaceholder(RCTTextField *self) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd reactTag:self.reactTag text:self.text + key:nil eventCount:_nativeEventCount]; } - (void)textFieldSubmitEditing { + _submitted = YES; [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit reactTag:self.reactTag text:self.text + key:nil eventCount:_nativeEventCount]; } @@ -162,9 +184,19 @@ static void RCTUpdatePlaceholder(RCTTextField *self) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus reactTag:self.reactTag text:self.text + key:nil eventCount:_nativeEventCount]; } +- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField +{ + if (_submitted) { + _submitted = NO; + return _blurOnSubmit; + } + return YES; +} + - (BOOL)becomeFirstResponder { _jsRequestingFirstResponder = YES; @@ -181,6 +213,7 @@ static void RCTUpdatePlaceholder(RCTTextField *self) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur reactTag:self.reactTag text:self.text + key:nil eventCount:_nativeEventCount]; } return result; diff --git a/Libraries/Text/RCTTextFieldManager.m b/Libraries/Text/RCTTextFieldManager.m index 8ce0b1430..88a59e1bd 100644 --- a/Libraries/Text/RCTTextFieldManager.m +++ b/Libraries/Text/RCTTextFieldManager.m @@ -31,6 +31,13 @@ RCT_EXPORT_MODULE() - (BOOL)textField:(RCTTextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { + // Only allow single keypresses for onKeyPress, pasted text will not be sent. + if (textField.textWasPasted) { + textField.textWasPasted = NO; + } else { + [textField sendKeyValueForString:string]; + } + if (textField.maxLength == nil || [string isEqualToString:@"\n"]) { // Make sure forms can be submitted via return return YES; } @@ -54,6 +61,19 @@ RCT_EXPORT_MODULE() } } +// This method allows us to detect a `Backspace` keyPress +// even when there is no more text in the TextField +- (BOOL)keyboardInputShouldDelete:(RCTTextField *)textField +{ + [self textField:textField shouldChangeCharactersInRange:NSMakeRange(0, 0) replacementString:@""]; + return YES; +} + +- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField +{ + return [textField textFieldShouldEndEditing:textField]; +} + RCT_EXPORT_VIEW_PROPERTY(caretHidden, BOOL) RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL) RCT_REMAP_VIEW_PROPERTY(editable, enabled, BOOL) @@ -64,6 +84,7 @@ RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber) RCT_EXPORT_VIEW_PROPERTY(clearButtonMode, UITextFieldViewMode) RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, clearsOnBeginEditing, BOOL) RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL) +RCT_EXPORT_VIEW_PROPERTY(blurOnSubmit, BOOL) RCT_EXPORT_VIEW_PROPERTY(keyboardType, UIKeyboardType) RCT_EXPORT_VIEW_PROPERTY(returnKeyType, UIReturnKeyType) RCT_EXPORT_VIEW_PROPERTY(enablesReturnKeyAutomatically, BOOL) diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index f7150b46b..be63428fc 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -17,8 +17,16 @@ #import "RCTShadowText.h" #import "RCTSparseArray.h" #import "RCTText.h" +#import "RCTTextView.h" #import "UIView+React.h" +@interface RCTShadowText (Private) + +- (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width; + +@end + + @implementation RCTTextManager RCT_EXPORT_MODULE() @@ -51,9 +59,11 @@ RCT_EXPORT_SHADOW_PROPERTY(textDecorationColor, UIColor) RCT_EXPORT_SHADOW_PROPERTY(textDecorationLine, RCTTextDecorationLineType) RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection) RCT_EXPORT_SHADOW_PROPERTY(allowFontScaling, BOOL) +RCT_EXPORT_SHADOW_PROPERTY(opacity, CGFloat) - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry { + NSMutableSet *textViewTagsToUpdate = [NSMutableSet new]; for (RCTShadowView *rootView in shadowViewRegistry.allObjects) { if (![rootView isReactRootView]) { // This isn't a root view @@ -65,7 +75,7 @@ RCT_EXPORT_SHADOW_PROPERTY(allowFontScaling, BOOL) continue; } - NSMutableArray *queue = [NSMutableArray arrayWithObject:rootView]; + NSMutableArray *queue = [NSMutableArray arrayWithObject:rootView]; for (NSInteger i = 0; i < queue.count; i++) { RCTShadowView *shadowView = queue[i]; RCTAssert([shadowView isTextDirty], @"Don't process any nodes that don't have dirty text"); @@ -77,6 +87,19 @@ RCT_EXPORT_SHADOW_PROPERTY(allowFontScaling, BOOL) RCTLogError(@"Raw text cannot be used outside of a tag. Not rendering string: '%@'", [(RCTShadowRawText *)shadowView text]); } else { + NSNumber *reactTag = shadowView.reactTag; + // This isn't pretty, but hopefully it's temporary + // the problem is, there's no easy way (besides the viewName) + // to tell from the shadowView if the view is an RKTextView + if ([shadowView.viewName hasSuffix:@"TextView"]) { + // Add to textViewTagsToUpdate only if has a RCTShadowText subview + for (RCTShadowView *subview in shadowView.reactSubviews) { + if ([subview isKindOfClass:[RCTShadowText class]]) { + [textViewTagsToUpdate addObject:reactTag]; + break; + } + } + } for (RCTShadowView *child in [shadowView reactSubviews]) { if ([child isTextDirty]) { [queue addObject:child]; @@ -88,7 +111,53 @@ RCT_EXPORT_SHADOW_PROPERTY(allowFontScaling, BOOL) } } - return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {}; + /** + * NOTE: this logic is included to support rich text editing inside multiline + * `` controls, a feature which is not yet supported in open source. + * It is required in order to ensure that the textStorage (aka attributed + * string) is copied over from the RCTShadowText to the RCTText view in time + * to be used to update the editable text content. + */ + if (textViewTagsToUpdate.count) { + + NSMutableArray *uiBlocks = [NSMutableArray new]; + for (NSNumber *reactTag in textViewTagsToUpdate) { + RCTShadowView *shadowTextView = shadowViewRegistry[reactTag]; + RCTShadowText *shadowText; + for (RCTShadowText *subview in shadowTextView.reactSubviews) { + if ([subview isKindOfClass:[RCTShadowText class]]) { + shadowText = subview; + break; + } + } + + UIEdgeInsets padding = shadowText.paddingAsInsets; + CGFloat width = shadowText.frame.size.width - (padding.left + padding.right); + NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:width]; + + [uiBlocks addObject:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTTextView *textView = viewRegistry[reactTag]; + RCTText *text; + for (RCTText *subview in textView.reactSubviews) { + if ([subview isKindOfClass:[RCTText class]]) { + text = subview; + break; + } + } + + text.textStorage = textStorage; + [textView performTextUpdate]; + }]; + } + + return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + for (RCTViewManagerUIBlock uiBlock in uiBlocks) { + uiBlock(uiManager, viewRegistry); + } + }; + } else { + return nil; + } } - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowText *)shadowView diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h index c5012ec09..8fc6d4c28 100644 --- a/Libraries/Text/RCTTextView.h +++ b/Libraries/Text/RCTTextView.h @@ -30,4 +30,6 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; +- (void)performTextUpdate; + @end diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 5e24e3a1c..7ad01b0ce 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -11,9 +11,26 @@ #import "RCTConvert.h" #import "RCTEventDispatcher.h" +#import "RCTText.h" #import "RCTUtils.h" #import "UIView+React.h" +@interface RCTUITextView : UITextView + +@property (nonatomic, assign) BOOL textWasPasted; + +@end + +@implementation RCTUITextView + +- (void)paste:(id)sender +{ + _textWasPasted = YES; + [super paste:sender]; +} + +@end + @implementation RCTTextView { RCTEventDispatcher *_eventDispatcher; @@ -22,6 +39,10 @@ UITextView *_placeholderView; UITextView *_textView; NSInteger _nativeEventCount; + RCTText *_richTextView; + NSAttributedString *_pendingAttributedText; + NSMutableArray *> *_subviews; + BOOL _blockTextShouldChange; } - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher @@ -33,10 +54,12 @@ _eventDispatcher = eventDispatcher; _placeholderTextColor = [self defaultPlaceholderTextColor]; - _textView = [[UITextView alloc] initWithFrame:self.bounds]; + _textView = [[RCTUITextView alloc] initWithFrame:self.bounds]; _textView.backgroundColor = [UIColor clearColor]; _textView.scrollsToTop = NO; _textView.delegate = self; + + _subviews = [NSMutableArray new]; [self addSubview:_textView]; } return self; @@ -45,6 +68,90 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) +- (NSArray *> *)reactSubviews +{ + return _subviews; +} + +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index +{ + if ([subview isKindOfClass:[RCTText class]]) { + if (_richTextView) { + RCTLogError(@"Tried to insert a second into - there can only be one."); + } + _richTextView = (RCTText *)subview; + [_subviews insertObject:_richTextView atIndex:index]; + } else { + [_subviews insertObject:subview atIndex:index]; + [self insertSubview:subview atIndex:index]; + } +} + +- (void)removeReactSubview:(UIView *)subview +{ + if (_richTextView == subview) { + [_subviews removeObject:_richTextView]; + _richTextView = nil; + } else { + [_subviews removeObject:subview]; + [subview removeFromSuperview]; + } +} + +- (void)setMostRecentEventCount:(NSInteger)mostRecentEventCount +{ + _mostRecentEventCount = mostRecentEventCount; + + // Props are set after uiBlockToAmendWithShadowViewRegistry, which means that + // at the time performTextUpdate is called, _mostRecentEventCount will be + // behind _eventCount, with the result that performPendingTextUpdate will do + // nothing. For that reason we call it again here after mostRecentEventCount + // has been set. + [self performPendingTextUpdate]; +} + +- (void)performTextUpdate +{ + if (_richTextView) { + _pendingAttributedText = _richTextView.textStorage; + [self performPendingTextUpdate]; + } else if (!self.text) { + _textView.attributedText = nil; + } +} + +- (void)performPendingTextUpdate +{ + if (!_pendingAttributedText || _mostRecentEventCount < _nativeEventCount) { + return; + } + + if ([_textView.attributedText isEqualToAttributedString:_pendingAttributedText]) { + _pendingAttributedText = nil; // Don't try again. + return; + } + + // When we update the attributed text, there might be pending autocorrections + // that will get accepted by default. In order for this to not garble our text, + // we temporarily block all textShouldChange events so they are not applied. + _blockTextShouldChange = YES; + + // We compute the new selectedRange manually to make sure the cursor is at the + // end of the newly inserted/deleted text after update. + NSRange range = _textView.selectedRange; + CGPoint contentOffset = _textView.contentOffset; + + _textView.attributedText = _pendingAttributedText; + _pendingAttributedText = nil; + _textView.selectedRange = range; + [_textView layoutIfNeeded]; + _textView.contentOffset = contentOffset; + + [self _setPlaceholderVisibility]; + + _blockTextShouldChange = NO; +} + - (void)updateFrames { // Adjust the insets so that they are as close as possible to single-line @@ -56,15 +163,15 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) // first focused. UIEdgeInsets adjustedFrameInset = UIEdgeInsetsZero; adjustedFrameInset.left = _contentInset.left - 5; - + UIEdgeInsets adjustedTextContainerInset = _contentInset; adjustedTextContainerInset.top += 5; adjustedTextContainerInset.left = 0; - + CGRect frame = UIEdgeInsetsInsetRect(self.bounds, adjustedFrameInset); _textView.frame = frame; _placeholderView.frame = frame; - + _textView.textContainerInset = adjustedTextContainerInset; _placeholderView.textContainerInset = adjustedTextContainerInset; } @@ -138,8 +245,22 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) return _textView.text; } -- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +- (BOOL)textView:(RCTUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if (_blockTextShouldChange) { + return NO; + } + + if (textView.textWasPasted) { + textView.textWasPasted = NO; + } else { + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress + reactTag:self.reactTag + text:nil + key:text + eventCount:_nativeEventCount]; + } + if (_maxLength == nil) { return YES; } @@ -215,6 +336,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus reactTag:self.reactTag text:textView.text + key:nil eventCount:_nativeEventCount]; } @@ -225,6 +347,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange reactTag:self.reactTag text:textView.text + key:nil eventCount:_nativeEventCount]; } @@ -234,6 +357,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd reactTag:self.reactTag text:textView.text + key:nil eventCount:_nativeEventCount]; } @@ -253,6 +377,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur reactTag:self.reactTag text:_textView.text + key:nil eventCount:_nativeEventCount]; } return result; diff --git a/Libraries/Utilities/BridgeProfiling.js b/Libraries/Utilities/BridgeProfiling.js index 8ff7583cd..16b6a7159 100644 --- a/Libraries/Utilities/BridgeProfiling.js +++ b/Libraries/Utilities/BridgeProfiling.js @@ -14,7 +14,7 @@ var GLOBAL = GLOBAL || this; var TRACE_TAG_REACT_APPS = 1 << 17; -var _enabled = false; +var _enabled; var _ReactPerf = null; function ReactPerf() { if (!_ReactPerf) { @@ -34,13 +34,13 @@ var BridgeProfiling = { if (_enabled) { profileName = typeof profileName === 'function' ? profileName() : profileName; - console.profile(TRACE_TAG_REACT_APPS, profileName); + global.nativeTraceBeginSection(TRACE_TAG_REACT_APPS, profileName); } }, profileEnd() { if (_enabled) { - console.profileEnd(TRACE_TAG_REACT_APPS); + global.nativeTraceEndSection(TRACE_TAG_REACT_APPS); } }, @@ -63,4 +63,6 @@ var BridgeProfiling = { }, }; +BridgeProfiling.setEnabled(global.__RCTProfileIsProfiling || false); + module.exports = BridgeProfiling; diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index d34bda8b9..415df27c7 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -30,7 +30,6 @@ let MIN_TIME_BETWEEN_FLUSHES_MS = 5; let SPY_MODE = false; let MethodTypes = keyMirror({ - local: null, remote: null, remoteAsync: null, }); @@ -62,15 +61,18 @@ class MessageQueue { 'flushedQueue', ].forEach((fn) => this[fn] = this[fn].bind(this)); - this._genModules(remoteModules); + let modulesConfig = this._genModulesConfig(remoteModules); + this._genModules(modulesConfig); localModules && this._genLookupTables( - localModules, this._moduleTable, this._methodTable); + this._genModulesConfig(localModules),this._moduleTable, this._methodTable + ); this._debugInfo = {}; this._remoteModuleTable = {}; this._remoteMethodTable = {}; this._genLookupTables( - remoteModules, this._remoteModuleTable, this._remoteMethodTable); + modulesConfig, this._remoteModuleTable, this._remoteMethodTable + ); } /** @@ -182,50 +184,102 @@ class MessageQueue { /** * Private helper methods */ - _genLookupTables(localModules, moduleTable, methodTable) { - let moduleNames = Object.keys(localModules); - for (var i = 0, l = moduleNames.length; i < l; i++) { - let moduleName = moduleNames[i]; - let methods = localModules[moduleName].methods; - let moduleID = localModules[moduleName].moduleID; - moduleTable[moduleID] = moduleName; - methodTable[moduleID] = {}; - let methodNames = Object.keys(methods); - for (var j = 0, k = methodNames.length; j < k; j++) { - let methodName = methodNames[j]; - let methodConfig = methods[methodName]; - methodTable[moduleID][methodConfig.methodID] = methodName; + /** + * Converts the old, object-based module structure to the new + * array-based structure. TODO (t8823865) Removed this + * function once Android has been updated. + */ + _genModulesConfig(modules /* array or object */) { + if (Array.isArray(modules)) { + return modules; + } else { + let moduleArray = []; + let moduleNames = Object.keys(modules); + for (var i = 0, l = moduleNames.length; i < l; i++) { + let moduleName = moduleNames[i]; + let moduleConfig = modules[moduleName]; + let module = [moduleName]; + if (moduleConfig.constants) { + module.push(moduleConfig.constants); + } + let methodsConfig = moduleConfig.methods; + if (methodsConfig) { + let methods = []; + let asyncMethods = []; + let methodNames = Object.keys(methodsConfig); + for (var j = 0, ll = methodNames.length; j < ll; j++) { + let methodName = methodNames[j]; + let methodConfig = methodsConfig[methodName]; + methods[methodConfig.methodID] = methodName; + if (methodConfig.type === MethodTypes.remoteAsync) { + asyncMethods.push(methodConfig.methodID); + } + } + if (methods.length) { + module.push(methods); + if (asyncMethods.length) { + module.push(asyncMethods); + } + } + } + moduleArray[moduleConfig.moduleID] = module; } + return moduleArray; } } + _genLookupTables(modulesConfig, moduleTable, methodTable) { + modulesConfig.forEach((module, moduleID) => { + if (!module) { + return; + } + + let moduleName, methods; + if (moduleHasConstants(module)) { + [moduleName, , methods] = module; + } else { + [moduleName, methods] = module; + } + + moduleTable[moduleID] = moduleName; + methodTable[moduleID] = Object.assign({}, methods); + }); + } + _genModules(remoteModules) { - let moduleNames = Object.keys(remoteModules); - for (var i = 0, l = moduleNames.length; i < l; i++) { - let moduleName = moduleNames[i]; - let moduleConfig = remoteModules[moduleName]; + remoteModules.forEach((module, moduleID) => { + if (!module) { + return; + } + + let moduleName, constants, methods, asyncMethods; + if (moduleHasConstants(module)) { + [moduleName, constants, methods, asyncMethods] = module; + } else { + [moduleName, methods, asyncMethods] = module; + } + + const moduleConfig = {moduleID, constants, methods, asyncMethods}; this.RemoteModules[moduleName] = this._genModule({}, moduleConfig); - } + }); } _genModule(module, moduleConfig) { - let methodNames = Object.keys(moduleConfig.methods); - for (var i = 0, l = methodNames.length; i < l; i++) { - let methodName = methodNames[i]; - let methodConfig = moduleConfig.methods[methodName]; - module[methodName] = this._genMethod( - moduleConfig.moduleID, methodConfig.methodID, methodConfig.type); - } - Object.assign(module, moduleConfig.constants); + const {moduleID, constants, methods = [], asyncMethods = []} = moduleConfig; + + methods.forEach((methodName, methodID) => { + const methodType = + arrayContains(asyncMethods, methodID) ? + MethodTypes.remoteAsync : MethodTypes.remote; + module[methodName] = this._genMethod(moduleID, methodID, methodType); + }); + Object.assign(module, constants); + return module; } _genMethod(module, method, type) { - if (type === MethodTypes.local) { - return null; - } - let fn = null; let self = this; if (type === MethodTypes.remoteAsync) { @@ -260,7 +314,15 @@ class MessageQueue { } -function createErrorFromErrorData(errorData: ErrorData): Error { +function moduleHasConstants(moduleArray: Array>): boolean { + return !Array.isArray(moduleArray[1]); +} + +function arrayContains(array: Array, value: T): boolean { + return array.indexOf(value) !== -1; +} + +function createErrorFromErrorData(errorData: {message: string}): Error { var { message, ...extraErrorInfo, diff --git a/Libraries/Utilities/PerformanceLogger.js b/Libraries/Utilities/PerformanceLogger.js index e75611974..75d8478bc 100644 --- a/Libraries/Utilities/PerformanceLogger.js +++ b/Libraries/Utilities/PerformanceLogger.js @@ -14,6 +14,7 @@ var performanceNow = require('performanceNow'); var timespans = {}; +var extras = {}; /** * This is meant to collect and log performance data in production, which means @@ -70,8 +71,19 @@ var PerformanceLogger = { timespans[key].endTime - timespans[key].startTime; }, - clearTimespans() { + clear() { timespans = {}; + extras = {}; + }, + + clearExceptTimespans(keys) { + timespans = Object.keys(timespans).reduce(function(previous, key) { + if (keys.indexOf(key) !== -1) { + previous[key] = timespans[key]; + } + return previous; + }, {}); + extras = {}; }, getTimespans() { @@ -99,6 +111,23 @@ var PerformanceLogger = { label ); } + }, + + setExtra(key, value) { + if (extras[key]) { + if (__DEV__) { + console.log( + 'PerformanceLogger: Attempting to set an extra that already exists ', + key + ); + } + return; + } + extras[key] = value; + }, + + getExtras() { + return extras; } }; diff --git a/Libraries/WebSocket/RCTSRWebSocket.h b/Libraries/WebSocket/RCTSRWebSocket.h index c185daca9..0c209c21c 100644 --- a/Libraries/WebSocket/RCTSRWebSocket.h +++ b/Libraries/WebSocket/RCTSRWebSocket.h @@ -60,11 +60,11 @@ extern NSString *const RCTSRHTTPResponseErrorKey; @property (nonatomic, readonly, copy) NSString *protocol; // Protocols should be an array of strings that turn into Sec-WebSocket-Protocol. -- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols NS_DESIGNATED_INITIALIZER; - (instancetype)initWithURLRequest:(NSURLRequest *)request; // Some helper constructors. -- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; +- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; - (instancetype)initWithURL:(NSURL *)url; // Delegate queue will be dispatch_main_queue by default. diff --git a/Libraries/WebSocket/RCTSRWebSocket.m b/Libraries/WebSocket/RCTSRWebSocket.m index 4085d750c..a345f373a 100644 --- a/Libraries/WebSocket/RCTSRWebSocket.m +++ b/Libraries/WebSocket/RCTSRWebSocket.m @@ -186,7 +186,7 @@ typedef void (^data_callback)(RCTSRWebSocket *webSocket, NSData *data); dispatch_queue_t _delegateDispatchQueue; dispatch_queue_t _workQueue; - NSMutableArray *_consumers; + NSMutableArray *_consumers; NSInputStream *_inputStream; NSOutputStream *_outputStream; @@ -228,12 +228,12 @@ typedef void (^data_callback)(RCTSRWebSocket *webSocket, NSData *data); BOOL _isPumping; - NSMutableSet *_scheduledRunloops; + NSMutableSet *_scheduledRunloops; // We use this to retain ourselves. __strong RCTSRWebSocket *_selfRetain; - NSArray *_requestedProtocols; + NSArray *_requestedProtocols; RCTSRIOConsumerPool *_consumerPool; } @@ -244,7 +244,7 @@ static __strong NSData *CRLFCRLF; CRLFCRLF = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; } -- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; +- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; { RCTAssertParam(request); @@ -271,7 +271,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) return [self initWithURL:URL protocols:nil]; } -- (instancetype)initWithURL:(NSURL *)URL protocols:(NSArray *)protocols; +- (instancetype)initWithURL:(NSURL *)URL protocols:(NSArray *)protocols; { NSURLRequest *request = URL ? [NSURLRequest requestWithURL:URL] : nil; return [self initWithURLRequest:request protocols:protocols]; @@ -1456,7 +1456,7 @@ static const size_t RCTSRFrameHeaderOverhead = 32; @implementation RCTSRIOConsumerPool { NSUInteger _poolSize; - NSMutableArray *_bufferedConsumers; + NSMutableArray *_bufferedConsumers; } - (instancetype)initWithBufferCapacity:(NSUInteger)poolSize; diff --git a/Libraries/WebSocket/RCTWebSocketExecutor.m b/Libraries/WebSocket/RCTWebSocketExecutor.m index 017df74b1..28333d2db 100644 --- a/Libraries/WebSocket/RCTWebSocketExecutor.m +++ b/Libraries/WebSocket/RCTWebSocketExecutor.m @@ -39,7 +39,10 @@ RCT_EXPORT_MODULE() - (instancetype)init { - return [self initWithURL:[RCTConvert NSURL:@"http://localhost:8081/debugger-proxy"]]; + NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults]; + NSInteger port = [standardDefaults integerForKey:@"websocket-executor-port"] ?: 8081; + NSString *URLString = [NSString stringWithFormat:@"http://localhost:%zd/debugger-proxy", port]; + return [self initWithURL:[RCTConvert NSURL:URLString]]; } - (instancetype)initWithURL:(NSURL *)URL diff --git a/Libraries/WebSocket/WebSocket.js b/Libraries/WebSocket/WebSocket.js index a328aa5b4..3ce40bc67 100644 --- a/Libraries/WebSocket/WebSocket.js +++ b/Libraries/WebSocket/WebSocket.js @@ -104,7 +104,7 @@ class WebSocket extends WebSocketBase { this.onclose && this.onclose(event); this.dispatchEvent(event); this._unregisterEvents(); - this._closeWebSocket(id); + this.close(); }), RCTDeviceEventEmitter.addListener('websocketFailed', ev => { if (ev.id !== id) { @@ -115,7 +115,7 @@ class WebSocket extends WebSocketBase { this.onerror && this.onerror(event); this.dispatchEvent(event); this._unregisterEvents(); - this.readyState === this.OPEN && this._closeWebSocket(id); + this.close(); }) ]; } diff --git a/Libraries/WebSocket/WebSocketBase.js b/Libraries/WebSocket/WebSocketBase.js index aa4777c91..ef52352d4 100644 --- a/Libraries/WebSocket/WebSocketBase.js +++ b/Libraries/WebSocket/WebSocketBase.js @@ -44,24 +44,26 @@ class WebSocketBase extends EventTarget { protocols = []; } + this.readyState = this.CONNECTING; this.connectToSocketImpl(url); } close(): void { - if (this.readyState === WebSocketBase.CLOSING || - this.readyState === WebSocketBase.CLOSED) { + if (this.readyState === this.CLOSING || + this.readyState === this.CLOSED) { return; } - if (this.readyState === WebSocketBase.CONNECTING) { + if (this.readyState === this.CONNECTING) { this.cancelConnectionImpl(); } + this.readyState = this.CLOSING; this.closeConnectionImpl(); } send(data: any): void { - if (this.readyState === WebSocketBase.CONNECTING) { + if (this.readyState === this.CONNECTING) { throw new Error('INVALID_STATE_ERR'); } @@ -95,4 +97,9 @@ class WebSocketBase extends EventTarget { } } +WebSocketBase.CONNECTING = 0; +WebSocketBase.OPEN = 1; +WebSocketBase.CLOSING = 2; +WebSocketBase.CLOSED = 3; + module.exports = WebSocketBase; diff --git a/Libraries/WebSocket/__tests__/WebSocket-test.js b/Libraries/WebSocket/__tests__/WebSocket-test.js new file mode 100644 index 000000000..b453eef7b --- /dev/null +++ b/Libraries/WebSocket/__tests__/WebSocket-test.js @@ -0,0 +1,26 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +jest.dontMock('WebSocket'); +jest.dontMock('WebSocketBase'); +jest.setMock('NativeModules', { + WebSocketModule: { + connect: () => {} + } +}); + +var WebSocket = require('WebSocket'); + +describe('WebSocketBase', function() { + + it('should have connection lifecycle constants defined on the class', () => { + expect(WebSocket.CONNECTING).toEqual(0); + }); + + it('should have connection lifecycle constants defined on the instance', () => { + expect(new WebSocket('wss://echo.websocket.org').CONNECTING).toEqual(0); + }); + +}); diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index b461d87f3..0cc6afe3a 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -20,6 +20,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { // Components ActivityIndicatorIOS: require('ActivityIndicatorIOS'), + ART: require('ReactNativeART'), DatePickerIOS: require('DatePickerIOS'), DrawerLayoutAndroid: require('DrawerLayoutAndroid'), Image: require('Image'), @@ -43,6 +44,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { TextInput: require('TextInput'), ToastAndroid: require('ToastAndroid'), ToolbarAndroid: require('ToolbarAndroid'), + Touchable: require('Touchable'), TouchableHighlight: require('TouchableHighlight'), TouchableNativeFeedback: require('TouchableNativeFeedback'), TouchableOpacity: require('TouchableOpacity'), diff --git a/Libraries/vendor/react/event/EventPropagators.js b/Libraries/vendor/react/event/EventPropagators.js deleted file mode 100644 index ad868a5cb..000000000 --- a/Libraries/vendor/react/event/EventPropagators.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * 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 EventPropagators - */ - -"use strict"; - -var EventConstants = require('EventConstants'); -var EventPluginHub = require('EventPluginHub'); - -var accumulateInto = require('accumulateInto'); -var forEachAccumulated = require('forEachAccumulated'); - -var PropagationPhases = EventConstants.PropagationPhases; -var getListener = EventPluginHub.getListener; - -/** - * Some event types have a notion of different registration names for different - * "phases" of propagation. This finds listeners by a given phase. - */ -function listenerAtPhase(id, event, propagationPhase) { - var registrationName = - event.dispatchConfig.phasedRegistrationNames[propagationPhase]; - return getListener(id, registrationName); -} - -/** - * Tags a `SyntheticEvent` with dispatched listeners. Creating this function - * here, allows us to not have to bind or create functions for each event. - * Mutating the event's members allows us to not have to create a wrapping - * "dispatch" object that pairs the event with the listener. - */ -function accumulateDirectionalDispatches(domID, upwards, event) { - if (__DEV__) { - if (!domID) { - throw new Error('Dispatching id must not be null'); - } - } - var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured; - var listener = listenerAtPhase(domID, event, phase); - if (listener) { - event._dispatchListeners = - accumulateInto(event._dispatchListeners, listener); - event._dispatchIDs = accumulateInto(event._dispatchIDs, domID); - } -} - -/** - * Collect dispatches (must be entirely collected before dispatching - see unit - * tests). Lazily allocate the array to conserve memory. We must loop through - * each event and perform the traversal for each one. We can not perform a - * single traversal for the entire collection of events because each event may - * have a different target. - */ -function accumulateTwoPhaseDispatchesSingle(event) { - if (event && event.dispatchConfig.phasedRegistrationNames) { - EventPluginHub.injection.getInstanceHandle().traverseTwoPhase( - event.dispatchMarker, - accumulateDirectionalDispatches, - event - ); - } -} - -/** - * Same as `accumulateTwoPhaseDispatchesSingle`, but skips over the targetID. - */ -function accumulateTwoPhaseDispatchesSingleSkipTarget(event) { - if (event && event.dispatchConfig.phasedRegistrationNames) { - EventPluginHub.injection.getInstanceHandle().traverseTwoPhaseSkipTarget( - event.dispatchMarker, - accumulateDirectionalDispatches, - event - ); - } -} - - -/** - * Accumulates without regard to direction, does not look for phased - * registration names. Same as `accumulateDirectDispatchesSingle` but without - * requiring that the `dispatchMarker` be the same as the dispatched ID. - */ -function accumulateDispatches(id, ignoredDirection, event) { - if (event && event.dispatchConfig.registrationName) { - var registrationName = event.dispatchConfig.registrationName; - var listener = getListener(id, registrationName); - if (listener) { - event._dispatchListeners = - accumulateInto(event._dispatchListeners, listener); - event._dispatchIDs = accumulateInto(event._dispatchIDs, id); - } - } -} - -/** - * Accumulates dispatches on an `SyntheticEvent`, but only for the - * `dispatchMarker`. - * @param {SyntheticEvent} event - */ -function accumulateDirectDispatchesSingle(event) { - if (event && event.dispatchConfig.registrationName) { - accumulateDispatches(event.dispatchMarker, null, event); - } -} - -function accumulateTwoPhaseDispatches(events) { - forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle); -} - -function accumulateTwoPhaseDispatchesSkipTarget(events) { - forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget); -} - -function accumulateEnterLeaveDispatches(leave, enter, fromID, toID) { - EventPluginHub.injection.getInstanceHandle().traverseEnterLeave( - fromID, - toID, - accumulateDispatches, - leave, - enter - ); -} - - -function accumulateDirectDispatches(events) { - forEachAccumulated(events, accumulateDirectDispatchesSingle); -} - - - -/** - * A small set of propagation patterns, each of which will accept a small amount - * of information, and generate a set of "dispatch ready event objects" - which - * are sets of events that have already been annotated with a set of dispatched - * listener functions/ids. The API is designed this way to discourage these - * propagation strategies from actually executing the dispatches, since we - * always want to collect the entire set of dispatches before executing event a - * single one. - * - * @constructor EventPropagators - */ -var EventPropagators = { - accumulateTwoPhaseDispatches: accumulateTwoPhaseDispatches, - accumulateTwoPhaseDispatchesSkipTarget: accumulateTwoPhaseDispatchesSkipTarget, - accumulateDirectDispatches: accumulateDirectDispatches, - accumulateEnterLeaveDispatches: accumulateEnterLeaveDispatches -}; - -module.exports = EventPropagators; diff --git a/README.md b/README.md index 83dc68a6f..28232fb8b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Supported operating systems are >= Android 4.1 (API 16) and >= iOS 7.0. - [Documentation](#documentation) - [Examples](#examples) - [Extending React Native](#extending-react-native) +- [Upgrading](#upgrading) - [Opening Issues](#opening-issues) - [Contributing](#contributing) - [License](#license) @@ -27,6 +28,8 @@ See the official [React Native website](https://facebook.github.io/react-native/ ## Getting Help +Please use these community resources for getting help. We use the GitHub issues for tracking bugs and feature requests and have limited bandwidth to address them. + - Ask a question on [StackOverflow](http://stackoverflow.com/) and tag it with `react-native` - Start a thread on the [React Discussion Board](https://discuss.reactjs.org/) - Join #reactnative on IRC: chat.freenode.net @@ -72,6 +75,10 @@ Note that you'll need the Android NDK installed, see [prerequisites](https://git - 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. +## Upgrading + +React Native is under active development. See the guide on [upgrading React Native](https://facebook.github.io/react-native/docs/upgrading.html) to keep your project up-to-date. + ## 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. diff --git a/React.podspec b/React.podspec index 7c20b2730..d8991c8ed 100644 --- a/React.podspec +++ b/React.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "React" - s.version = "0.8.0" + s.version = "0.15.0" s.summary = "Build high quality mobile apps using React." s.description = <<-DESC React Native apps are built using the React JS @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.preserve_paths = "cli.js", "Libraries/**/*.js", "lint", "linter.js", "node_modules", "package.json", "packager", "PATENTS", "react-native-cli" s.subspec 'Core' do |ss| - ss.source_files = "React/**/*.{c,h,m}" + ss.source_files = "React/**/*.{c,h,m,S}" ss.exclude_files = "**/__tests__/*", "IntegrationTests/*" ss.frameworks = "JavaScriptCore" end @@ -103,11 +103,11 @@ Pod::Spec.new do |s| ss.source_files = "Libraries/LinkingIOS/*.{h,m}" ss.preserve_paths = "Libraries/LinkingIOS/*.js" end - + s.subspec 'RCTTest' do |ss| ss.source_files = "Libraries/RCTTest/**/*.{h,m}" ss.preserve_paths = "Libraries/RCTTest/**/*.js" ss.frameworks = "XCTest" end - + end diff --git a/React/Base/RCTAssert.h b/React/Base/RCTAssert.h index 7914664ad..b109bf81a 100644 --- a/React/Base/RCTAssert.h +++ b/React/Base/RCTAssert.h @@ -11,25 +11,10 @@ #import "RCTDefines.h" -/** - * The default error domain to be used for React errors. - */ -RCT_EXTERN NSString *const RCTErrorDomain; - -/** - * A block signature to be used for custom assertion handling. - */ -typedef void (^RCTAssertFunction)( - NSString *condition, - NSString *fileName, - NSNumber *lineNumber, - NSString *function, - NSString *message -); - /** * This is the main assert macro that you should use. Asserts should be compiled out - * in production builds + * in production builds. You can customize the assert behaviour by setting a custom + * assert handler through `RCTSetAssertFunction`. */ #ifndef NS_BLOCK_ASSERTIONS #define RCTAssert(condition, ...) do { \ @@ -48,21 +33,50 @@ RCT_EXTERN void _RCTAssertFormat( const char *, const char *, int, const char *, NSString *, ... ) NS_FORMAT_FUNCTION(5,6); +/** + * Report a fatal condition when executing. These calls will _NOT_ be compiled out + * in production, and crash the app by default. You can customize the fatal behaviour + * by setting a custom fatal handler through `RCTSetFatalHandler`. + */ +RCT_EXTERN void RCTFatal(NSError *error); + +/** + * The default error domain to be used for React errors. + */ +RCT_EXTERN NSString *const RCTErrorDomain; + +/** + * JS Stack trace provided as part of an NSError's userInfo + */ +RCT_EXTERN NSString *const RCTJSStackTraceKey; + +/** + * A block signature to be used for custom assertion handling. + */ +typedef void (^RCTAssertFunction)( + NSString *condition, + NSString *fileName, + NSNumber *lineNumber, + NSString *function, + NSString *message + ); + +typedef void (^RCTFatalHandler)(NSError *error); + /** * Convenience macro for asserting that a parameter is non-nil/non-zero. */ -#define RCTAssertParam(name) RCTAssert(name, \ -@"'%s' is a required parameter", #name) +#define RCTAssertParam(name) RCTAssert(name, @"'%s' is a required parameter", #name) /** * Convenience macro for asserting that we're running on main thread. */ #define RCTAssertMainThread() RCTAssert([NSThread isMainThread], \ -@"This function must be called on the main thread") + @"This function must be called on the main thread") /** * These methods get and set the current assert function called by the RCTAssert - * macros. You can use these to replace the standard behavior with custom log + * macros. You can use these to replace the standard behavior with custom assert * functionality. */ RCT_EXTERN void RCTSetAssertFunction(RCTAssertFunction assertFunction); @@ -82,11 +96,22 @@ RCT_EXTERN void RCTAddAssertFunction(RCTAssertFunction assertFunction); */ RCT_EXTERN void RCTPerformBlockWithAssertFunction(void (^block)(void), RCTAssertFunction assertFunction); +/** + These methods get and set the current fatal handler called by the RCTFatal method. + */ +RCT_EXTERN void RCTSetFatalHandler(RCTFatalHandler fatalHandler); +RCT_EXTERN RCTFatalHandler RCTGetFatalHandler(void); + /** * Get the current thread's name (or the current queue, if in debug mode) */ RCT_EXTERN NSString *RCTCurrentThreadName(void); +/** + * Helper to get generate exception message from NSError + */ +RCT_EXTERN NSString *RCTFormatError(NSString *message, NSArray *stacktrace, NSUInteger maxMessageLength); + /** * Convenience macro to assert which thread is currently running (DEBUG mode only) */ @@ -106,6 +131,6 @@ _Pragma("clang diagnostic pop") #else -#define RCTAssertThread(thread, format...) +#define RCTAssertThread(thread, format...) do { } while (0) #endif diff --git a/React/Base/RCTAssert.m b/React/Base/RCTAssert.m index 80bb6d65b..872920834 100644 --- a/React/Base/RCTAssert.m +++ b/React/Base/RCTAssert.m @@ -8,12 +8,15 @@ */ #import "RCTAssert.h" +#import "RCTLog.h" NSString *const RCTErrorDomain = @"RCTErrorDomain"; +NSString *const RCTJSStackTraceKey = @"RCTJSStackTraceKey"; static NSString *const RCTAssertFunctionStack = @"RCTAssertFunctionStack"; RCTAssertFunction RCTCurrentAssertFunction = nil; +RCTFatalHandler RCTCurrentFatalHandler = nil; NSException *_RCTNotImplementedException(SEL, Class); NSException *_RCTNotImplementedException(SEL cmd, Class cls) @@ -59,7 +62,7 @@ void RCTAddAssertFunction(RCTAssertFunction assertFunction) static RCTAssertFunction RCTGetLocalAssertFunction() { NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; - NSArray *functionStack = threadDictionary[RCTAssertFunctionStack]; + NSArray *functionStack = threadDictionary[RCTAssertFunctionStack]; RCTAssertFunction assertFunction = functionStack.lastObject; if (assertFunction) { return assertFunction; @@ -70,7 +73,7 @@ static RCTAssertFunction RCTGetLocalAssertFunction() void RCTPerformBlockWithAssertFunction(void (^block)(void), RCTAssertFunction assertFunction) { NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; - NSMutableArray *functionStack = threadDictionary[RCTAssertFunctionStack]; + NSMutableArray *functionStack = threadDictionary[RCTAssertFunctionStack]; if (!functionStack) { functionStack = [NSMutableArray new]; threadDictionary[RCTAssertFunctionStack] = functionStack; @@ -112,3 +115,49 @@ void _RCTAssertFormat( assertFunction(@(condition), @(fileName), @(lineNumber), @(function), message); } } + +void RCTFatal(NSError *error) +{ + _RCTLogInternal(RCTLogLevelFatal, NULL, 0, @"%@", [error localizedDescription]); + + RCTFatalHandler fatalHandler = RCTGetFatalHandler(); + if (fatalHandler) { + fatalHandler(error); + } else { +#if DEBUG + @try { +#endif + NSString *message = RCTFormatError([error localizedDescription], error.userInfo[RCTJSStackTraceKey], 75); + [NSException raise:@"RCTFatalException" format:@"%@", message]; +#if DEBUG + } @catch (NSException *e) {} +#endif + } +} + +void RCTSetFatalHandler(RCTFatalHandler fatalhandler) +{ + RCTCurrentFatalHandler = fatalhandler; +} + +RCTFatalHandler RCTGetFatalHandler(void) +{ + return RCTCurrentFatalHandler; +} + +NSString *RCTFormatError(NSString *message, NSArray *stackTrace, NSUInteger maxMessageLength) +{ + if (maxMessageLength > 0 && message.length > maxMessageLength) { + message = [[message substringToIndex:maxMessageLength] stringByAppendingString:@"..."]; + } + + NSMutableString *prettyStack = [NSMutableString string]; + if (stackTrace) { + [prettyStack appendString:@", stack:\n"]; + for (NSDictionary *frame in stackTrace) { + [prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]]; + } + } + + return [NSString stringWithFormat:@"Message: %@%@", message, prettyStack]; +} diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index aedb23008..871d54eac 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -11,6 +11,7 @@ #import "RCTAssert.h" #import "RCTBridge.h" +#import "RCTBridgeMethod.h" #import "RCTConvert.h" #import "RCTContextExecutor.h" #import "RCTFrameUpdate.h" @@ -18,11 +19,8 @@ #import "RCTLog.h" #import "RCTModuleData.h" #import "RCTModuleMap.h" -#import "RCTBridgeMethod.h" #import "RCTPerformanceLogger.h" -#import "RCTPerfStats.h" #import "RCTProfile.h" -#import "RCTRedBox.h" #import "RCTSourceCode.h" #import "RCTSparseArray.h" #import "RCTUtils.h" @@ -44,7 +42,7 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCTBridgeFieldParamss, }; -RCT_EXTERN NSArray *RCTGetModuleClasses(void); +RCT_EXTERN NSArray *RCTGetModuleClasses(void); @interface RCTBridge () @@ -65,12 +63,11 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); BOOL _valid; BOOL _wasBatchActive; __weak id _javaScriptExecutor; - NSMutableArray *_pendingCalls; - NSMutableArray *_moduleDataByID; + NSMutableArray *_pendingCalls; + NSMutableArray *_moduleDataByID; RCTModuleMap *_modulesByName; - CADisplayLink *_mainDisplayLink; CADisplayLink *_jsDisplayLink; - NSMutableSet *_frameUpdateObservers; + NSMutableSet *_frameUpdateObservers; } - (instancetype)initWithParentBridge:(RCTBridge *)bridge @@ -94,11 +91,6 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); _frameUpdateObservers = [NSMutableSet new]; _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; - if (RCT_DEV) { - _mainDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_mainThreadUpdate:)]; - [_mainDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; - } - [RCTBridge setCurrentBridge:self]; [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptWillStartLoadingNotification @@ -183,7 +175,7 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); - (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad { RCTPerformanceLoggerStart(RCTPLScriptDownload); - int cookie = RCTProfileBeginAsyncEvent(0, @"JavaScript download", nil); + NSUInteger cookie = RCTProfileBeginAsyncEvent(0, @"JavaScript download", nil); RCTSourceLoadBlock onSourceLoad = ^(NSError *error, NSData *source) { RCTProfileEndAsyncEvent(0, @"init,download", cookie, @"JavaScript download", nil); @@ -197,13 +189,20 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); // Force JS __DEV__ value to match RCT_DEBUG if (shouldOverrideDev) { NSString *sourceString = [[NSString alloc] initWithData:source encoding:NSUTF8StringEncoding]; - NSRange range = [sourceString rangeOfString:@"__DEV__="]; + NSRange range = [sourceString rangeOfString:@"\\b__DEV__\\s*?=\\s*?(!1|!0|false|true)" + options:NSRegularExpressionSearch]; + RCTAssert(range.location != NSNotFound, @"It looks like the implementation" "of __DEV__ has changed. Update -[RCTBatchedBridge loadSource:]."); - NSRange valueRange = {range.location + range.length, 2}; - if ([[sourceString substringWithRange:valueRange] isEqualToString:@"!1"]) { - source = [[sourceString stringByReplacingCharactersInRange:valueRange withString:@" 1"] dataUsingEncoding:NSUTF8StringEncoding]; + + NSString *valueString = [sourceString substringWithRange:range]; + if ([valueString rangeOfString:@"!1"].length) { + valueString = [valueString stringByReplacingOccurrencesOfString:@"!1" withString:@"!0"]; + } else if ([valueString rangeOfString:@"false"].length) { + valueString = [valueString stringByReplacingOccurrencesOfString:@"false" withString:@"true"]; } + source = [[sourceString stringByReplacingCharactersInRange:range withString:valueString] + dataUsingEncoding:NSUTF8StringEncoding]; } _onSourceLoad(error, source); @@ -233,7 +232,7 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); // Register passed-in module instances NSMutableDictionary *preregisteredModules = [NSMutableDictionary new]; - NSArray *extraModules = nil; + NSArray> *extraModules = nil; if (self.delegate) { if ([self.delegate respondsToSelector:@selector(extraModulesForBridge:)]) { extraModules = [self.delegate extraModulesForBridge:_parentBridge]; @@ -250,7 +249,7 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); _moduleDataByID = [NSMutableArray new]; NSMutableDictionary *modulesByName = [preregisteredModules mutableCopy]; for (Class moduleClass in RCTGetModuleClasses()) { - NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); + NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); // Check if module instance has already been registered for this name id module = modulesByName[moduleName]; @@ -308,12 +307,11 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); - (NSString *)moduleConfig { - NSMutableDictionary *config = [NSMutableDictionary new]; + NSMutableArray *config = [NSMutableArray new]; for (RCTModuleData *moduleData in _moduleDataByID) { - config[moduleData.name] = moduleData.config; + [config addObject:moduleData.config]; if ([moduleData.instance conformsToProtocol:@protocol(RCTFrameUpdateObserver)]) { [_frameUpdateObservers addObject:moduleData]; - id observer = (id)moduleData.instance; __weak typeof(self) weakSelf = self; __weak typeof(_javaScriptExecutor) weakJavaScriptExecutor = _javaScriptExecutor; @@ -417,17 +415,10 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); _loading = NO; - NSArray *stack = error.userInfo[@"stack"]; - if (stack) { - [self.redBox showErrorMessage:error.localizedDescription withStack:stack]; - } else { - [self.redBox showError:error]; - } - - NSDictionary *userInfo = @{@"bridge": self, @"error": error}; [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification object:_parentBridge - userInfo:userInfo]; + userInfo:@{@"bridge": self, @"error": error}]; + RCTFatal(error); } RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleURL @@ -507,9 +498,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR [RCTBridge setCurrentBridge:nil]; } - [_mainDisplayLink invalidate]; - _mainDisplayLink = nil; - // Invalidate modules dispatch_group_t group = dispatch_group_create(); for (RCTModuleData *moduleData in _moduleDataByID) { @@ -561,7 +549,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR */ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args { - NSArray *ids = [moduleDotMethod componentsSeparatedByString:@"."]; + NSArray *ids = [moduleDotMethod componentsSeparatedByString:@"."]; [self _invokeAndProcessModule:@"BatchedBridge" method:@"callFunctionReturnFlushedQueue" @@ -639,7 +627,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR __weak RCTBatchedBridge *weakSelf = self; [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ RCTProfileEndFlowEvent(); - RCTProfileBeginEvent(0, @"enqueue_call", nil); RCTBatchedBridge *strongSelf = weakSelf; if (!strongSelf || !strongSelf.valid) { @@ -664,7 +651,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR RCTJavaScriptCallback processResponse = ^(id json, NSError *error) { if (error) { - [self.redBox showError:error]; + RCTFatal(error); } if (!self.isValid) { @@ -700,62 +687,54 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR } } -- (void)handleBuffer:(id)buffer +- (void)handleBuffer:(NSArray *)buffer { - NSArray *requestsArray = [RCTConvert NSArray:buffer]; + NSArray *requestsArray = [RCTConvert NSArrayArray:buffer]; -#if RCT_DEBUG - - if (![buffer isKindOfClass:[NSArray class]]) { - RCTLogError(@"Buffer must be an instance of NSArray, got %@", NSStringFromClass([buffer class])); + if (RCT_DEBUG && requestsArray.count <= RCTBridgeFieldParamss) { + RCTLogError(@"Buffer should contain at least %tu sub-arrays. Only found %tu", + RCTBridgeFieldParamss + 1, requestsArray.count); return; } - for (NSUInteger fieldIndex = RCTBridgeFieldRequestModuleIDs; fieldIndex <= RCTBridgeFieldParamss; fieldIndex++) { - id field = requestsArray[fieldIndex]; - if (![field isKindOfClass:[NSArray class]]) { - RCTLogError(@"Field at index %zd in buffer must be an instance of NSArray, got %@", fieldIndex, NSStringFromClass([field class])); - return; - } - } + NSArray *moduleIDs = requestsArray[RCTBridgeFieldRequestModuleIDs]; + NSArray *methodIDs = requestsArray[RCTBridgeFieldMethodIDs]; + NSArray *paramsArrays = requestsArray[RCTBridgeFieldParamss]; -#endif - - NSArray *moduleIDs = requestsArray[RCTBridgeFieldRequestModuleIDs]; - NSArray *methodIDs = requestsArray[RCTBridgeFieldMethodIDs]; - NSArray *paramsArrays = requestsArray[RCTBridgeFieldParamss]; - - NSUInteger numRequests = moduleIDs.count; - - if (RCT_DEBUG && (numRequests != methodIDs.count || numRequests != paramsArrays.count)) { - RCTLogError(@"Invalid data message - all must be length: %zd", numRequests); + if (RCT_DEBUG && (moduleIDs.count != methodIDs.count || moduleIDs.count != paramsArrays.count)) { + RCTLogError(@"Invalid data message - all must be length: %zd", moduleIDs.count); return; } NSMapTable *buckets = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsStrongMemory capacity:_moduleDataByID.count]; - for (NSUInteger i = 0; i < numRequests; i++) { - RCTModuleData *moduleData = _moduleDataByID[[moduleIDs[i] integerValue]]; + + [moduleIDs enumerateObjectsUsingBlock:^(NSNumber *moduleID, NSUInteger i, __unused BOOL *stop) { + RCTModuleData *moduleData = _moduleDataByID[moduleID.integerValue]; if (RCT_DEBUG) { // verify that class has been registered (void)_modulesByName[moduleData.name]; } - id queue = [moduleData queue]; - NSMutableOrderedSet *set = [buckets objectForKey:queue]; + dispatch_queue_t queue = moduleData.queue; + NSMutableOrderedSet *set = [buckets objectForKey:queue]; if (!set) { set = [NSMutableOrderedSet new]; [buckets setObject:set forKey:queue]; } [set addObject:@(i)]; - } + }]; - for (id queue in buckets) { + for (dispatch_queue_t queue in buckets) { RCTProfileBeginFlowEvent(); dispatch_block_t block = ^{ RCTProfileEndFlowEvent(); - RCTProfileBeginEvent(0, RCTCurrentThreadName(), nil); + +#if RCT_DEV + NSString *_threadName = RCTCurrentThreadName(); + RCTProfileBeginEvent(0, _threadName, nil); +#endif NSOrderedSet *calls = [buckets objectForKey:queue]; @autoreleasepool { @@ -807,8 +786,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR return NO; } - RCTProfileBeginEvent(0, @"Invoke callback", nil); - RCTModuleData *moduleData = _moduleDataByID[moduleID]; if (RCT_DEBUG && !moduleData) { RCTLogError(@"No module found for id '%zd'", moduleID); @@ -821,21 +798,29 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR return NO; } + RCTProfileBeginEvent(0, [NSString stringWithFormat:@"[%@ %@]", moduleData.name, method.JSMethodName], nil); + @try { [method invokeWithBridge:self module:moduleData.instance arguments:params]; } @catch (NSException *exception) { - RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, moduleData.name, params, exception); - if (!RCT_DEBUG && [exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { + // Pass on JS exceptions + if ([exception.name rangeOfString:@"Unhandled JS Exception"].location == 0) { @throw exception; } + + NSString *message = [NSString stringWithFormat: + @"Exception thrown while invoking %@ on target %@ with params %@: %@", + method.JSMethodName, moduleData.name, params, exception]; + RCTFatal(RCTErrorWithMessage(message)); } - NSMutableDictionary *args = [method.profileArgs mutableCopy]; - [args setValue:method.JSMethodName forKey:@"method"]; - [args setValue:RCTJSONStringify(RCTNullIfNil(params), NULL) forKey:@"args"]; - - RCTProfileEndEvent(0, @"objc_call", args); + if (RCTProfileIsProfiling()) { + NSMutableDictionary *args = [method.profileArgs mutableCopy]; + args[@"method"] = method.JSMethodName; + args[@"args"] = RCTJSONStringify(RCTNullIfNil(params), NULL); + RCTProfileEndEvent(0, @"objc_call", args); + } return YES; } @@ -867,21 +852,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR RCTProfileImmediateEvent(0, @"JS Thread Tick", 'g'); RCTProfileEndEvent(0, @"objc_call", nil); - - RCT_IF_DEV( - dispatch_async(dispatch_get_main_queue(), ^{ - [self.perfStats.jsGraph onTick:displayLink.timestamp]; - }); - ) -} - -- (void)_mainThreadUpdate:(CADisplayLink *)displayLink -{ - RCTAssertMainThread(); - - RCTProfileImmediateEvent(0, @"VSYNC", 'g'); - - _modulesByName == nil ?: [self.perfStats.uiGraph onTick:displayLink.timestamp]; } - (void)startProfiling @@ -898,9 +868,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR RCTAssertMainThread(); [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ - NSString *log = RCTProfileEnd(self); - NSData *logData = [log dataUsingEncoding:NSUTF8StringEncoding]; - callback(logData); + RCTProfileEnd(self, ^(NSString *log) { + NSData *logData = [log dataUsingEncoding:NSUTF8StringEncoding]; + callback(logData); + }); }]; } diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 88bcf8971..c16786b45 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -52,7 +52,7 @@ RCT_EXTERN NSString *const RCTDidCreateNativeModules; * For this reason, the block should always return new module instances, and * module instances should not be shared between bridges. */ -typedef NSArray *(^RCTBridgeModuleProviderBlock)(void); +typedef NSArray> *(^RCTBridgeModuleProviderBlock)(void); /** * This function returns the module name for a given class. diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 4334be311..15f85c8da 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -11,6 +11,7 @@ #import +#import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "RCTKeyCommands.h" #import "RCTLog.h" @@ -39,9 +40,9 @@ NSString *const RCTDidCreateNativeModules = @"RCTDidCreateNativeModules"; @end -static NSMutableArray *RCTModuleClasses; -NSArray *RCTGetModuleClasses(void); -NSArray *RCTGetModuleClasses(void) +static NSMutableArray *RCTModuleClasses; +NSArray *RCTGetModuleClasses(void); +NSArray *RCTGetModuleClasses(void) { return RCTModuleClasses; } @@ -250,6 +251,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) RCTAssertMainThread(); _bundleURL = [self.delegate sourceURLForBridge:self] ?: _bundleURL; + + // Sanitize the bundle URL + _bundleURL = [RCTConvert NSURL:_bundleURL.absoluteString]; + _batchedBridge = [[RCTBatchedBridge alloc] initWithParentBridge:self]; } @@ -284,8 +289,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) #define RCT_INNER_BRIDGE_ONLY(...) \ - (void)__VA_ARGS__ \ { \ - RCTLogMustFix(@"Called method \"%@\" on top level bridge. This method should \ - only be called from bridge instance in a bridge module", @(__func__)); \ + NSString *errorMessage = [NSString stringWithFormat:@"Called method \"%@\" on top level bridge. \ + This method should oly be called from bridge instance in a bridge module", @(__func__)]; \ + RCTFatal(RCTErrorWithMessage(errorMessage)); \ } - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args diff --git a/React/Base/RCTBridgeDelegate.h b/React/Base/RCTBridgeDelegate.h index a8a3c937f..d8606c8d9 100644 --- a/React/Base/RCTBridgeDelegate.h +++ b/React/Base/RCTBridgeDelegate.h @@ -13,11 +13,38 @@ typedef void (^RCTSourceLoadBlock)(NSError *error, NSData *source); @protocol RCTBridgeDelegate +/** + * The location of the JavaScript source file. When running from the packager + * this should be an absolute URL, e.g. `http://localhost:8081/index.ios.bundle`. + * When running from a locally bundled JS file, this should be a `file://` url + * pointing to a path inside the app resources, e.g. `file://.../main.jsbundle`. + */ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge; @optional +/** + * The bridge initializes any registered RCTBridgeModules automatically, however + * if you wish to instantiate your own module instances, you can return them + * from this method. + * + * Note: You should always return a new instance for each call, rather than + * returning the same instance each time the bridge is reloaded. Module instances + * should not be shared between bridges, and this may cause unexpected behavior. + * + * It is also possible to override standard modules with your own implementations + * by returning a class with the same `moduleName` from this method, but this is + * not recommended in most cases - if the module methods and behavior do not + * match exactly, it may lead to bugs or crashes. + */ - (NSArray *)extraModulesForBridge:(RCTBridge *)bridge; -- (void)loadSourceForBridge:(RCTBridge *)bridge withBlock:(RCTSourceLoadBlock)loadCallback; + +/** + * The bridge will automatically attempt to load the JS source code from the + * location specified by the `sourceURLForBridge:` method, however, if you want + * to handle loading the JS yourself, you can do so by implementing this method. + */ +- (void)loadSourceForBridge:(RCTBridge *)bridge + withBlock:(RCTSourceLoadBlock)loadCallback; @end diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h index 93f8f6e98..07c6932b5 100644 --- a/React/Base/RCTBridgeModule.h +++ b/React/Base/RCTBridgeModule.h @@ -12,6 +12,7 @@ #import "RCTDefines.h" @class RCTBridge; +@protocol RCTBridgeMethod; /** * The type of a block that is capable of sending a response to a bridged @@ -208,7 +209,8 @@ RCT_EXTERN void RCTRegisterModule(Class); \ * Like RCT_EXTERN_REMAP_METHOD, but allows setting a custom JavaScript name. */ #define RCT_EXTERN_REMAP_METHOD(js_name, method) \ - + (NSArray *)RCT_CONCAT(__rct_export__, RCT_CONCAT(js_name, RCT_CONCAT(__LINE__, __COUNTER__))) { \ + + (NSArray *)RCT_CONCAT(__rct_export__, \ + RCT_CONCAT(js_name, RCT_CONCAT(__LINE__, __COUNTER__))) { \ return @[@#js_name, @#method]; \ } @@ -217,7 +219,7 @@ RCT_EXTERN void RCTRegisterModule(Class); \ * methods defined using the macros above. This method is called only once, * before registration. */ -- (NSArray *)methodsToExport; +- (NSArray> *)methodsToExport; /** * Injects constants into JS. These constants are made accessible via diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index cad32b860..7dbcab6d8 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -56,6 +56,7 @@ typedef NSURL RCTFileURL; + (NSTimeZone *)NSTimeZone:(id)json; + (NSTimeInterval)NSTimeInterval:(id)json; ++ (NSLineBreakMode)NSLineBreakMode:(id)json; + (NSTextAlignment)NSTextAlignment:(id)json; + (NSUnderlineStyle)NSUnderlineStyle:(id)json; + (NSWritingDirection)NSWritingDirection:(id)json; @@ -95,25 +96,28 @@ typedef NSURL RCTFileURL; scaleMultiplier:(CGFloat)scaleMultiplier; typedef NSArray NSArrayArray; -+ (NSArrayArray *)NSArrayArray:(id)json; ++ (NSArray *)NSArrayArray:(id)json; typedef NSArray NSStringArray; -+ (NSStringArray *)NSStringArray:(id)json; ++ (NSArray *)NSStringArray:(id)json; + +typedef NSArray NSStringArrayArray; ++ (NSArray *> *)NSStringArrayArray:(id)json; typedef NSArray NSDictionaryArray; -+ (NSDictionaryArray *)NSDictionaryArray:(id)json; ++ (NSArray *)NSDictionaryArray:(id)json; typedef NSArray NSURLArray; -+ (NSURLArray *)NSURLArray:(id)json; ++ (NSArray *)NSURLArray:(id)json; typedef NSArray RCTFileURLArray; -+ (RCTFileURLArray *)RCTFileURLArray:(id)json; ++ (NSArray *)RCTFileURLArray:(id)json; typedef NSArray NSNumberArray; -+ (NSNumberArray *)NSNumberArray:(id)json; ++ (NSArray *)NSNumberArray:(id)json; typedef NSArray UIColorArray; -+ (UIColorArray *)UIColorArray:(id)json; ++ (NSArray *)UIColorArray:(id)json; typedef NSArray CGColorArray; + (CGColorArray *)CGColorArray:(id)json; @@ -222,7 +226,7 @@ RCT_CUSTOM_CONVERTER(type, type, [RCT_DEBUG ? [self NSNumber:json] : json getter * This macro is used for creating converter functions for typed arrays. */ #define RCT_ARRAY_CONVERTER(type) \ -+ (NSArray *)type##Array:(id)json \ ++ (NSArray *)type##Array:(id)json \ { \ return RCTConvertArrayValue(@selector(type:), json); \ } diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 88e7e3f51..fb6ef99dd 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -101,7 +101,7 @@ RCT_CUSTOM_CONVERTER(NSData *, NSData, [json dataUsingEncoding:NSUTF8StringEncod path = path.stringByExpandingTildeInPath; } else if (!path.absolutePath) { // Assume it's a resource path - path = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:path]; + path = [[NSBundle mainBundle].resourcePath stringByAppendingPathComponent:path]; } if (!(URL = [NSURL fileURLWithPath:path])) { RCTLogConvertError(json, @"a valid URL"); @@ -205,6 +205,15 @@ NSNumber *RCTConvertMultiEnumValue(const char *typeName, NSDictionary *mapping, return RCTConvertEnumValue(typeName, mapping, defaultValue, json); } +RCT_ENUM_CONVERTER(NSLineBreakMode, (@{ + @"wordWrapping": @(NSLineBreakByWordWrapping), + @"charWrapping": @(NSLineBreakByCharWrapping), + @"clipping": @(NSLineBreakByClipping), + @"truncatingHead": @(NSLineBreakByTruncatingHead), + @"truncatingTail": @(NSLineBreakByTruncatingTail), + @"truncatingMiddle": @(NSLineBreakByTruncatingMiddle), +}), NSLineBreakByWordWrapping, integerValue) + RCT_ENUM_CONVERTER(NSTextAlignment, (@{ @"auto": @(NSTextAlignmentNatural), @"left": @(NSTextAlignmentLeft), @@ -702,6 +711,7 @@ RCT_ARRAY_CONVERTER(UIColor) RCT_JSON_ARRAY_CONVERTER(NSArray) RCT_JSON_ARRAY_CONVERTER(NSString) +RCT_JSON_ARRAY_CONVERTER(NSStringArray) RCT_JSON_ARRAY_CONVERTER(NSDictionary) RCT_JSON_ARRAY_CONVERTER(NSNumber) diff --git a/React/Base/RCTDefines.h b/React/Base/RCTDefines.h index 2e4fb3f61..afc0320c3 100644 --- a/React/Base/RCTDefines.h +++ b/React/Base/RCTDefines.h @@ -7,7 +7,9 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import +#if __OBJC__ +# import +#endif /** * Make global functions usable in C++ diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index 114d91586..4cbd84fa2 100644 --- a/React/Base/RCTEventDispatcher.h +++ b/React/Base/RCTEventDispatcher.h @@ -16,7 +16,8 @@ typedef NS_ENUM(NSInteger, RCTTextEventType) { RCTTextEventTypeBlur, RCTTextEventTypeChange, RCTTextEventTypeSubmit, - RCTTextEventTypeEnd + RCTTextEventTypeEnd, + RCTTextEventTypeKeyPress }; typedef NS_ENUM(NSInteger, RCTScrollEventType) { @@ -95,6 +96,7 @@ RCT_EXTERN NSString *RCTNormalizeInputEventName(NSString *eventName); - (void)sendTextEventWithType:(RCTTextEventType)type reactTag:(NSNumber *)reactTag text:(NSString *)text + key:(NSString *)key eventCount:(NSInteger)eventCount; /** diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index 48f98e457..d8d6f3268 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -144,6 +144,7 @@ RCT_EXPORT_MODULE() - (void)sendTextEventWithType:(RCTTextEventType)type reactTag:(NSNumber *)reactTag text:(NSString *)text + key:(NSString *)key eventCount:(NSInteger)eventCount { static NSString *events[] = { @@ -152,16 +153,36 @@ RCT_EXPORT_MODULE() @"change", @"submitEditing", @"endEditing", + @"keyPress" }; - [self sendInputEventWithName:events[type] body:text ? @{ - @"text": text, - @"eventCount": @(eventCount), - @"target": reactTag - } : @{ + NSMutableDictionary *body = [[NSMutableDictionary alloc] initWithDictionary:@{ @"eventCount": @(eventCount), @"target": reactTag }]; + + if (text) { + body[@"text"] = text; + } + + if (key) { + if (key.length == 0) { + key = @"Backspace"; // backspace + } else { + switch ([key characterAtIndex:0]) { + case '\t': + key = @"Tab"; + break; + case '\n': + key = @"Enter"; + default: + break; + } + } + body[@"key"] = key; + } + + [self sendInputEventWithName:events[type] body:body]; } - (void)sendEvent:(id)event @@ -188,7 +209,7 @@ RCT_EXPORT_MODULE() - (void)dispatchEvent:(id)event { - NSMutableArray *arguments = [NSMutableArray new]; + NSMutableArray *arguments = [NSMutableArray new]; if (event.viewTag) { [arguments addObject:event.viewTag]; @@ -200,8 +221,7 @@ RCT_EXPORT_MODULE() [arguments addObject:event.body]; } - [_bridge enqueueJSCall:[[event class] moduleDotMethod] - args:arguments]; + [_bridge enqueueJSCall:[[event class] moduleDotMethod] args:arguments]; } - (dispatch_queue_t)methodQueue diff --git a/React/Base/RCTFPSGraph.m b/React/Base/RCTFPSGraph.m deleted file mode 100644 index 1cd33432f..000000000 --- a/React/Base/RCTFPSGraph.m +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTFPSGraph.h" - -#import "RCTAssert.h" -#import "RCTDefines.h" - -#if RCT_DEV - -@implementation RCTFPSGraph -{ - CAShapeLayer *_graph; - NSString *_name; - NSTimeInterval _prevTime; - RCTFPSGraphPosition _position; - UILabel *_label; - - float *_frames; - int _frameCount; - int _maxFPS; - int _minFPS; - int _length; - int _margin; - int _height; -} - -- (instancetype)initWithFrame:(CGRect)frame graphPosition:(RCTFPSGraphPosition)position name:(NSString *)name color:(UIColor *)color -{ - if ((self = [super initWithFrame:frame])) { - _margin = 2; - _prevTime = -1; - _maxFPS = 0; - _minFPS = 60; - _length = (frame.size.width - 2 * _margin) / 2; - _height = frame.size.height - 2 * _margin; - _frames = malloc(sizeof(float) * _length); - memset(_frames, 0, sizeof(float) * _length); - - _name = name ?: @"FPS"; - _position = position ?: RCTFPSGraphPositionLeft; - - color = color ?: [UIColor greenColor]; - _graph = [self createGraph:color]; - _label = [self createLabel:color]; - - [self addSubview:_label]; - [self.layer addSublayer:_graph]; - } - return self; -} - -RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) -RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - -- (void)dealloc -{ - free(_frames); -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; -} - -- (CAShapeLayer *)createGraph:(UIColor *)color -{ - CGFloat left = _position & RCTFPSGraphPositionLeft ? 0 : _length; - CAShapeLayer *graph = [CAShapeLayer new]; - graph.frame = CGRectMake(left, 0, 2 * _margin + _length, self.frame.size.height); - graph.backgroundColor = [color colorWithAlphaComponent:0.2].CGColor; - graph.fillColor = color.CGColor; - return graph; -} - -- (UILabel *)createLabel:(UIColor *)color -{ - CGFloat left = _position & RCTFPSGraphPositionLeft ? 2 * _margin + _length : 0; - UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(left, 0, _length, self.frame.size.height)]; - label.textColor = color; - label.font = [UIFont systemFontOfSize:9]; - label.minimumScaleFactor = .5; - label.adjustsFontSizeToFitWidth = YES; - label.numberOfLines = 3; - label.lineBreakMode = NSLineBreakByWordWrapping; - label.textAlignment = NSTextAlignmentCenter; - return label; -} - -- (void)onTick:(NSTimeInterval)timestamp -{ - _frameCount++; - if (_prevTime == -1) { - _prevTime = timestamp; - } else if (timestamp - _prevTime > 1) { - float fps = round(_frameCount / (timestamp - _prevTime)); - _minFPS = MIN(_minFPS, fps); - _maxFPS = MAX(_maxFPS, fps); - - _label.text = [NSString stringWithFormat:@"%@\n%d FPS\n(%d - %d)", _name, (int)fps, _minFPS, _maxFPS]; - - float scale = 60.0 / _height; - for (int i = 0; i < _length - 1; i++) { - _frames[i] = _frames[i + 1]; - } - _frames[_length - 1] = fps / scale; - - CGMutablePathRef path = CGPathCreateMutable(); - if (_position & RCTFPSGraphPositionLeft) { - CGPathMoveToPoint(path, NULL, _margin, _margin + _height); - for (int i = 0; i < _length; i++) { - CGPathAddLineToPoint(path, NULL, _margin + i, _margin + _height - _frames[i]); - } - CGPathAddLineToPoint(path, NULL, _margin + _length - 1, _margin + _height); - } else { - CGPathMoveToPoint(path, NULL, _margin + _length - 1, _margin + _height); - for (int i = 0; i < _length; i++) { - CGPathAddLineToPoint(path, NULL, _margin + _length - i - 1, _margin + _height - _frames[i]); - } - CGPathAddLineToPoint(path, NULL, _margin, _margin + _height); - } - _graph.path = path; - CGPathRelease(path); - - _prevTime = timestamp; - _frameCount = 0; - } -} - -@end - -#endif diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index 6af63ac71..763e36893 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -13,6 +13,7 @@ #import "RCTConvert.h" #import "RCTSourceCode.h" #import "RCTUtils.h" +#import "RCTPerformanceLogger.h" @implementation RCTJavaScriptLoader @@ -39,6 +40,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) NSData *source = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error]; + RCTPerformanceLoggerSet(RCTPLBundleSize, source.length); onComplete(error, source); }); return; @@ -73,7 +75,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); } } - // Handle HTTP errors if ([response isKindOfClass:[NSHTTPURLResponse class]] && ((NSHTTPURLResponse *)response).statusCode != 200) { NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding]; @@ -81,7 +82,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) NSDictionary *errorDetails = RCTJSONParse(rawText, nil); if ([errorDetails isKindOfClass:[NSDictionary class]] && [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) { - NSMutableArray *fakeStack = [NSMutableArray new]; + NSMutableArray *fakeStack = [NSMutableArray new]; for (NSDictionary *err in errorDetails[@"errors"]) { [fakeStack addObject: @{ @"methodName": err[@"description"] ?: @"", @@ -103,6 +104,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) onComplete(error, nil); return; } + RCTPerformanceLoggerSet(RCTPLBundleSize, data.length); onComplete(nil, data); }]; diff --git a/React/Base/RCTKeyCommands.m b/React/Base/RCTKeyCommands.m index 36516bbc1..92747519f 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -77,15 +77,15 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) @interface RCTKeyCommands () -@property (nonatomic, strong) NSMutableSet *commands; +@property (nonatomic, strong) NSMutableSet *commands; @end @implementation UIResponder (RCTKeyCommands) -- (NSArray *)RCT_keyCommands +- (NSArray *)RCT_keyCommands { - NSSet *commands = [RCTKeyCommands sharedInstance].commands; + NSSet *commands = [RCTKeyCommands sharedInstance].commands; return [[commands valueForKeyPath:@"keyCommand"] allObjects]; } diff --git a/React/Base/RCTLog.h b/React/Base/RCTLog.h index 13bb150be..28a615af8 100644 --- a/React/Base/RCTLog.h +++ b/React/Base/RCTLog.h @@ -17,14 +17,9 @@ #endif /** - * Thresholds for logs to raise an assertion, or display redbox, respectively. - * You can override these values when debugging in order to tweak the default - * logging behavior. + * Thresholds for logs to display a redbox. You can override these values when debugging + * in order to tweak the default logging behavior. */ -#ifndef RCTLOG_FATAL_LEVEL -#define RCTLOG_FATAL_LEVEL RCTLogLevelMustFix -#endif - #ifndef RCTLOG_REDBOX_LEVEL #define RCTLOG_REDBOX_LEVEL RCTLogLevelError #endif @@ -34,19 +29,20 @@ * own code. */ #define RCTLog(...) _RCTLog(RCTLogLevelInfo, __VA_ARGS__) +#define RCTLogTrace(...) _RCTLog(RCTLogLevelTrace, __VA_ARGS__) #define RCTLogInfo(...) _RCTLog(RCTLogLevelInfo, __VA_ARGS__) #define RCTLogWarn(...) _RCTLog(RCTLogLevelWarning, __VA_ARGS__) #define RCTLogError(...) _RCTLog(RCTLogLevelError, __VA_ARGS__) -#define RCTLogMustFix(...) _RCTLog(RCTLogLevelMustFix, __VA_ARGS__) /** * An enum representing the severity of the log message. */ typedef NS_ENUM(NSInteger, RCTLogLevel) { + RCTLogLevelTrace = 0, RCTLogLevelInfo = 1, RCTLogLevelWarning = 2, RCTLogLevelError = 3, - RCTLogLevelMustFix = 4 + RCTLogLevelFatal = 4 }; /** @@ -119,9 +115,7 @@ RCT_EXTERN void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *pref * Private logging function - ignore this. */ #if RCTLOG_ENABLED -#define _RCTLog(lvl, ...) do { \ -if (lvl >= RCTLOG_FATAL_LEVEL) { RCTAssert(NO, __VA_ARGS__); } \ -_RCTLogInternal(lvl, __FILE__, __LINE__, __VA_ARGS__); } while (0) +#define _RCTLog(lvl, ...) _RCTLogInternal(lvl, __FILE__, __LINE__, __VA_ARGS__); #else #define _RCTLog(lvl, ...) do { } while (0) #endif diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m index 5f8420aef..b9360f449 100644 --- a/React/Base/RCTLog.m +++ b/React/Base/RCTLog.m @@ -26,10 +26,11 @@ static NSString *const RCTLogFunctionStack = @"RCTLogFunctionStack"; const char *RCTLogLevels[] = { + "trace", "info", "warn", "error", - "mustfix" + "fatal", }; #if RCT_DEBUG @@ -63,8 +64,11 @@ RCTLogFunction RCTDefaultLogFunction = ^( fprintf(stderr, "%s\n", log.UTF8String); fflush(stderr); - int aslLevel = ASL_LEVEL_ERR; + int aslLevel; switch(level) { + case RCTLogLevelTrace: + aslLevel = ASL_LEVEL_DEBUG; + break; case RCTLogLevelInfo: aslLevel = ASL_LEVEL_NOTICE; break; @@ -74,11 +78,9 @@ RCTLogFunction RCTDefaultLogFunction = ^( case RCTLogLevelError: aslLevel = ASL_LEVEL_ERR; break; - case RCTLogLevelMustFix: - aslLevel = ASL_LEVEL_EMERG; + case RCTLogLevelFatal: + aslLevel = ASL_LEVEL_CRIT; break; - default: - aslLevel = ASL_LEVEL_DEBUG; } asl_log(NULL, NULL, aslLevel, "%s", message.UTF8String); }; @@ -120,7 +122,7 @@ void RCTAddLogFunction(RCTLogFunction logFunction) static RCTLogFunction RCTGetLocalLogFunction() { NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; - NSArray *functionStack = threadDictionary[RCTLogFunctionStack]; + NSArray *functionStack = threadDictionary[RCTLogFunctionStack]; RCTLogFunction logFunction = functionStack.lastObject; if (logFunction) { return logFunction; @@ -131,7 +133,7 @@ static RCTLogFunction RCTGetLocalLogFunction() void RCTPerformBlockWithLogFunction(void (^block)(void), RCTLogFunction logFunction) { NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; - NSMutableArray *functionStack = threadDictionary[RCTLogFunctionStack]; + NSMutableArray *functionStack = threadDictionary[RCTLogFunctionStack]; if (!functionStack) { functionStack = [NSMutableArray new]; threadDictionary[RCTLogFunctionStack] = functionStack; @@ -212,18 +214,18 @@ void _RCTLogInternal( logFunction(level, fileName ? @(fileName) : nil, (lineNumber >= 0) ? @(lineNumber) : nil, message); } -#if RCT_DEBUG // Red box is only available in debug mode - - // Log to red box +#if RCT_DEBUG + // Log to red box in debug mode. if ([UIApplication sharedApplication] && level >= RCTLOG_REDBOX_LEVEL) { - NSArray *stackSymbols = [NSThread callStackSymbols]; - NSMutableArray *stack = [NSMutableArray arrayWithCapacity:(stackSymbols.count - 1)]; + NSArray *stackSymbols = [NSThread callStackSymbols]; + NSMutableArray *stack = + [NSMutableArray arrayWithCapacity:(stackSymbols.count - 1)]; [stackSymbols enumerateObjectsUsingBlock:^(NSString *frameSymbols, NSUInteger idx, __unused BOOL *stop) { if (idx > 0) { // don't include the current frame NSString *address = [[frameSymbols componentsSeparatedByString:@"0x"][1] componentsSeparatedByString:@" "][0]; NSRange addressRange = [frameSymbols rangeOfString:address]; NSString *methodName = [frameSymbols substringFromIndex:(addressRange.location + addressRange.length + 1)]; - if (idx == 1) { + if (idx == 1 && fileName) { NSString *file = [@(fileName) componentsSeparatedByString:@"/"].lastObject; [stack addObject:@{@"methodName": methodName, @"file": file, @"lineNumber": @(lineNumber)}]; } else { @@ -239,9 +241,7 @@ void _RCTLogInternal( } // Log to JS executor - [[RCTBridge currentBridge] logMessage:message level:level ? @(RCTLogLevels[level - 1]) : @"info"]; - + [[RCTBridge currentBridge] logMessage:message level:level ? @(RCTLogLevels[level]) : @"info"]; #endif - } } diff --git a/React/Base/RCTModuleData.h b/React/Base/RCTModuleData.h index 310dd2fc7..cc3747eae 100644 --- a/React/Base/RCTModuleData.h +++ b/React/Base/RCTModuleData.h @@ -11,6 +11,8 @@ #import "RCTJavaScriptExecutor.h" +@protocol RCTBridgeMethod; + @interface RCTModuleData : NSObject @property (nonatomic, weak, readonly) id javaScriptExecutor; @@ -19,8 +21,8 @@ @property (nonatomic, strong, readonly) Class moduleClass; @property (nonatomic, copy, readonly) NSString *name; -@property (nonatomic, copy, readonly) NSArray *methods; -@property (nonatomic, copy, readonly) NSDictionary *config; +@property (nonatomic, copy, readonly) NSArray> *methods; +@property (nonatomic, copy, readonly) NSArray *config; @property (nonatomic, strong) dispatch_queue_t queue; diff --git a/React/Base/RCTModuleData.m b/React/Base/RCTModuleData.m index 23f46cb79..ddd0384d4 100644 --- a/React/Base/RCTModuleData.m +++ b/React/Base/RCTModuleData.m @@ -12,14 +12,16 @@ #import "RCTBridge.h" #import "RCTModuleMethod.h" #import "RCTLog.h" +#import "RCTUtils.h" @implementation RCTModuleData { NSDictionary *_constants; - NSArray *_methods; NSString *_queueName; } +@synthesize methods = _methods; + - (instancetype)initWithExecutor:(id)javaScriptExecutor moduleID:(NSNumber *)moduleID instance:(id)instance @@ -45,10 +47,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init); -- (NSArray *)methods +- (NSArray> *)methods { if (!_methods) { - NSMutableArray *moduleMethods = [NSMutableArray new]; + NSMutableArray> *moduleMethods = [NSMutableArray new]; if ([_instance respondsToSelector:@selector(methodsToExport)]) { [moduleMethods addObjectsFromArray:[_instance methodsToExport]]; @@ -62,11 +64,12 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init); SEL selector = method_getName(method); if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) { IMP imp = method_getImplementation(method); - NSArray *entries = ((NSArray *(*)(id, SEL))imp)(_moduleClass, selector); + NSArray *entries = + ((NSArray *(*)(id, SEL))imp)(_moduleClass, selector); id moduleMethod = - [[RCTModuleMethod alloc] initWithObjCMethodName:entries[1] - JSMethodName:entries[0] - moduleClass:_moduleClass]; + [[RCTModuleMethod alloc] initWithObjCMethodName:entries[1] + JSMethodName:entries[0] + moduleClass:_moduleClass]; [moduleMethods addObject:moduleMethod]; } @@ -79,25 +82,36 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init); return _methods; } -- (NSDictionary *)config +- (NSArray *)config { - NSMutableDictionary *config = [NSMutableDictionary new]; - config[@"moduleID"] = _moduleID; - - if (_constants) { - config[@"constants"] = _constants; + if (_constants.count == 0 && self.methods.count == 0) { + return (id)kCFNull; // Nothing to export } - NSMutableDictionary *methodconfig = [NSMutableDictionary new]; - [self.methods enumerateObjectsUsingBlock:^(id method, NSUInteger idx, __unused BOOL *stop) { - methodconfig[method.JSMethodName] = @{ - @"methodID": @(idx), - @"type": method.functionType == RCTFunctionTypePromise ? @"remoteAsync" : @"remote", - }; - }]; - config[@"methods"] = [methodconfig copy]; + NSMutableArray *methods = self.methods.count ? [NSMutableArray new] : nil; + NSMutableArray *asyncMethods = nil; + for (id method in self.methods) { + if (method.functionType == RCTFunctionTypePromise) { + if (!asyncMethods) { + asyncMethods = [NSMutableArray new]; + } + [asyncMethods addObject:@(methods.count)]; + } + [methods addObject:method.JSMethodName]; + } - return [config copy]; + NSMutableArray *config = [NSMutableArray new]; + [config addObject:_name]; + if (_constants.count) { + [config addObject:_constants]; + } + if (methods) { + [config addObject:methods]; + if (asyncMethods) { + [config addObject:asyncMethods]; + } + } + return config; } - (dispatch_queue_t)queue diff --git a/React/Base/RCTModuleMap.m b/React/Base/RCTModuleMap.m index 5491c1256..fef338763 100644 --- a/React/Base/RCTModuleMap.m +++ b/React/Base/RCTModuleMap.m @@ -69,7 +69,7 @@ extern BOOL RCTBridgeModuleClassIsRegistered(Class cls); return [_modulesByName keyEnumerator]; } -- (NSArray *)allValues +- (NSArray> *)allValues { // don't perform validation in this case because we only want to error when // an invalid module is specifically requested diff --git a/React/Base/RCTModuleMethod.h b/React/Base/RCTModuleMethod.h index ff5c1cda6..f450c412c 100644 --- a/React/Base/RCTModuleMethod.h +++ b/React/Base/RCTModuleMethod.h @@ -31,7 +31,6 @@ typedef NS_ENUM(NSUInteger, RCTNullability) { @property (nonatomic, readonly) Class moduleClass; @property (nonatomic, readonly) SEL selector; -@property (nonatomic, readonly) RCTFunctionType functionType; - (instancetype)initWithObjCMethodName:(NSString *)objCMethodName JSMethodName:(NSString *)JSMethodName diff --git a/React/Base/RCTModuleMethod.m b/React/Base/RCTModuleMethod.m index f01e60cfd..c35017c98 100644 --- a/React/Base/RCTModuleMethod.m +++ b/React/Base/RCTModuleMethod.m @@ -47,13 +47,14 @@ typedef BOOL (^RCTArgumentBlock)(RCTBridge *, NSUInteger, id); { Class _moduleClass; NSInvocation *_invocation; - NSArray *_argumentBlocks; + NSArray *_argumentBlocks; NSString *_objCMethodName; SEL _selector; NSDictionary *_profileArgs; } @synthesize JSMethodName = _JSMethodName; +@synthesize functionType = _functionType; static void RCTLogArgumentError(RCTModuleMethod *method, NSUInteger index, id valueOrType, const char *issue) @@ -65,8 +66,8 @@ static void RCTLogArgumentError(RCTModuleMethod *method, NSUInteger index, RCT_NOT_IMPLEMENTED(- (instancetype)init) -void RCTParseObjCMethodName(NSString **, NSArray **); -void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) +void RCTParseObjCMethodName(NSString **, NSArray **); +void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) { static NSRegularExpression *typeNameRegex; static dispatch_once_t onceToken; @@ -129,6 +130,7 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) if (colonRange.location != NSNotFound) { methodName = [methodName substringToIndex:colonRange.location]; } + methodName = [methodName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; RCTAssert(methodName.length, @"%@ is not a valid JS function name, please" " supply an alternative using RCT_REMAP_METHOD()", objCMethodName); methodName; @@ -146,7 +148,7 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) - (void)processMethodSignature { - NSArray *arguments; + NSArray *arguments; NSString *objCMethodName = _objCMethodName; RCTParseObjCMethodName(&objCMethodName, &arguments); @@ -158,12 +160,12 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) RCTAssert(methodSignature, @"%@ is not a recognized Objective-C method.", objCMethodName); NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; invocation.selector = _selector; - [invocation retainArguments]; _invocation = invocation; // Process arguments NSUInteger numberOfArguments = methodSignature.numberOfArguments; - NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2]; + NSMutableArray *argumentBlocks = + [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2]; #define RCT_ARG_BLOCK(_logic) \ [argumentBlocks addObject:^(__unused RCTBridge *bridge, NSUInteger index, id json) { \ @@ -172,6 +174,13 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) return YES; \ }]; +/** + * Explicitly copy the block and retain it, since NSInvocation doesn't retain them. + */ +#define RCT_BLOCK_ARGUMENT(block...) \ + id value = json ? [block copy] : (id)^(__unused NSArray *_){}; \ + CFBridgingRetain(value) + __weak RCTModuleMethod *weakSelf = self; void (^addBlockArgument)(void) = ^{ RCT_ARG_BLOCK( @@ -181,12 +190,11 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) return NO; } - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing id value = (json ? ^(NSArray *args) { + RCT_BLOCK_ARGUMENT(^(NSArray *args) { [bridge _invokeAndProcessModule:@"BatchedBridge" method:@"invokeCallbackAndReturnFlushedQueue" arguments:@[json, args]]; - } : ^(__unused NSArray *unused) {}); + }); ) }; @@ -231,7 +239,16 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) RCT_NULLABLE_CASE(_C_SEL, SEL) RCT_NULLABLE_CASE(_C_CHARPTR, const char *) RCT_NULLABLE_CASE(_C_PTR, void *) - RCT_NULLABLE_CASE(_C_ID, id) + + case _C_ID: { + isNullableType = YES; + id (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; + RCT_ARG_BLOCK( + id value = convert([RCTConvert class], selector, json); + CFBridgingRetain(value); + ) + break; + } case _C_STRUCT_B: { @@ -272,12 +289,11 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) return NO; } - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing id value = (json ? ^(NSError *error) { + RCT_BLOCK_ARGUMENT(^(NSError *error) { [bridge _invokeAndProcessModule:@"BatchedBridge" method:@"invokeCallbackAndReturnFlushedQueue" arguments:@[json, @[RCTJSErrorFromNSError(error)]]]; - } : ^(__unused NSError *error) {}); + }); ) } else if ([typeName isEqualToString:@"RCTPromiseResolveBlock"]) { RCTAssert(i == numberOfArguments - 2, @@ -289,8 +305,7 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) return NO; } - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing RCTPromiseResolveBlock value = (^(id result) { + RCT_BLOCK_ARGUMENT(^(id result) { [bridge _invokeAndProcessModule:@"BatchedBridge" method:@"invokeCallbackAndReturnFlushedQueue" arguments:@[json, result ? @[result] : @[]]]; @@ -306,8 +321,7 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) return NO; } - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) { + RCT_BLOCK_ARGUMENT(^(NSError *error) { NSDictionary *errorJSON = RCTJSErrorFromNSError(error); [bridge _invokeAndProcessModule:@"BatchedBridge" method:@"invokeCallbackAndReturnFlushedQueue" @@ -351,12 +365,23 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) if (nullability == RCTNonnullable) { RCTArgumentBlock oldBlock = argumentBlocks[i - 2]; argumentBlocks[i - 2] = ^(RCTBridge *bridge, NSUInteger index, id json) { - if (json == nil) { - RCTLogArgumentError(weakSelf, index, typeName, "must not be null"); - return NO; - } else { - return oldBlock(bridge, index, json); + if (json != nil) { + if (!oldBlock(bridge, index, json)) { + return NO; + } + if (isNullableType) { + // Check converted value wasn't null either, as method probably + // won't gracefully handle a nil vallue for a nonull argument + void *value; + [invocation getArgument:&value atIndex:index + 2]; + if (value == NULL) { + return NO; + } + } + return YES; } + RCTLogArgumentError(weakSelf, index, typeName, "must not be null"); + return NO; }; } } @@ -433,6 +458,25 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) // Invoke method [_invocation invokeWithTarget:module]; + + RCTAssert( + @encode(RCTArgumentBlock)[0] == _C_ID, + @"Block type encoding has changed, it won't be released. A check for the block" + "type encoding (%s) has to be added below.", + @encode(RCTArgumentBlock) + ); + + index = 2; + for (NSUInteger length = _invocation.methodSignature.numberOfArguments; index < length; index++) { + if ([_invocation.methodSignature getArgumentTypeAtIndex:index][0] == _C_ID) { + __unsafe_unretained id value; + [_invocation getArgument:&value atIndex:index]; + + if (value) { + CFRelease((__bridge CFTypeRef)value); + } + } + } } - (NSString *)methodName diff --git a/React/Base/RCTPerfStats.h b/React/Base/RCTPerfStats.h deleted file mode 100644 index 18c13cad6..000000000 --- a/React/Base/RCTPerfStats.h +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTBridge.h" -#import "RCTFPSGraph.h" - -@interface RCTPerfStats : NSObject - -@property (nonatomic, strong) RCTFPSGraph *jsGraph; -@property (nonatomic, strong) RCTFPSGraph *uiGraph; - -- (void)show; -- (void)hide; - -@end - -@interface RCTBridge (RCTPerfStats) - -@property (nonatomic, strong, readonly) RCTPerfStats *perfStats; - -@end diff --git a/React/Base/RCTPerfStats.m b/React/Base/RCTPerfStats.m deleted file mode 100644 index 462aa0f9e..000000000 --- a/React/Base/RCTPerfStats.m +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTPerfStats.h" - -#import "RCTDefines.h" -#import "RCTUtils.h" - -#if RCT_DEV - -@interface RCTPerfStats() - -@end - -@implementation RCTPerfStats -{ - UIView *_container; -} - -RCT_EXPORT_MODULE() - -- (void)dealloc -{ - [self hide]; -} - -- (UIView *)container -{ - if (!_container) { - _container = [UIView new]; - _container.backgroundColor = [UIColor colorWithRed:0 green:0 blue:34/255.0 alpha:1]; - _container.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth; - } - return _container; -} - -- (RCTFPSGraph *)jsGraph -{ - if (!_jsGraph && _container) { - UIColor *jsColor = [UIColor colorWithRed:0 green:1 blue:0 alpha:1]; - _jsGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(2, 2, 124, 34) - graphPosition:RCTFPSGraphPositionRight - name:@"[ JS ]" - color:jsColor]; - _jsGraph.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; - } - return _jsGraph; -} - -- (RCTFPSGraph *)uiGraph -{ - if (!_uiGraph && _container) { - UIColor *uiColor = [UIColor colorWithRed:0 green:1 blue:1 alpha:1]; - _uiGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(2, 2, 124, 34) - graphPosition:RCTFPSGraphPositionLeft - name:@"[ UI ]" - color:uiColor]; - } - return _uiGraph; -} - -- (void)show -{ - if (RCTRunningInAppExtension()) { - return; - } - - UIView *targetView = RCTSharedApplication().delegate.window.rootViewController.view; - - targetView.frame = (CGRect){ - targetView.frame.origin, - { - targetView.frame.size.width, - targetView.frame.size.height - 38, - } - }; - - self.container.frame = (CGRect){{0, targetView.frame.size.height}, {targetView.frame.size.width, 38}}; - self.jsGraph.frame = (CGRect){ - { - targetView.frame.size.width - self.uiGraph.frame.size.width - self.uiGraph.frame.origin.x, - self.uiGraph.frame.origin.x, - }, - self.uiGraph.frame.size, - }; - - [self.container addSubview:self.jsGraph]; - [self.container addSubview:self.uiGraph]; - [targetView addSubview:self.container]; -} - -- (void)hide -{ - UIView *targetView = _container.superview; - - targetView.frame = (CGRect){ - targetView.frame.origin, - { - targetView.frame.size.width, - targetView.frame.size.height + _container.frame.size.height - } - }; - - [_container removeFromSuperview]; -} - -- (dispatch_queue_t)methodQueue -{ - return dispatch_get_main_queue(); -} - -@end - -@implementation RCTBridge (RCTPerfStats) - -- (RCTPerfStats *)perfStats -{ - return self.modules[RCTBridgeModuleNameForClass([RCTPerfStats class])]; -} - -@end - -#else - -@implementation RCTPerfStats - -- (void)show {} -- (void)hide {} - -@end - -@implementation RCTBridge (RCTPerfStats) - -- (RCTPerfStats *)perfStats -{ - return nil; -} - -@end - -#endif diff --git a/React/Base/RCTPerformanceLogger.h b/React/Base/RCTPerformanceLogger.h index d2f8d7b50..499617df1 100644 --- a/React/Base/RCTPerformanceLogger.h +++ b/React/Base/RCTPerformanceLogger.h @@ -18,9 +18,12 @@ typedef NS_ENUM(NSUInteger, RCTPLTag) { RCTPLNativeModulePrepareConfig, RCTPLNativeModuleInjectConfig, RCTPLTTI, + RCTPLBundleSize, RCTPLSize }; void RCTPerformanceLoggerStart(RCTPLTag tag); void RCTPerformanceLoggerEnd(RCTPLTag tag); -NSArray *RCTPerformanceLoggerOutput(void); +void RCTPerformanceLoggerSet(RCTPLTag tag, int64_t value); +NSArray *RCTPerformanceLoggerOutput(void); +NSArray *RCTPerformanceLoggerLabels(void); diff --git a/React/Base/RCTPerformanceLogger.m b/React/Base/RCTPerformanceLogger.m index 1a7a94362..f099db677 100644 --- a/React/Base/RCTPerformanceLogger.m +++ b/React/Base/RCTPerformanceLogger.m @@ -30,7 +30,13 @@ void RCTPerformanceLoggerEnd(RCTPLTag tag) } } -NSArray *RCTPerformanceLoggerOutput(void) +void RCTPerformanceLoggerSet(RCTPLTag tag, int64_t value) +{ + RCTPLData[tag][0] = 0; + RCTPLData[tag][1] = value; +} + +NSArray *RCTPerformanceLoggerOutput(void) { return @[ @(RCTPLData[RCTPLScriptDownload][0]), @@ -45,6 +51,21 @@ NSArray *RCTPerformanceLoggerOutput(void) @(RCTPLData[RCTPLNativeModuleInjectConfig][1]), @(RCTPLData[RCTPLTTI][0]), @(RCTPLData[RCTPLTTI][1]), + @(RCTPLData[RCTPLBundleSize][0]), + @(RCTPLData[RCTPLBundleSize][1]), + ]; +} + +NSArray *RCTPerformanceLoggerLabels(void) +{ + return @[ + @"ScriptDownload", + @"ScriptExecution", + @"NativeModuleInit", + @"NativeModulePrepareConfig", + @"NativeModuleInjectConfig", + @"TTI", + @"BundleSize", ]; } @@ -80,14 +101,7 @@ RCT_EXPORT_MODULE() [_bridge enqueueJSCall:@"PerformanceLogger.addTimespans" args:@[ RCTPerformanceLoggerOutput(), - @[ - @"ScriptDownload", - @"ScriptExecution", - @"NativeModuleInit", - @"NativeModulePrepareConfig", - @"NativeModuleInjectConfig", - @"TTI", - ], + RCTPerformanceLoggerLabels(), ]]; } diff --git a/React/Base/RCTRootView.h b/React/Base/RCTRootView.h index 80f86d0cb..fdb7d6b9b 100644 --- a/React/Base/RCTRootView.h +++ b/React/Base/RCTRootView.h @@ -11,6 +11,22 @@ #import "RCTBridge.h" +@protocol RCTRootViewDelegate; + +/** + * This enum is used to define size flexibility type of the root view. + * If a dimension is flexible, the view will recalculate that dimension + * so the content fits. Recalculations are performed when the root's frame, + * size flexibility mode or content size changes. After a recalculation, + * rootViewDidChangeIntrinsicSize method of the RCTRootViewDelegate will be called. + */ +typedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) { + RCTRootViewSizeFlexibilityNone = 0, + RCTRootViewSizeFlexibilityWidth, + RCTRootViewSizeFlexibilityHeight, + RCTRootViewSizeFlexibilityWidthAndHeight, +}; + /** * This notification is sent when the first subviews are added to the root view * after the application has loaded. This is used to hide the `loadingView`, and @@ -59,10 +75,21 @@ extern NSString *const RCTContentDidAppearNotification; @property (nonatomic, strong, readonly) RCTBridge *bridge; /** + * DEPRECATED: access app properties via appProperties property instead + * * The default properties to apply to the view when the script bundle * is first loaded. Defaults to nil/empty. */ -@property (nonatomic, copy, readonly) NSDictionary *initialProperties; +@property (nonatomic, copy, readonly) NSDictionary *initialProperties DEPRECATED_MSG_ATTRIBUTE ("use appProperties instead"); + +/** + * The properties to apply to the view. Use this property to update + * application properties and rerender the view. Initialized with + * initialProperties argument of the initializer. + * + * Set this property only on the main thread. + */ +@property (nonatomic, copy, readwrite) NSDictionary *appProperties; /** * The class of the RCTJavaScriptExecutor to use with this view. @@ -71,6 +98,22 @@ extern NSString *const RCTContentDidAppearNotification; */ @property (nonatomic, strong) Class executorClass; +/** + * The size flexibility mode of the root view. + */ +@property (nonatomic, assign) RCTRootViewSizeFlexibility sizeFlexibility; + +/** + * The size of the root view's content. This is set right before the + * rootViewDidChangeIntrinsicSize method of RCTRootViewDelegate is called. + */ +@property (readonly, nonatomic, assign) CGSize intrinsicSize; + +/** + * The delegate that handles intrinsic size updates. + */ +@property (nonatomic, weak) id delegate; + /** * The backing view controller of the root view. */ diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index ca2f75e3f..90a3fed9c 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -8,6 +8,8 @@ */ #import "RCTRootView.h" +#import "RCTRootViewDelegate.h" +#import "RCTRootViewInternal.h" #import @@ -52,6 +54,7 @@ NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotificat RCTBridge *_bridge; NSString *_moduleName; NSDictionary *_launchOptions; + NSDictionary *_initialProperties; RCTRootContentView *_contentView; } @@ -70,8 +73,10 @@ NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotificat _bridge = bridge; _moduleName = moduleName; _initialProperties = [initialProperties copy]; + _appProperties = [initialProperties copy]; _loadingViewFadeDelay = 0.25; _loadingViewFadeDuration = 0.25; + _sizeFlexibility = RCTRootViewSizeFlexibilityNone; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(javaScriptDidLoad:) @@ -179,16 +184,27 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) _contentView.backgroundColor = self.backgroundColor; [self insertSubview:_contentView atIndex:0]; + [self runApplication:bridge]; +} + +- (void)runApplication:(RCTBridge *)bridge +{ NSString *moduleName = _moduleName ?: @""; NSDictionary *appParameters = @{ @"rootTag": _contentView.reactTag, - @"initialProps": _initialProperties ?: @{}, + @"initialProps": _appProperties ?: @{}, }; [bridge enqueueJSCall:@"AppRegistry.runApplication" args:@[moduleName, appParameters]]; } +- (void)setSizeFlexibility:(RCTRootViewSizeFlexibility)sizeFlexibility +{ + _sizeFlexibility = sizeFlexibility; + [self setNeedsLayout]; +} + - (void)layoutSubviews { [super layoutSubviews]; @@ -199,6 +215,35 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) }; } +- (NSDictionary *)initialProperties +{ + RCTLogWarn(@"Using deprecated 'initialProperties' property. Use 'appProperties' instead."); + return _initialProperties; +} + +- (void)setAppProperties:(NSDictionary *)appProperties +{ + RCTAssertMainThread(); + + if ([_appProperties isEqualToDictionary:appProperties]) { + return; + } + + _appProperties = [appProperties copy]; + + if (_bridge.valid && !_bridge.loading) { + [self runApplication:_bridge.batchedBridge]; + } +} + +- (void)setIntrinsicSize:(CGSize)intrinsicSize +{ + if (!CGSizeEqualToSize(_intrinsicSize, intrinsicSize)) { + _intrinsicSize = intrinsicSize; + [_delegate rootViewDidChangeIntrinsicSize:self]; + } +} + - (NSNumber *)reactTag { return _contentView.reactTag; @@ -243,7 +288,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) if ((self = [super initWithFrame:frame])) { _bridge = bridge; [self setUp]; - self.frame = frame; self.layer.backgroundColor = NULL; } return self; diff --git a/React/Base/RCTRootViewDelegate.h b/React/Base/RCTRootViewDelegate.h new file mode 100644 index 000000000..61cab6fbf --- /dev/null +++ b/React/Base/RCTRootViewDelegate.h @@ -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. + */ + +#import + +@class RCTRootView; + +@protocol RCTRootViewDelegate +/** + * Called after the root view's content is updated to a new size. + * + * The delegate can use this callback to appropriately resize the root view frame to fit the new + * content view size. The view will not resize itself. The new content size is available via the + * intrinsicSize propery of the root view. + */ +- (void)rootViewDidChangeIntrinsicSize:(RCTRootView *)rootView; + +@end diff --git a/React/Base/RCTRootViewInternal.h b/React/Base/RCTRootViewInternal.h new file mode 100644 index 000000000..d95cc10cd --- /dev/null +++ b/React/Base/RCTRootViewInternal.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. + */ + +#import "RCTRootView.h" + +/** + * The interface provides a set of functions that allow other internal framework + * classes to change the RCTRootViews's internal state. + */ +@interface RCTRootView () + +/** + * This setter should be used only by RCTUIManager on react root view size update. + */ +@property (readwrite, nonatomic, assign) CGSize intrinsicSize; + +@end diff --git a/React/Base/RCTSparseArray.h b/React/Base/RCTSparseArray.h index 806f27315..78410d72d 100644 --- a/React/Base/RCTSparseArray.h +++ b/React/Base/RCTSparseArray.h @@ -27,7 +27,7 @@ - (id)objectForKeyedSubscript:(NSNumber *)key; @property (readonly, nonatomic) NSUInteger count; -@property (readonly, nonatomic, copy) NSArray *allIndexes; +@property (readonly, nonatomic, copy) NSArray *allIndexes; @property (readonly, nonatomic, copy) NSArray *allObjects; - (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSNumber *idx, BOOL *stop))block; diff --git a/React/Base/RCTSparseArray.m b/React/Base/RCTSparseArray.m index 1c67df644..76e01d677 100644 --- a/React/Base/RCTSparseArray.m +++ b/React/Base/RCTSparseArray.m @@ -79,7 +79,7 @@ return _storage.count; } -- (NSArray *)allIndexes +- (NSArray *)allIndexes { return _storage.allKeys; } diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index eb902b8eb..707830aab 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -31,9 +31,9 @@ * These must be kept track of because `UIKit` destroys the touch targets * if touches are canceled, and we have no other way to recover this info. */ - NSMutableOrderedSet *_nativeTouches; - NSMutableArray *_reactTouches; - NSMutableArray *_touchViews; + NSMutableOrderedSet *_nativeTouches; + NSMutableArray *_reactTouches; + NSMutableArray *_touchViews; BOOL _dispatchedInitialTouches; BOOL _recordingInteractionTiming; @@ -71,7 +71,7 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { #pragma mark - Bookkeeping for touch indices -- (void)_recordNewTouches:(NSSet *)touches +- (void)_recordNewTouches:(NSSet *)touches { for (UITouch *touch in touches) { @@ -121,7 +121,7 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { } } -- (void)_recordRemovedTouches:(NSSet *)touches +- (void)_recordRemovedTouches:(NSSet *)touches { for (UITouch *touch in touches) { NSUInteger index = [_nativeTouches indexOfObject:touch]; @@ -163,12 +163,12 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { * (start/end/move/cancel) and the indices that represent "changed" `Touch`es * from that array. */ -- (void)_updateAndDispatchTouches:(NSSet *)touches +- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName originatingTime:(__unused CFTimeInterval)originatingTime { // Update touches - NSMutableArray *changedIndexes = [NSMutableArray new]; + NSMutableArray *changedIndexes = [NSMutableArray new]; for (UITouch *touch in touches) { NSInteger index = [_nativeTouches indexOfObject:touch]; if (index == NSNotFound) { @@ -185,7 +185,8 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { // Deep copy the touches because they will be accessed from another thread // TODO: would it be safer to do this in the bridge or executor, rather than trusting caller? - NSMutableArray *reactTouches = [[NSMutableArray alloc] initWithCapacity:_reactTouches.count]; + NSMutableArray *reactTouches = + [[NSMutableArray alloc] initWithCapacity:_reactTouches.count]; for (NSDictionary *touch in _reactTouches) { [reactTouches addObject:[touch copy]]; } @@ -197,7 +198,7 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { #pragma mark - Gesture Recognizer Delegate Callbacks -static BOOL RCTAllTouchesAreCancelledOrEnded(NSSet *touches) +static BOOL RCTAllTouchesAreCancelledOrEnded(NSSet *touches) { for (UITouch *touch in touches) { if (touch.phase == UITouchPhaseBegan || @@ -209,7 +210,7 @@ static BOOL RCTAllTouchesAreCancelledOrEnded(NSSet *touches) return YES; } -static BOOL RCTAnyTouchesChanged(NSSet *touches) +static BOOL RCTAnyTouchesChanged(NSSet *touches) { for (UITouch *touch in touches) { if (touch.phase == UITouchPhaseBegan || @@ -238,7 +239,7 @@ static BOOL RCTAnyTouchesChanged(NSSet *touches) // dispatch the updates from the raw touch methods below. } -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; @@ -253,7 +254,7 @@ static BOOL RCTAnyTouchesChanged(NSSet *touches) } } -- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; @@ -263,7 +264,7 @@ static BOOL RCTAnyTouchesChanged(NSSet *touches) } } -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; @@ -279,7 +280,7 @@ static BOOL RCTAnyTouchesChanged(NSSet *touches) [self _recordRemovedTouches:touches]; } -- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesCancelled:touches withEvent:event]; diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 1f915e66d..02e37c8b9 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -58,9 +58,17 @@ RCT_EXTERN BOOL RCTRunningInAppExtension(void); // Returns the shared UIApplication instance, or nil if running in an App Extension RCT_EXTERN UIApplication *RCTSharedApplication(void); +// Returns the current main window, useful if you need to access the root view +// or view controller, e.g. to present a modal view controller or alert. +RCT_EXTERN UIWindow *RCTKeyWindow(void); + // Return a UIAlertView initialized with the given values // or nil if running in an app extension -RCT_EXTERN UIAlertView *RCTAlertView(NSString *title, NSString *message, id delegate, NSString *cancelButtonTitle, NSArray *otherButtonTitles); +RCT_EXTERN UIAlertView *RCTAlertView(NSString *title, + NSString *message, + id delegate, + NSString *cancelButtonTitle, + NSArray *otherButtonTitles); // Return YES if image has an alpha component RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index d262c2ced..732240aec 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -24,7 +24,7 @@ NSString *RCTJSONStringify(id jsonObject, NSError **error) { static SEL JSONKitSelector = NULL; - static NSSet *collectionTypes; + static NSSet *collectionTypes; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SEL selector = NSSelectorFromString(@"JSONStringWithOptions:error:"); @@ -121,7 +121,7 @@ id RCTJSONParseMutable(NSString *jsonString, NSError **error) id RCTJSONClean(id object) { static dispatch_once_t onceToken; - static NSSet *validLeafTypes; + static NSSet *validLeafTypes; dispatch_once(&onceToken, ^{ validLeafTypes = [[NSSet alloc] initWithArray:@[ [NSString class], @@ -301,7 +301,7 @@ NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extr NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData) { - id error = RCTMakeError(message, toStringify, extraData); + NSDictionary *error = RCTMakeError(message, toStringify, extraData); RCTLogError(@"\nError: %@", error); return error; } @@ -310,9 +310,9 @@ NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary NSDictionary *RCTJSErrorFromNSError(NSError *error) { NSString *errorMessage; - NSArray *stackTrace = [NSThread callStackSymbols]; + NSArray *stackTrace = [NSThread callStackSymbols]; NSMutableDictionary *errorInfo = - [NSMutableDictionary dictionaryWithObject:stackTrace forKey:@"nativeStackIOS"]; + [NSMutableDictionary dictionaryWithObject:stackTrace forKey:@"nativeStackIOS"]; if (error) { errorMessage = error.localizedDescription ?: @"Unknown error from a native module"; @@ -342,23 +342,36 @@ BOOL RCTRunningInAppExtension(void) return [[[[NSBundle mainBundle] bundlePath] pathExtension] isEqualToString:@"appex"]; } -id RCTSharedApplication(void) +UIApplication *RCTSharedApplication(void) { if (RCTRunningInAppExtension()) { return nil; } - return [[UIApplication class] performSelector:@selector(sharedApplication)]; } -id RCTAlertView(NSString *title, NSString *message, id delegate, NSString *cancelButtonTitle, NSArray *otherButtonTitles) +UIWindow *RCTKeyWindow(void) +{ + if (RCTRunningInAppExtension()) { + return nil; + } + + // TODO: replace with a more robust solution + return RCTSharedApplication().keyWindow; +} + +UIAlertView *RCTAlertView(NSString *title, + NSString *message, + id delegate, + NSString *cancelButtonTitle, + NSArray *otherButtonTitles) { if (RCTRunningInAppExtension()) { RCTLogError(@"RCTAlertView is unavailable when running in an app extension"); return nil; } - - UIAlertView *alertView = [[UIAlertView alloc] init]; + + UIAlertView *alertView = [UIAlertView new]; alertView.title = title; alertView.message = message; alertView.delegate = delegate; @@ -366,8 +379,7 @@ id RCTAlertView(NSString *title, NSString *message, id delegate, NSString *cance [alertView addButtonWithTitle:cancelButtonTitle]; alertView.cancelButtonIndex = 0; } - for (NSString *buttonTitle in otherButtonTitles) - { + for (NSString *buttonTitle in otherButtonTitles) { [alertView addButtonWithTitle:buttonTitle]; } return alertView; @@ -388,8 +400,7 @@ BOOL RCTImageHasAlpha(CGImageRef image) NSError *RCTErrorWithMessage(NSString *message) { NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message}; - NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]; - return error; + return [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]; } id RCTNullIfNil(id value) diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index a0f9f0be8..53e0b2ad9 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -33,7 +33,7 @@ #if RCT_JSC_PROFILER #include -static NSString * const RCTJSCProfilerEnabledDefaultsKey = @"RCTJSCProfilerEnabled"; +static NSString *const RCTJSCProfilerEnabledDefaultsKey = @"RCTJSCProfilerEnabled"; #ifndef RCT_JSC_PROFILER_DYLIB #define RCT_JSC_PROFILER_DYLIB [[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"RCTJSCProfiler.ios%zd", [[[UIDevice currentDevice] systemVersion] integerValue]] ofType:@"dylib" inDirectory:@"RCTJSCProfiler"] UTF8String] @@ -93,7 +93,7 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init) // Private bridge interface to allow middle-batch calls @interface RCTBridge (RCTContextExecutor) -- (void)handleBuffer:(NSArray *)buffer batchEnded:(BOOL)hasEnded; +- (void)handleBuffer:(NSArray *)buffer batchEnded:(BOOL)hasEnded; @end @@ -119,26 +119,14 @@ RCT_EXPORT_MODULE() static JSValueRef RCTNativeLoggingHook(JSContextRef context, __unused JSObjectRef object, __unused JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception) { if (argumentCount > 0) { - JSStringRef messageRef = JSValueToStringCopy(context, arguments[0], exception); - if (!messageRef) { - return JSValueMakeUndefined(context); - } - NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, messageRef); - JSStringRelease(messageRef); - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern: - @"( stack: )?([_a-z0-9]*)@?(http://|file:///)[a-z.0-9:/_-]+/([a-z0-9_]+).bundle(:[0-9]+:[0-9]+)" - options:NSRegularExpressionCaseInsensitive - error:NULL]; - message = [regex stringByReplacingMatchesInString:message - options:0 - range:(NSRange){0, message.length} - withTemplate:@"[$4$5] \t$2"]; + NSString *message = RCTJSValueToNSString(context, arguments[0], exception); RCTLogLevel level = RCTLogLevelInfo; if (argumentCount > 1) { - level = MAX(level, JSValueToNumber(context, arguments[1], exception) - 1); + level = MAX(level, JSValueToNumber(context, arguments[1], exception)); } - RCTGetLogFunction()(level, nil, nil, message); + + _RCTLog(level, @"%@", message); } return JSValueMakeUndefined(context); @@ -152,18 +140,22 @@ static JSValueRef RCTNoop(JSContextRef context, __unused JSObjectRef object, __u return JSValueMakeUndefined(context); } -static NSString *RCTJSValueToNSString(JSContextRef context, JSValueRef value) +static NSString *RCTJSValueToNSString(JSContextRef context, JSValueRef value, JSValueRef *exception) { - JSStringRef JSString = JSValueToStringCopy(context, value, NULL); + JSStringRef JSString = JSValueToStringCopy(context, value, exception); + if (!JSString) { + return nil; + } + CFStringRef string = JSStringCopyCFString(kCFAllocatorDefault, JSString); JSStringRelease(JSString); return (__bridge_transfer NSString *)string; } -static NSString *RCTJSValueToJSONString(JSContextRef context, JSValueRef value, unsigned indent) +static NSString *RCTJSValueToJSONString(JSContextRef context, JSValueRef value, JSValueRef *exception, unsigned indent) { - JSStringRef JSString = JSValueCreateJSONString(context, value, indent, NULL); + JSStringRef JSString = JSValueCreateJSONString(context, value, indent, exception); CFStringRef string = JSStringCopyCFString(kCFAllocatorDefault, JSString); JSStringRelease(JSString); @@ -172,14 +164,14 @@ static NSString *RCTJSValueToJSONString(JSContextRef context, JSValueRef value, static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) { - NSString *errorMessage = jsError ? RCTJSValueToNSString(context, jsError) : @"unknown JS error"; - NSString *details = jsError ? RCTJSValueToJSONString(context, jsError, 2) : @"no details"; + NSString *errorMessage = jsError ? RCTJSValueToNSString(context, jsError, NULL) : @"unknown JS error"; + NSString *details = jsError ? RCTJSValueToJSONString(context, jsError, NULL, 2) : @"no details"; return [NSError errorWithDomain:@"JS" code:1 userInfo:@{NSLocalizedDescriptionKey: errorMessage, NSLocalizedFailureReasonErrorKey: details}]; } #if RCT_DEV -static JSValueRef RCTNativeTraceBeginSection(JSContextRef context, __unused JSObjectRef object, __unused JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], __unused JSValueRef *exception) +static JSValueRef RCTNativeTraceBeginSection(JSContextRef context, __unused JSObjectRef object, __unused JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception) { static int profileCounter = 1; NSString *profileName; @@ -189,14 +181,14 @@ static JSValueRef RCTNativeTraceBeginSection(JSContextRef context, __unused JSOb if (JSValueIsNumber(context, arguments[0])) { tag = JSValueToNumber(context, arguments[0], NULL); } else { - profileName = RCTJSValueToNSString(context, arguments[0]); + profileName = RCTJSValueToNSString(context, arguments[0], exception); } } else { profileName = [NSString stringWithFormat:@"Profile %d", profileCounter++]; } if (argumentCount > 1 && JSValueIsString(context, arguments[1])) { - profileName = RCTJSValueToNSString(context, arguments[1]); + profileName = RCTJSValueToNSString(context, arguments[1], exception); } if (profileName) { @@ -206,15 +198,11 @@ static JSValueRef RCTNativeTraceBeginSection(JSContextRef context, __unused JSOb return JSValueMakeUndefined(context); } -static JSValueRef RCTNativeTraceEndSection(JSContextRef context, __unused JSObjectRef object, __unused JSObjectRef thisObject, __unused size_t argumentCount, __unused const JSValueRef arguments[], __unused JSValueRef *exception) +static JSValueRef RCTNativeTraceEndSection(JSContextRef context, __unused JSObjectRef object, __unused JSObjectRef thisObject, __unused size_t argumentCount, __unused const JSValueRef arguments[], JSValueRef *exception) { if (argumentCount > 0) { - JSValueRef *error = NULL; - double tag = JSValueToNumber(context, arguments[0], error); - - if (error == NULL) { - RCTProfileEndEvent((uint64_t)tag, @"console", nil); - } + double tag = JSValueToNumber(context, arguments[0], exception); + RCTProfileEndEvent((uint64_t)tag, @"console", nil); } return JSValueMakeUndefined(context); @@ -231,11 +219,11 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) (__typeof__(nativeProfilerEnd))dlsym(JSCProfiler, "nativeProfilerEnd"); if (nativeProfilerStart != NULL && nativeProfilerEnd != NULL) { - void (*nativeProfilerEnableByteCode)(void) = - (__typeof__(nativeProfilerEnableByteCode))dlsym(JSCProfiler, "nativeProfilerEnableByteCode"); + void (*nativeProfilerEnableBytecode)(void) = + (__typeof__(nativeProfilerEnableBytecode))dlsym(JSCProfiler, "nativeProfilerEnableBytecode"); - if (nativeProfilerEnableByteCode != NULL) { - nativeProfilerEnableByteCode(); + if (nativeProfilerEnableBytecode != NULL) { + nativeProfilerEnableBytecode(); } static BOOL isProfiling = NO; @@ -338,14 +326,14 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) return; } if (!strongSelf->_context) { - JSContext *context = [[JSContext alloc] init]; + JSContext *context = [JSContext new]; strongSelf->_context = [[RCTJavaScriptContext alloc] initWithJSContext:context]; } [strongSelf _addNativeHook:RCTNativeLoggingHook withName:"nativeLoggingHook"]; [strongSelf _addNativeHook:RCTNoop withName:"noop"]; __weak RCTBridge *bridge = strongSelf->_bridge; - strongSelf->_context.context[@"nativeFlushQueueImmediate"] = ^(NSArray *calls){ + strongSelf->_context.context[@"nativeFlushQueueImmediate"] = ^(NSArray *calls){ if (!weakSelf.valid || !calls) { return; } @@ -353,7 +341,15 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) [bridge handleBuffer:calls batchEnded:NO]; }; + strongSelf->_context.context[@"RCTPerformanceNow"] = ^(){ + return CACurrentMediaTime() * 1000 * 1000; + }; + #if RCT_DEV + if (RCTProfileIsProfiling()) { + strongSelf->_context.context[@"__RCTProfileIsProfiling"] = @YES; + } + [strongSelf _addNativeHook:RCTNativeTraceBeginSection withName:"nativeTraceBeginSection"]; [strongSelf _addNativeHook:RCTNativeTraceEndSection withName:"nativeTraceEndSection"]; @@ -535,13 +531,14 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) return; } + RCTPerformanceLoggerStart(RCTPLScriptExecution); + // JSStringCreateWithUTF8CString expects a null terminated C string NSMutableData *nullTerminatedScript = [NSMutableData dataWithCapacity:script.length + 1]; [nullTerminatedScript appendData:script]; [nullTerminatedScript appendBytes:"" length:1]; - RCTPerformanceLoggerStart(RCTPLScriptExecution); JSValueRef jsError = NULL; JSStringRef execJSString = JSStringCreateWithUTF8CString(nullTerminatedScript.bytes); JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString); diff --git a/React/Modules/RCTAlertManager.m b/React/Modules/RCTAlertManager.m index b07dcde54..7c6ef18f5 100644 --- a/React/Modules/RCTAlertManager.m +++ b/React/Modules/RCTAlertManager.m @@ -10,6 +10,7 @@ #import "RCTAlertManager.h" #import "RCTAssert.h" +#import "RCTConvert.h" #import "RCTLog.h" #import "RCTUtils.h" @@ -19,9 +20,10 @@ @implementation RCTAlertManager { - NSMutableArray *_alerts; - NSMutableArray *_alertCallbacks; - NSMutableArray *_alertButtonKeys; + NSMutableArray *_alerts; + NSMutableArray *_alertControllers; + NSMutableArray *_alertCallbacks; + NSMutableArray *> *_alertButtonKeys; } RCT_EXPORT_MODULE() @@ -30,6 +32,7 @@ RCT_EXPORT_MODULE() { if ((self = [super init])) { _alerts = [NSMutableArray new]; + _alertControllers = [NSMutableArray new]; _alertCallbacks = [NSMutableArray new]; _alertButtonKeys = [NSMutableArray new]; } @@ -43,9 +46,12 @@ RCT_EXPORT_MODULE() - (void)invalidate { - for (UIAlertView *alert in _alerts) { - [alert dismissWithClickedButtonIndex:0 animated:YES]; - } + for (UIAlertView *alert in _alerts) { + [alert dismissWithClickedButtonIndex:0 animated:YES]; + } + for (UIAlertController *alertController in _alertControllers) { + [alertController.presentingViewController dismissViewControllerAnimated:YES completion:nil]; + } } /** @@ -65,10 +71,11 @@ RCT_EXPORT_MODULE() RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args callback:(RCTResponseSenderBlock)callback) { - NSString *title = args[@"title"]; - NSString *message = args[@"message"]; - NSString *type = args[@"type"]; - NSArray *buttons = args[@"buttons"]; + NSString *title = [RCTConvert NSString:args[@"title"]]; + NSString *message = [RCTConvert NSString:args[@"message"]]; + NSString *type = [RCTConvert NSString:args[@"type"]]; + NSDictionaryArray *buttons = [RCTConvert NSDictionaryArray:args[@"buttons"]]; + BOOL allowsTextInput = [type isEqual:@"plain-text"]; if (!title && !message) { RCTLogError(@"Must specify either an alert title, or message, or both"); @@ -77,41 +84,93 @@ RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args RCTLogError(@"Must have at least one button."); return; } - + if (RCTRunningInAppExtension()) { return; } - - UIAlertView *alertView = RCTAlertView(title, nil, self, nil, nil); - NSMutableArray *buttonKeys = [[NSMutableArray alloc] initWithCapacity:buttons.count]; - if ([type isEqualToString:@"plain-text"]) { - alertView.alertViewStyle = UIAlertViewStylePlainTextInput; - [alertView textFieldAtIndex:0].text = message; - } else { - alertView.message = message; + UIViewController *presentingController = RCTKeyWindow().rootViewController; + if (presentingController == nil) { + RCTLogError(@"Tried to display alert view but there is no application window. args: %@", args); + return; } - NSInteger index = 0; - for (NSDictionary *button in buttons) { - if (button.count != 1) { - RCTLogError(@"Button definitions should have exactly one key."); +#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 + + if ([UIAlertController class] == nil) { + + UIAlertView *alertView = RCTAlertView(title, nil, self, nil, nil); + NSMutableArray *buttonKeys = [[NSMutableArray alloc] initWithCapacity:buttons.count]; + + if (allowsTextInput) { + alertView.alertViewStyle = UIAlertViewStylePlainTextInput; + [alertView textFieldAtIndex:0].text = message; + } else { + alertView.message = message; } - NSString *buttonKey = button.allKeys.firstObject; - NSString *buttonTitle = [button[buttonKey] description]; - [alertView addButtonWithTitle:buttonTitle]; - if ([buttonKey isEqualToString: @"cancel"]) { - alertView.cancelButtonIndex = index; + + NSInteger index = 0; + for (NSDictionary *button in buttons) { + if (button.count != 1) { + RCTLogError(@"Button definitions should have exactly one key."); + } + NSString *buttonKey = button.allKeys.firstObject; + NSString *buttonTitle = [button[buttonKey] description]; + [alertView addButtonWithTitle:buttonTitle]; + if ([buttonKey isEqualToString:@"cancel"]) { + alertView.cancelButtonIndex = index; + } + [buttonKeys addObject:buttonKey]; + index ++; } - [buttonKeys addObject:buttonKey]; - index ++; + + [_alerts addObject:alertView]; + [_alertCallbacks addObject:callback ?: ^(__unused id unused) {}]; + [_alertButtonKeys addObject:buttonKeys]; + + [alertView show]; + + } else + +#endif + + { + UIAlertController *alertController = + [UIAlertController alertControllerWithTitle:title + message:nil + preferredStyle:UIAlertControllerStyleAlert]; + + if (allowsTextInput) { + [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.text = message; + }]; + } else { + alertController.message = message; + } + + for (NSDictionary *button in buttons) { + if (button.count != 1) { + RCTLogError(@"Button definitions should have exactly one key."); + } + NSString *buttonKey = button.allKeys.firstObject; + NSString *buttonTitle = [button[buttonKey] description]; + UIAlertActionStyle buttonStyle = [buttonKey isEqualToString:@"cancel"] ? UIAlertActionStyleCancel : UIAlertActionStyleDefault; + UITextField *textField = allowsTextInput ? alertController.textFields.firstObject : nil; + [alertController addAction:[UIAlertAction actionWithTitle:buttonTitle + style:buttonStyle + handler:^(__unused UIAlertAction *action) { + if (callback) { + if (allowsTextInput) { + callback(@[buttonKey, textField.text]); + } else { + callback(@[buttonKey]); + } + } + }]]; + } + + [presentingController presentViewController:alertController animated:YES completion:nil]; } - - [_alerts addObject:alertView]; - [_alertCallbacks addObject:callback ?: ^(__unused id unused) {}]; - [_alertButtonKeys addObject:buttonKeys]; - - [alertView show]; } #pragma mark - UIAlertViewDelegate @@ -122,17 +181,14 @@ RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args RCTAssert(index != NSNotFound, @"Dismissed alert was not recognised"); RCTResponseSenderBlock callback = _alertCallbacks[index]; - NSArray *buttonKeys = _alertButtonKeys[index]; - NSArray *args; + NSArray *buttonKeys = _alertButtonKeys[index]; if (alertView.alertViewStyle == UIAlertViewStylePlainTextInput) { - args = @[buttonKeys[buttonIndex], [alertView textFieldAtIndex:0].text]; + callback(@[buttonKeys[buttonIndex], [alertView textFieldAtIndex:0].text]); } else { - args = @[buttonKeys[buttonIndex]]; + callback(@[buttonKeys[buttonIndex]]); } - callback(args); - [_alerts removeObjectAtIndex:index]; [_alertCallbacks removeObjectAtIndex:index]; [_alertButtonKeys removeObjectAtIndex:index]; diff --git a/React/Modules/RCTAsyncLocalStorage.h b/React/Modules/RCTAsyncLocalStorage.h index e7e871b02..e6c129ef2 100644 --- a/React/Modules/RCTAsyncLocalStorage.h +++ b/React/Modules/RCTAsyncLocalStorage.h @@ -27,11 +27,8 @@ @property (nonatomic, readonly, getter=isValid) BOOL valid; -- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; -- (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback; -- (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; -- (void)clear:(RCTResponseSenderBlock)callback; -- (void)getAllKeys:(RCTResponseSenderBlock)callback; +// Clear the RCTAsyncLocalStorage data from native code +- (void)clearAllData; // For clearing data when the bridge may not exist, e.g. when logging out. + (void)clearAllData; diff --git a/React/Modules/RCTAsyncLocalStorage.m b/React/Modules/RCTAsyncLocalStorage.m index 1ef86eb84..cb8e24857 100644 --- a/React/Modules/RCTAsyncLocalStorage.m +++ b/React/Modules/RCTAsyncLocalStorage.m @@ -14,6 +14,7 @@ #import #import +#import "RCTConvert.h" #import "RCTLog.h" #import "RCTUtils.h" @@ -23,7 +24,7 @@ static const NSUInteger RCTInlineValueThreshold = 100; #pragma mark - Static helper functions -static id RCTErrorForKey(NSString *key) +static NSDictionary *RCTErrorForKey(NSString *key) { if (![key isKindOfClass:[NSString class]]) { return RCTMakeAndLogError(@"Invalid key - must be a string. Key: ", key, @{@"key": key}); @@ -34,7 +35,7 @@ static id RCTErrorForKey(NSString *key) } } -static void RCTAppendError(id error, NSMutableArray **errors) +static void RCTAppendError(NSDictionary *error, NSMutableArray **errors) { if (error && errors) { if (!*errors) { @@ -44,7 +45,7 @@ static void RCTAppendError(id error, NSMutableArray **errors) } } -static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut) +static NSString *RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut) { if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { NSError *error; @@ -122,12 +123,12 @@ static dispatch_queue_t RCTGetMethodQueue() } static BOOL RCTHasCreatedStorageDirectory = NO; -static NSError *RCTDeleteStorageDirectory() +static NSDictionary *RCTDeleteStorageDirectory() { NSError *error; [[NSFileManager defaultManager] removeItemAtPath:RCTGetStorageDirectory() error:&error]; RCTHasCreatedStorageDirectory = NO; - return error; + return error ? RCTMakeError(@"Failed to delete storage directory.", error, nil) : nil; } #pragma mark - RCTAsyncLocalStorage @@ -148,6 +149,14 @@ RCT_EXPORT_MODULE() return RCTGetMethodQueue(); } +- (void)clearAllData +{ + dispatch_async(RCTGetMethodQueue(), ^{ + _manifest = [NSMutableDictionary new]; + RCTDeleteStorageDirectory(); + }); +} + + (void)clearAllData { dispatch_async(RCTGetMethodQueue(), ^{ @@ -181,7 +190,7 @@ RCT_EXPORT_MODULE() return [RCTGetStorageDirectory() stringByAppendingPathComponent:safeFileName]; } -- (id)_ensureSetup +- (NSDictionary *)_ensureSetup { RCTAssertThread(RCTGetMethodQueue(), @"Must be executed on storage thread"); @@ -199,7 +208,7 @@ RCT_EXPORT_MODULE() if (!_haveSetup) { NSDictionary *errorOut; NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), nil, &errorOut); - _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [NSMutableDictionary new]; + _manifest = serialized ? RCTJSONParseMutable(serialized, &error) : [NSMutableDictionary new]; if (error) { RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); _manifest = [NSMutableDictionary new]; @@ -209,12 +218,12 @@ RCT_EXPORT_MODULE() return nil; } -- (id)_writeManifest:(NSMutableArray **)errors +- (NSDictionary *)_writeManifest:(NSMutableArray **)errors { NSError *error; NSString *serialized = RCTJSONStringify(_manifest, &error); [serialized writeToFile:RCTGetManifestFilePath() atomically:YES encoding:NSUTF8StringEncoding error:&error]; - id errorOut; + NSDictionary *errorOut; if (error) { errorOut = RCTMakeError(@"Failed to write manifest file.", error, nil); RCTAppendError(errorOut, errors); @@ -222,20 +231,21 @@ RCT_EXPORT_MODULE() return errorOut; } -- (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result +- (NSDictionary *)_appendItemForKey:(NSString *)key + toArray:(NSMutableArray *> *)result { - id errorOut = RCTErrorForKey(key); + NSDictionary *errorOut = RCTErrorForKey(key); if (errorOut) { return errorOut; } - id value = [self _getValueForKey:key errorOut:&errorOut]; + NSString *value = [self _getValueForKey:key errorOut:&errorOut]; [result addObject:@[key, RCTNullIfNil(value)]]; // Insert null if missing or failure. return errorOut; } - (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut { - id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. + id value = _manifest[key]; // nil means missing, null means there is a data file, else: NSString if (value == (id)kCFNull) { NSString *filePath = [self _filePathForKey:key]; value = RCTReadFile(filePath, key, errorOut); @@ -243,16 +253,13 @@ RCT_EXPORT_MODULE() return value; } -- (id)_writeEntry:(NSArray *)entry +- (NSDictionary *)_writeEntry:(NSArray *)entry { - if (![entry isKindOfClass:[NSArray class]] || entry.count != 2) { + if (entry.count != 2) { return RCTMakeAndLogError(@"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); } - if (![entry[1] isKindOfClass:[NSString class]]) { - return RCTMakeAndLogError(@"Values must be strings, got: ", entry[1], @{@"key": entry[0]}); - } NSString *key = entry[0]; - id errorOut = RCTErrorForKey(key); + NSDictionary *errorOut = RCTErrorForKey(key); if (errorOut) { return errorOut; } @@ -278,66 +285,63 @@ RCT_EXPORT_MODULE() #pragma mark - Exported JS Functions -RCT_EXPORT_METHOD(multiGet:(NSArray *)keys +RCT_EXPORT_METHOD(multiGet:(NSStringArray *)keys callback:(RCTResponseSenderBlock)callback) { - if (!callback) { - RCTLogError(@"Called getItem without a callback."); - return; - } - - id errorOut = [self _ensureSetup]; + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[@[errorOut], (id)kCFNull]); return; } - NSMutableArray *errors; - NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; + NSMutableArray *errors; + NSMutableArray *> *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; for (NSString *key in keys) { - id keyError = [self _appendItemForKey:key toArray:result]; + NSDictionary *keyError = [self _appendItemForKey:key toArray:result]; RCTAppendError(keyError, &errors); } callback(@[RCTNullIfNil(errors), result]); } -RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs +RCT_EXPORT_METHOD(multiSet:(NSStringArrayArray *)kvPairs callback:(RCTResponseSenderBlock)callback) { - id errorOut = [self _ensureSetup]; + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[@[errorOut]]); return; } - NSMutableArray *errors; - for (NSArray *entry in kvPairs) { - id keyError = [self _writeEntry:entry]; + NSMutableArray *errors; + for (NSArray *entry in kvPairs) { + NSDictionary *keyError = [self _writeEntry:entry]; RCTAppendError(keyError, &errors); } [self _writeManifest:&errors]; - if (callback) { - callback(@[RCTNullIfNil(errors)]); - } + callback(@[RCTNullIfNil(errors)]); } -RCT_EXPORT_METHOD(multiMerge:(NSArray *)kvPairs - callback:(RCTResponseSenderBlock)callback) +RCT_EXPORT_METHOD(multiMerge:(NSStringArrayArray *)kvPairs + callback:(RCTResponseSenderBlock)callback) { - id errorOut = [self _ensureSetup]; + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[@[errorOut]]); return; } - NSMutableArray *errors; - for (__strong NSArray *entry in kvPairs) { - id keyError; + NSMutableArray *errors; + for (__strong NSArray *entry in kvPairs) { + NSDictionary *keyError; NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; if (keyError) { RCTAppendError(keyError, &errors); } else { if (value) { - NSMutableDictionary *mergedVal = [RCTJSONParseMutable(value, &keyError) mutableCopy]; - RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &keyError)); - entry = @[entry[0], RCTJSONStringify(mergedVal, &keyError)]; + NSError *jsonError; + NSMutableDictionary *mergedVal = RCTJSONParseMutable(value, &jsonError); + RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &jsonError)); + entry = @[entry[0], RCTNullIfNil(RCTJSONStringify(mergedVal, NULL))]; + if (jsonError) { + keyError = RCTJSErrorFromNSError(jsonError); + } } if (!keyError) { keyError = [self _writeEntry:entry]; @@ -346,22 +350,20 @@ RCT_EXPORT_METHOD(multiMerge:(NSArray *)kvPairs } } [self _writeManifest:&errors]; - if (callback) { - callback(@[RCTNullIfNil(errors)]); - } + callback(@[RCTNullIfNil(errors)]); } -RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys +RCT_EXPORT_METHOD(multiRemove:(NSStringArray *)keys callback:(RCTResponseSenderBlock)callback) { - id errorOut = [self _ensureSetup]; + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[@[errorOut]]); return; } - NSMutableArray *errors; + NSMutableArray *errors; for (NSString *key in keys) { - id keyError = RCTErrorForKey(key); + NSDictionary *keyError = RCTErrorForKey(key); if (!keyError) { NSString *filePath = [self _filePathForKey:key]; [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; @@ -370,23 +372,19 @@ RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys RCTAppendError(keyError, &errors); } [self _writeManifest:&errors]; - if (callback) { - callback(@[RCTNullIfNil(errors)]); - } + callback(@[RCTNullIfNil(errors)]); } RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { _manifest = [NSMutableDictionary new]; - NSError *error = RCTDeleteStorageDirectory(); - if (callback) { - callback(@[RCTNullIfNil(error)]); - } + NSDictionary *error = RCTDeleteStorageDirectory(); + callback(@[RCTNullIfNil(error)]); } RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) { - id errorOut = [self _ensureSetup]; + NSDictionary *errorOut = [self _ensureSetup]; if (errorOut) { callback(@[errorOut, (id)kCFNull]); } else { diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index db24baf0a..165237ce0 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -15,7 +15,6 @@ #import "RCTEventDispatcher.h" #import "RCTKeyCommands.h" #import "RCTLog.h" -#import "RCTPerfStats.h" #import "RCTProfile.h" #import "RCTRootView.h" #import "RCTSourceCode.h" @@ -139,8 +138,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) NSURLSessionDataTask *_updateTask; NSURL *_liveReloadURL; BOOL _jsLoaded; - NSArray *_presentedItems; - NSMutableArray *_extraMenuItems; + NSArray *_presentedItems; + NSMutableArray *_extraMenuItems; + NSString *_webSocketExecutorName; + NSString *_executorOverride; } @synthesize bridge = _bridge; @@ -182,22 +183,6 @@ RCT_EXPORT_MODULE() __weak RCTDevMenu *weakSelf = self; - [_extraMenuItems addObject:[RCTDevMenuItem toggleItemWithKey:@"showFPS" - title:@"Show FPS Monitor" - selectedTitle:@"Hide FPS Monitor" - handler:^(BOOL showFPS) - { - RCTDevMenu *strongSelf = weakSelf; - if (strongSelf) { - strongSelf->_showFPS = showFPS; - if (showFPS) { - [strongSelf.bridge.perfStats show]; - } else { - [strongSelf.bridge.perfStats hide]; - } - } - }]]; - [_extraMenuItems addObject:[RCTDevMenuItem toggleItemWithKey:@"showInspector" title:@"Show Inspector" selectedTitle:@"Hide Inspector" @@ -206,6 +191,13 @@ RCT_EXPORT_MODULE() [weakSelf.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; }]]; + _webSocketExecutorName = [_defaults objectForKey:@"websocket-executor-name"] ?: @"Chrome"; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _executorOverride = [_defaults objectForKey:@"executor-override"]; + }); + // Delay setup until after Bridge init dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf updateSettings:_settings]; @@ -283,7 +275,7 @@ RCT_EXPORT_MODULE() self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue]; self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue]; self.showFPS = [_settings[@"showFPS"] ?: @NO boolValue]; - self.executorClass = NSClassFromString(_settings[@"executorClass"]); + self.executorClass = NSClassFromString(_executorOverride ?: _settings[@"executorClass"]); } /** @@ -392,9 +384,9 @@ RCT_EXPORT_MODULE() [self settingsDidChange]; } -- (NSArray *)menuItems +- (NSArray *)menuItems { - NSMutableArray *items = [NSMutableArray new]; + NSMutableArray *items = [NSMutableArray new]; // Add built-in items @@ -406,13 +398,18 @@ RCT_EXPORT_MODULE() Class chromeExecutorClass = NSClassFromString(@"RCTWebSocketExecutor"); if (!chromeExecutorClass) { - [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Chrome Debugger Unavailable" handler:^{ - UIAlertView *alert = RCTAlertView(@"Chrome Debugger Unavailable", @"You need to include the RCTWebSocket library to enable Chrome debugging", nil, @"OK", nil); + [items addObject:[RCTDevMenuItem buttonItemWithTitle:[NSString stringWithFormat:@"%@ Debugger Unavailable", _webSocketExecutorName] handler:^{ + UIAlertView *alert = RCTAlertView( + [NSString stringWithFormat:@"%@ Debugger Unavailable", _webSocketExecutorName], + [NSString stringWithFormat:@"You need to include the RCTWebSocket library to enable %@ debugging", _webSocketExecutorName], + nil, + @"OK", + nil); [alert show]; }]]; } else { BOOL isDebuggingInChrome = _executorClass && _executorClass == chromeExecutorClass; - NSString *debugTitleChrome = isDebuggingInChrome ? @"Disable Chrome Debugging" : @"Debug in Chrome"; + NSString *debugTitleChrome = isDebuggingInChrome ? [NSString stringWithFormat:@"Disable %@ Debugging", _webSocketExecutorName] : [NSString stringWithFormat:@"Debug in %@", _webSocketExecutorName]; [items addObject:[RCTDevMenuItem buttonItemWithTitle:debugTitleChrome handler:^{ weakSelf.executorClass = isDebuggingInChrome ? Nil : chromeExecutorClass; }]]; @@ -452,7 +449,7 @@ RCT_EXPORT_METHOD(show) actionSheet.title = @"React Native: Development"; actionSheet.delegate = self; - NSArray *items = [self menuItems]; + NSArray *items = [self menuItems]; for (RCTDevMenuItem *item in items) { switch (item.type) { case RCTDevMenuTypeButton: { @@ -471,7 +468,7 @@ RCT_EXPORT_METHOD(show) actionSheet.cancelButtonIndex = actionSheet.numberOfButtons - 1; actionSheet.actionSheetStyle = UIBarStyleBlack; - [actionSheet showInView:RCTSharedApplication().keyWindow.rootViewController.view]; + [actionSheet showInView:RCTKeyWindow().rootViewController.view]; _actionSheet = actionSheet; _presentedItems = items; } @@ -544,6 +541,7 @@ RCT_EXPORT_METHOD(reload) { if (_executorClass != executorClass) { _executorClass = executorClass; + _executorOverride = nil; [self updateSetting:@"executorClass" value:NSStringFromClass(executorClass)]; } diff --git a/React/Modules/RCTExceptionsManager.h b/React/Modules/RCTExceptionsManager.h index adb575501..20ca8d26e 100644 --- a/React/Modules/RCTExceptionsManager.h +++ b/React/Modules/RCTExceptionsManager.h @@ -13,14 +13,10 @@ @protocol RCTExceptionsManagerDelegate -// NOTE: Remove these three methods and the @optional directive after updating the codebase to use only the three below -@optional -- (void)handleSoftJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack; -- (void)handleFatalJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack; -- (void)updateJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack; - - (void)handleSoftJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId; - (void)handleFatalJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId; + +@optional - (void)updateJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId; @end diff --git a/React/Modules/RCTExceptionsManager.m b/React/Modules/RCTExceptionsManager.m index ac4048d2c..9cc5d5722 100644 --- a/React/Modules/RCTExceptionsManager.m +++ b/React/Modules/RCTExceptionsManager.m @@ -9,6 +9,7 @@ #import "RCTExceptionsManager.h" +#import "RCTConvert.h" #import "RCTDefines.h" #import "RCTLog.h" #import "RCTRedBox.h" @@ -39,84 +40,53 @@ RCT_EXPORT_MODULE() } RCT_EXPORT_METHOD(reportSoftException:(NSString *)message - stack:(NSArray *)stack + stack:(NSDictionaryArray *)stack exceptionId:(nonnull NSNumber *)exceptionId) { - // TODO(#7070533): report a soft error to the server - if (_delegate) { - if ([_delegate respondsToSelector:@selector(handleSoftJSExceptionWithMessage:stack:exceptionId:)]) { - [_delegate handleSoftJSExceptionWithMessage:message stack:stack exceptionId:exceptionId]; - } else { - [_delegate handleSoftJSExceptionWithMessage:message stack:stack]; - } - return; - } [_bridge.redBox showErrorMessage:message withStack:stack]; + + if (_delegate) { + [_delegate handleSoftJSExceptionWithMessage:message stack:stack exceptionId:exceptionId]; + } } RCT_EXPORT_METHOD(reportFatalException:(NSString *)message - stack:(NSArray *)stack + stack:(NSDictionaryArray *)stack exceptionId:(nonnull NSNumber *)exceptionId) { - if (_delegate) { - if ([_delegate respondsToSelector:@selector(handleFatalJSExceptionWithMessage:stack:exceptionId:)]) { - [_delegate handleFatalJSExceptionWithMessage:message stack:stack exceptionId:exceptionId]; - } else { - [_delegate handleFatalJSExceptionWithMessage:message stack:stack]; - } - return; - } - [_bridge.redBox showErrorMessage:message withStack:stack]; - if (!RCT_DEBUG) { + if (_delegate) { + [_delegate handleFatalJSExceptionWithMessage:message stack:stack exceptionId:exceptionId]; + } - static NSUInteger reloadRetries = 0; - const NSUInteger maxMessageLength = 75; - - if (reloadRetries < _maxReloadAttempts) { - - reloadRetries++; - [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification - object:nil]; - - } else { - - if (message.length > maxMessageLength) { - message = [[message substringToIndex:maxMessageLength] stringByAppendingString:@"..."]; - } - - NSMutableString *prettyStack = [NSMutableString stringWithString:@"\n"]; - for (NSDictionary *frame in stack) { - [prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]]; - } - - NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:message]; - [NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack]; - } + static NSUInteger reloadRetries = 0; + if (!RCT_DEBUG && reloadRetries < _maxReloadAttempts) { + reloadRetries++; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil]; + } else { + NSString *description = [@"Unhandled JS Exception: " stringByAppendingString:message]; + NSDictionary *errorInfo = @{ NSLocalizedDescriptionKey: description, RCTJSStackTraceKey: stack }; + RCTFatal([NSError errorWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]); } } RCT_EXPORT_METHOD(updateExceptionMessage:(NSString *)message - stack:(NSArray *)stack + stack:(NSDictionaryArray *)stack exceptionId:(nonnull NSNumber *)exceptionId) { - if (_delegate) { - if ([_delegate respondsToSelector:@selector(updateJSExceptionWithMessage:stack:exceptionId:)]) { - [_delegate updateJSExceptionWithMessage:message stack:stack exceptionId:exceptionId]; - } else { - [_delegate updateJSExceptionWithMessage:message stack:stack]; - } - return; - } - [_bridge.redBox updateErrorMessage:message withStack:stack]; + + if (_delegate && [_delegate respondsToSelector:@selector(updateJSExceptionWithMessage:stack:exceptionId:)]) { + [_delegate updateJSExceptionWithMessage:message stack:stack exceptionId:exceptionId]; + } } // Deprecated. Use reportFatalException directly instead. RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message - stack:(NSArray *)stack) + stack:(NSDictionaryArray *)stack) { [self reportFatalException:message stack:stack exceptionId:@-1]; } + @end diff --git a/React/Modules/RCTRedBox.h b/React/Modules/RCTRedBox.h index a90f43deb..7703f5f8b 100644 --- a/React/Modules/RCTRedBox.h +++ b/React/Modules/RCTRedBox.h @@ -17,8 +17,8 @@ - (void)showError:(NSError *)error; - (void)showErrorMessage:(NSString *)message; - (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details; -- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack; -- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack; +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack; +- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack; - (void)dismiss; diff --git a/React/Modules/RCTRedBox.m b/React/Modules/RCTRedBox.m index 733a43713..a334c7533 100644 --- a/React/Modules/RCTRedBox.m +++ b/React/Modules/RCTRedBox.m @@ -23,7 +23,7 @@ { UITableView *_stackTraceTableView; NSString *_lastErrorMessage; - NSArray *_lastStackTrace; + NSArray *_lastStackTrace; } - (instancetype)initWithFrame:(CGRect)frame @@ -103,7 +103,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [[[NSURLSession sharedSession] dataTaskWithRequest:request] resume]; } -- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow { if ((self.hidden && shouldShow) || (!self.hidden && [_lastErrorMessage isEqualToString:message])) { _lastStackTrace = stack; @@ -193,8 +193,14 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) } cell.textLabel.text = stackFrame[@"methodName"]; - cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ @ %@:%@", - [stackFrame[@"file"] lastPathComponent], stackFrame[@"lineNumber"], stackFrame[@"column"]]; + if (stackFrame[@"file"]) { + cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ @ %zd:%zd", + [stackFrame[@"file"] lastPathComponent], + [stackFrame[@"lineNumber"] integerValue], + [stackFrame[@"column"] integerValue]]; + } else { + cell.detailTextLabel.text = @""; + } return cell; } @@ -225,7 +231,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) #pragma mark - Key commands -- (NSArray *)keyCommands +- (NSArray *)keyCommands { // NOTE: We could use RCTKeyCommands for this, but since // we control this window, we can use the standard, non-hacky @@ -281,17 +287,17 @@ RCT_EXPORT_MODULE() [self showErrorMessage:combinedMessage]; } -- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack { [self showErrorMessage:message withStack:stack showIfHidden:YES]; } -- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack +- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack { [self showErrorMessage:message withStack:stack showIfHidden:NO]; } -- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow { dispatch_async(dispatch_get_main_queue(), ^{ if (!_window) { @@ -332,9 +338,9 @@ RCT_EXPORT_MODULE() - (void)showError:(NSError *)message {} - (void)showErrorMessage:(NSString *)message {} - (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details {} -- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack {} -- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack {} -- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow {} +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack {} +- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack {} +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow {} - (void)dismiss {} @end diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m index 9da28d9df..41d90d41a 100644 --- a/React/Modules/RCTTiming.m +++ b/React/Modules/RCTTiming.m @@ -145,7 +145,7 @@ RCT_EXPORT_MODULE() - (void)didUpdateFrame:(__unused RCTFrameUpdate *)update { - NSMutableArray *timersToCall = [NSMutableArray new]; + NSMutableArray *timersToCall = [NSMutableArray new]; for (RCTTimer *timer in _timers.allObjects) { if ([timer updateFoundNeedsJSUpdate]) { [timersToCall addObject:timer.callbackID]; diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 87ae23d80..0f675a518 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -24,6 +24,7 @@ #import "RCTLog.h" #import "RCTProfile.h" #import "RCTRootView.h" +#import "RCTRootViewInternal.h" #import "RCTScrollableProtocol.h" #import "RCTShadowView.h" #import "RCTSparseArray.h" @@ -190,9 +191,8 @@ static UIViewAnimationOptions UIViewAnimationOptionsFromRCTAnimationType(RCTAnim dispatch_queue_t _shadowQueue; // Root views are only mutated on the shadow queue - NSMutableSet *_rootViewTags; - NSMutableArray *_pendingUIBlocks; - NSLock *_pendingUIBlocksLock; + NSMutableSet *_rootViewTags; + NSMutableArray *_pendingUIBlocks; // Animation RCTLayoutAnimation *_nextLayoutAnimation; // RCT thread only @@ -201,7 +201,7 @@ static UIViewAnimationOptions UIViewAnimationOptionsFromRCTAnimationType(RCTAnim // Keyed by viewName NSDictionary *_componentDataByName; - NSMutableSet *_bridgeTransactionListeners; + NSMutableSet> *_bridgeTransactionListeners; } @synthesize bridge = _bridge; @@ -219,8 +219,6 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); _shadowQueue = dispatch_queue_create("com.facebook.React.ShadowQueue", DISPATCH_QUEUE_SERIAL); - _pendingUIBlocksLock = [NSLock new]; - _shadowViewRegistry = [RCTSparseArray new]; _viewRegistry = [RCTSparseArray new]; @@ -252,6 +250,9 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); * Called on the JS Thread since all modules are invalidated on the JS thread */ + // This only accessed from the shadow queue + _pendingUIBlocks = nil; + dispatch_async(dispatch_get_main_queue(), ^{ for (NSNumber *rootViewTag in _rootViewTags) { [_viewRegistry[rootViewTag] invalidate]; @@ -263,10 +264,6 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); _bridgeTransactionListeners = nil; _bridge = nil; - [_pendingUIBlocksLock lock]; - _pendingUIBlocks = nil; - [_pendingUIBlocksLock unlock]; - [[NSNotificationCenter defaultCenter] removeObserver:self]; }); } @@ -347,11 +344,28 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); { RCTAssertMainThread(); + // The following variable has no meaning if the view is not a react root view + RCTRootViewSizeFlexibility sizeFlexibility = RCTRootViewSizeFlexibilityNone; + + if (RCTIsReactRootView(view.reactTag)) { + RCTRootView *rootView = (RCTRootView *)[view superview]; + if (rootView != nil) { + sizeFlexibility = rootView.sizeFlexibility; + } + } + NSNumber *reactTag = view.reactTag; dispatch_async(_shadowQueue, ^{ RCTShadowView *rootShadowView = _shadowViewRegistry[reactTag]; RCTAssert(rootShadowView != nil, @"Could not locate root view with tag #%@", reactTag); - rootShadowView.frame = frame; + + if (RCTIsReactRootView(reactTag)) { + rootShadowView.frame = frame; + rootShadowView.sizeFlexibility = sizeFlexibility; + } else { + rootShadowView.frame = frame; + } + [rootShadowView updateLayout]; [self batchDidComplete]; @@ -382,7 +396,8 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); /** * Unregisters views from registries */ -- (void)_purgeChildren:(NSArray *)children fromRegistry:(RCTSparseArray *)registry +- (void)_purgeChildren:(NSArray> *)children + fromRegistry:(RCTSparseArray *)registry { for (id child in children) { RCTTraverseViewNodes(registry[child.reactTag], ^(id subview) { @@ -421,30 +436,31 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); } }; - [_pendingUIBlocksLock lock]; [_pendingUIBlocks addObject:outerBlock]; - [_pendingUIBlocksLock unlock]; } - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)rootShadowView { RCTAssert(![NSThread isMainThread], @"Should be called on shadow thread"); - NSMutableSet *viewsWithNewFrames = [NSMutableSet setWithCapacity:1]; + NSMutableSet *viewsWithNewFrames = [NSMutableSet setWithCapacity:1]; // This is nuanced. In the JS thread, we create a new update buffer // `frameTags`/`frames` that is created/mutated in the JS thread. We access // these structures in the UI-thread block. `NSMutableArray` is not thread // safe so we rely on the fact that we never mutate it after it's passed to // the main thread. - [rootShadowView collectRootUpdatedFrames:viewsWithNewFrames - parentConstraint:(CGSize){CSS_UNDEFINED, CSS_UNDEFINED}]; + [rootShadowView collectRootUpdatedFrames:viewsWithNewFrames]; // Parallel arrays are built and then handed off to main thread - NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - NSMutableArray *frames = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - NSMutableArray *areNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - NSMutableArray *parentsAreNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; + NSMutableArray *frameReactTags = + [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; + NSMutableArray *frames = + [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; + NSMutableArray *areNew = + [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; + NSMutableArray *parentsAreNew = + [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; for (RCTShadowView *shadowView in viewsWithNewFrames) { [frameReactTags addObject:shadowView.reactTag]; @@ -462,7 +478,7 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); // reactSetFrame: has been called. Note that if reactSetFrame: is not called, // these won't be called either, so this is not a suitable place to update // properties that aren't related to layout. - NSMutableArray *updateBlocks = [NSMutableArray new]; + NSMutableArray *updateBlocks = [NSMutableArray new]; for (RCTShadowView *shadowView in viewsWithNewFrames) { RCTViewManager *manager = [_componentDataByName[shadowView.viewName] manager]; RCTViewManagerUIBlock block = [manager uiBlockToAmendWithShadowView:shadowView]; @@ -477,11 +493,30 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); }, }); } + + if (RCTIsReactRootView(shadowView.reactTag)) { + NSNumber *reactTag = shadowView.reactTag; + CGSize contentSize = shadowView.frame.size; + + dispatch_async(dispatch_get_main_queue(), ^{ + UIView *view = _viewRegistry[reactTag]; + RCTAssert(view != nil, @"view (for ID %@) not found", reactTag); + + RCTRootView *rootView = (RCTRootView *)[view superview]; + + rootView.intrinsicSize = contentSize; + }); + } + if (block) { [updateBlocks addObject:block]; } } + if (!viewsWithNewFrames.count) { + // no frame change results in no UI update block + return nil; + } // Perform layout (possibly animated) return ^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { RCTResponseSenderBlock callback = self->_layoutAnimation.callback; @@ -548,26 +583,21 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); } withCompletionBlock:nil]; } } - - /** - * TODO(tadeu): Remove it once and for all - */ - for (id node in _bridgeTransactionListeners) { - [node reactBridgeDidFinishTransaction]; - } }; } - (void)_amendPendingUIBlocksWithStylePropagationUpdateForRootView:(RCTShadowView *)topView { - NSMutableSet *applierBlocks = [NSMutableSet setWithCapacity:1]; + NSMutableSet *applierBlocks = [NSMutableSet setWithCapacity:1]; [topView collectUpdatedProperties:applierBlocks parentProperties:@{}]; - [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - for (RCTApplierBlock block in applierBlocks) { - block(viewRegistry); - } - }]; + if (applierBlocks.count) { + [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + for (RCTApplierBlock block in applierBlocks) { + block(viewRegistry); + } + }]; + } } /** @@ -580,7 +610,7 @@ RCT_EXPORT_METHOD(removeSubviewsFromContainerWithID:(nonnull NSNumber *)containe RCTAssert(container != nil, @"container view (for ID %@) not found", containerID); NSUInteger subviewsCount = [container reactSubviews].count; - NSMutableArray *indices = [[NSMutableArray alloc] initWithCapacity:subviewsCount]; + NSMutableArray *indices = [[NSMutableArray alloc] initWithCapacity:subviewsCount]; for (NSUInteger childIndex = 0; childIndex < subviewsCount; childIndex++) { [indices addObject:@(childIndex)]; } @@ -599,8 +629,8 @@ RCT_EXPORT_METHOD(removeSubviewsFromContainerWithID:(nonnull NSNumber *)containe * * @returns Array of removed items. */ -- (NSArray *)_childrenToRemoveFromContainer:(id)container - atIndices:(NSArray *)atIndices +- (NSArray> *)_childrenToRemoveFromContainer:(id)container + atIndices:(NSArray *)atIndices { // If there are no indices to move or the container has no subviews don't bother // We support parents with nil subviews so long as they're all nil so this allows for this behavior @@ -608,7 +638,7 @@ RCT_EXPORT_METHOD(removeSubviewsFromContainerWithID:(nonnull NSNumber *)containe return nil; } // Construction of removed children must be done "up front", before indices are disturbed by removals. - NSMutableArray *removedChildren = [NSMutableArray arrayWithCapacity:atIndices.count]; + NSMutableArray> *removedChildren = [NSMutableArray arrayWithCapacity:atIndices.count]; RCTAssert(container != nil, @"container view (for ID %@) not found", container); for (NSNumber *indexNumber in atIndices) { NSUInteger index = indexNumber.unsignedIntegerValue; @@ -617,15 +647,17 @@ RCT_EXPORT_METHOD(removeSubviewsFromContainerWithID:(nonnull NSNumber *)containe } } if (removedChildren.count != atIndices.count) { - RCTLogMustFix(@"removedChildren count (%tu) was not what we expected (%tu)", - removedChildren.count, atIndices.count); + NSString *message = [NSString stringWithFormat:@"removedChildren count (%tu) was not what we expected (%tu)", + removedChildren.count, atIndices.count]; + RCTFatal(RCTErrorWithMessage(message)); } return removedChildren; } -- (void)_removeChildren:(NSArray *)children fromContainer:(id)container +- (void)_removeChildren:(NSArray> *)children + fromContainer:(id)container { - for (id removedChild in children) { + for (id removedChild in children) { [container removeReactSubview:removedChild]; } } @@ -634,14 +666,14 @@ RCT_EXPORT_METHOD(removeRootView:(nonnull NSNumber *)rootReactTag) { RCTShadowView *rootShadowView = _shadowViewRegistry[rootReactTag]; RCTAssert(rootShadowView.superview == nil, @"root view cannot have superview (ID %@)", rootReactTag); - [self _purgeChildren:rootShadowView.reactSubviews fromRegistry:_shadowViewRegistry]; + [self _purgeChildren:(NSArray> *)rootShadowView.reactSubviews fromRegistry:_shadowViewRegistry]; _shadowViewRegistry[rootReactTag] = nil; [_rootViewTags removeObject:rootReactTag]; [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ RCTAssertMainThread(); UIView *rootView = viewRegistry[rootReactTag]; - [uiManager _purgeChildren:rootView.reactSubviews fromRegistry:viewRegistry]; + [uiManager _purgeChildren:(NSArray> *)rootView.reactSubviews fromRegistry:viewRegistry]; viewRegistry[rootReactTag] = nil; [[NSNotificationCenter defaultCenter] postNotificationName:RCTUIManagerDidRemoveRootViewNotification @@ -650,7 +682,8 @@ RCT_EXPORT_METHOD(removeRootView:(nonnull NSNumber *)rootReactTag) }]; } -RCT_EXPORT_METHOD(replaceExistingNonRootView:(nonnull NSNumber *)reactTag withView:(nonnull NSNumber *)newReactTag) +RCT_EXPORT_METHOD(replaceExistingNonRootView:(nonnull NSNumber *)reactTag + withView:(nonnull NSNumber *)newReactTag) { RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; RCTAssert(shadowView != nil, @"shadowView (for ID %@) not found", reactTag); @@ -660,8 +693,8 @@ RCT_EXPORT_METHOD(replaceExistingNonRootView:(nonnull NSNumber *)reactTag withVi NSUInteger indexOfView = [superShadowView.reactSubviews indexOfObject:shadowView]; RCTAssert(indexOfView != NSNotFound, @"View's superview doesn't claim it as subview (id %@)", reactTag); - NSArray *removeAtIndices = @[@(indexOfView)]; - NSArray *addTags = @[newReactTag]; + NSArray *removeAtIndices = @[@(indexOfView)]; + NSArray *addTags = @[newReactTag]; [self manageChildren:superShadowView.reactTag moveFromIndices:nil moveToIndices:nil @@ -671,11 +704,11 @@ RCT_EXPORT_METHOD(replaceExistingNonRootView:(nonnull NSNumber *)reactTag withVi } RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag - moveFromIndices:(NSArray *)moveFromIndices - moveToIndices:(NSArray *)moveToIndices - addChildReactTags:(NSArray *)addChildReactTags - addAtIndices:(NSArray *)addAtIndices - removeAtIndices:(NSArray *)removeAtIndices) + moveFromIndices:(NSNumberArray *)moveFromIndices + moveToIndices:(NSNumberArray *)moveToIndices + addChildReactTags:(NSNumberArray *)addChildReactTags + addAtIndices:(NSNumberArray *)addAtIndices + removeAtIndices:(NSNumberArray *)removeAtIndices) { [self _manageChildren:containerReactTag moveFromIndices:moveFromIndices @@ -697,11 +730,11 @@ RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag } - (void)_manageChildren:(NSNumber *)containerReactTag - moveFromIndices:(NSArray *)moveFromIndices - moveToIndices:(NSArray *)moveToIndices - addChildReactTags:(NSArray *)addChildReactTags - addAtIndices:(NSArray *)addAtIndices - removeAtIndices:(NSArray *)removeAtIndices + moveFromIndices:(NSArray *)moveFromIndices + moveToIndices:(NSArray *)moveToIndices + addChildReactTags:(NSArray *)addChildReactTags + addAtIndices:(NSArray *)addAtIndices + removeAtIndices:(NSArray *)removeAtIndices registry:(RCTSparseArray *)registry { id container = registry[containerReactTag]; @@ -709,8 +742,10 @@ RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add"); // Removes (both permanent and temporary moves) are using "before" indices - NSArray *permanentlyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices]; - NSArray *temporarilyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:moveFromIndices]; + NSArray> *permanentlyRemovedChildren = + [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices]; + NSArray> *temporarilyRemovedChildren = + [self _childrenToRemoveFromContainer:container atIndices:moveFromIndices]; [self _removeChildren:permanentlyRemovedChildren fromContainer:container]; [self _removeChildren:temporarilyRemovedChildren fromContainer:container]; @@ -724,15 +759,17 @@ RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag destinationsToChildrenToAdd[moveToIndices[index]] = temporarilyRemovedChildren[index]; } for (NSInteger index = 0, length = addAtIndices.count; index < length; index++) { - id view = registry[addChildReactTags[index]]; + id view = registry[addChildReactTags[index]]; if (view) { destinationsToChildrenToAdd[addAtIndices[index]] = view; } } - NSArray *sortedIndices = [destinationsToChildrenToAdd.allKeys sortedArrayUsingSelector:@selector(compare:)]; + NSArray *sortedIndices = + [destinationsToChildrenToAdd.allKeys sortedArrayUsingSelector:@selector(compare:)]; for (NSNumber *reactIndex in sortedIndices) { - [container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:reactIndex.integerValue]; + [container insertReactSubview:destinationsToChildrenToAdd[reactIndex] + atIndex:reactIndex.integerValue]; } } @@ -824,8 +861,6 @@ RCT_EXPORT_METHOD(findSubviewIn:(nonnull NSNumber *)reactTag atPoint:(CGPoint)po - (void)batchDidComplete { - RCTProfileBeginEvent(0, @"[RCTUIManager batchDidComplete]", nil); - // Gather blocks to be executed now that all view hierarchy manipulations have // been completed (note that these may still take place before layout has finished) for (RCTComponentData *componentData in _componentDataByName.allValues) { @@ -856,39 +891,44 @@ RCT_EXPORT_METHOD(findSubviewIn:(nonnull NSNumber *)reactTag atPoint:(CGPoint)po _nextLayoutAnimation = nil; } - RCTProfileEndEvent(0, @"uimanager", @{ - @"view_count": @(_viewRegistry.count), - }); [self flushUIBlocks]; } - (void)flushUIBlocks { + RCTAssertThread(_shadowQueue, @"flushUIBlocks can only be called from the shadow queue"); + // First copy the previous blocks into a temporary variable, then reset the // pending blocks to a new array. This guards against mutation while // processing the pending blocks in another thread. - [_pendingUIBlocksLock lock]; - NSArray *previousPendingUIBlocks = _pendingUIBlocks; + NSArray *previousPendingUIBlocks = _pendingUIBlocks; _pendingUIBlocks = [NSMutableArray new]; - [_pendingUIBlocksLock unlock]; - // Execute the previously queued UI blocks - RCTProfileBeginFlowEvent(); - dispatch_async(dispatch_get_main_queue(), ^{ - RCTProfileEndFlowEvent(); - RCTProfileBeginEvent(0, @"UIManager flushUIBlocks", nil); - @try { - for (dispatch_block_t block in previousPendingUIBlocks) { - block(); + if (previousPendingUIBlocks.count) { + // Execute the previously queued UI blocks + RCTProfileBeginFlowEvent(); + dispatch_async(dispatch_get_main_queue(), ^{ + RCTProfileEndFlowEvent(); + RCTProfileBeginEvent(0, @"UIManager flushUIBlocks", nil); + @try { + for (dispatch_block_t block in previousPendingUIBlocks) { + block(); + } + /** + * TODO(tadeu): Remove it once and for all + */ + for (id node in _bridgeTransactionListeners) { + [node reactBridgeDidFinishTransaction]; + } } - } - @catch (NSException *exception) { - RCTLogError(@"Exception thrown while executing UI block: %@", exception); - } - RCTProfileEndEvent(0, @"objc_call", @{ - @"count": @(previousPendingUIBlocks.count), + @catch (NSException *exception) { + RCTLogError(@"Exception thrown while executing UI block: %@", exception); + } + RCTProfileEndEvent(0, @"objc_call", @{ + @"count": @(previousPendingUIBlocks.count), + }); }); - }); + } } RCT_EXPORT_METHOD(measure:(nonnull NSNumber *)reactTag @@ -1003,8 +1043,9 @@ RCT_EXPORT_METHOD(measureViewsInRect:(CGRect)rect RCTLogError(@"Attempting to measure view that does not exist (tag #%@)", reactTag); return; } - NSArray *childShadowViews = [shadowView reactSubviews]; - NSMutableArray *results = [[NSMutableArray alloc] initWithCapacity:childShadowViews.count]; + NSArray *childShadowViews = [shadowView reactSubviews]; + NSMutableArray *results = + [[NSMutableArray alloc] initWithCapacity:childShadowViews.count]; [childShadowViews enumerateObjectsUsingBlock: ^(RCTShadowView *childShadowView, NSUInteger idx, __unused BOOL *stop) { diff --git a/React/Base/RCTFPSGraph.h b/React/Profiler/RCTFPSGraph.h similarity index 55% rename from React/Base/RCTFPSGraph.h rename to React/Profiler/RCTFPSGraph.h index 0c0e2664a..ecec7aa5b 100644 --- a/React/Base/RCTFPSGraph.h +++ b/React/Profiler/RCTFPSGraph.h @@ -9,15 +9,21 @@ #import -typedef NS_ENUM(NSUInteger, RCTFPSGraphPosition) { - RCTFPSGraphPositionLeft = 1, - RCTFPSGraphPositionRight = 2 -}; +#import "RCTDefines.h" + +#if RCT_DEV @interface RCTFPSGraph : UIView -- (instancetype)initWithFrame:(CGRect)frame graphPosition:(RCTFPSGraphPosition)position name:(NSString *)name color:(UIColor *)color NS_DESIGNATED_INITIALIZER; +@property (nonatomic, assign, readonly) NSUInteger FPS; +@property (nonatomic, assign, readonly) NSUInteger maxFPS; +@property (nonatomic, assign, readonly) NSUInteger minFPS; + +- (instancetype)initWithFrame:(CGRect)frame + color:(UIColor *)color NS_DESIGNATED_INITIALIZER; - (void)onTick:(NSTimeInterval)timestamp; @end + +#endif diff --git a/React/Profiler/RCTFPSGraph.m b/React/Profiler/RCTFPSGraph.m new file mode 100644 index 000000000..44a381bfd --- /dev/null +++ b/React/Profiler/RCTFPSGraph.m @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTFPSGraph.h" + +#import "RCTAssert.h" + +#if RCT_DEV + +@interface RCTFPSGraph() + +@property (nonatomic, strong, readonly) CAShapeLayer *graph; +@property (nonatomic, strong, readonly) UILabel *label; + +@end + +@implementation RCTFPSGraph +{ + CAShapeLayer *_graph; + UILabel *_label; + + CGFloat *_frames; + UIColor *_color; + + NSTimeInterval _prevTime; + NSUInteger _frameCount; + NSUInteger _FPS; + NSUInteger _maxFPS; + NSUInteger _minFPS; + NSUInteger _length; + NSUInteger _height; +} + +- (instancetype)initWithFrame:(CGRect)frame color:(UIColor *)color +{ + if ((self = [super initWithFrame:frame])) { + _frameCount = -1; + _prevTime = -1; + _maxFPS = 0; + _minFPS = 60; + _length = (NSUInteger)floor(frame.size.width); + _height = (NSUInteger)floor(frame.size.height); + _frames = calloc(sizeof(CGFloat), _length); + _color = color; + + [self.layer addSublayer:self.graph]; + [self addSubview:self.label]; + } + return self; +} + +- (void)dealloc +{ + free(_frames); +} + +RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) + +- (CAShapeLayer *)graph +{ + if (!_graph) { + _graph = [CAShapeLayer new]; + _graph.frame = self.bounds; + _graph.backgroundColor = [_color colorWithAlphaComponent:0.2].CGColor; + _graph.fillColor = _color.CGColor; + } + + return _graph; +} + +- (UILabel *)label +{ + if (!_label) { + _label = [[UILabel alloc] initWithFrame:self.bounds]; + _label.font = [UIFont boldSystemFontOfSize:13]; + _label.textAlignment = NSTextAlignmentCenter; + } + + return _label; +} + +- (void)onTick:(NSTimeInterval)timestamp +{ + _frameCount++; + if (_prevTime == -1) { + _prevTime = timestamp; + } else if (timestamp - _prevTime >= 1) { + _FPS = round(_frameCount / (timestamp - _prevTime)); + _minFPS = MIN(_minFPS, _FPS); + _maxFPS = MAX(_maxFPS, _FPS); + + _label.text = [NSString stringWithFormat:@"%lu", (unsigned long)_FPS]; + + CGFloat scale = 60.0 / _height; + for (NSUInteger i = 0; i < _length - 1; i++) { + _frames[i] = _frames[i + 1]; + } + _frames[_length - 1] = _FPS / scale; + + CGMutablePathRef path = CGPathCreateMutable(); + CGPathMoveToPoint(path, NULL, 0, _height); + for (NSUInteger i = 0; i < _length; i++) { + CGPathAddLineToPoint(path, NULL, i, _height - _frames[i]); + } + CGPathAddLineToPoint(path, NULL, _length - 1, _height); + + _graph.path = path; + CGPathRelease(path); + + _prevTime = timestamp; + _frameCount = 0; + } +} + +@end + +#endif diff --git a/React/Profiler/RCTMacros.h b/React/Profiler/RCTMacros.h new file mode 100644 index 000000000..04635eb50 --- /dev/null +++ b/React/Profiler/RCTMacros.h @@ -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. + */ + +#define _CONCAT(A, B) A##B +#define CONCAT(A, B) _CONCAT(A, B) + +#define SYMBOL_NAME(S) CONCAT(__USER_LABEL_PREFIX__, S) diff --git a/React/Profiler/RCTPerfMonitor.m b/React/Profiler/RCTPerfMonitor.m new file mode 100644 index 000000000..e1ce3ad4b --- /dev/null +++ b/React/Profiler/RCTPerfMonitor.m @@ -0,0 +1,555 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTDefines.h" + +#if RCT_DEV + +#import + +#import + +#import "RCTBridge.h" +#import "RCTDevMenu.h" +#import "RCTFPSGraph.h" +#import "RCTInvalidating.h" +#import "RCTJavaScriptExecutor.h" +#import "RCTPerformanceLogger.h" +#import "RCTRootView.h" +#import "RCTSparseArray.h" +#import "RCTUIManager.h" + +static NSString *const RCTPerfMonitorKey = @"RCTPerfMonitorKey"; +static NSString *const RCTPerfMonitorCellIdentifier = @"RCTPerfMonitorCellIdentifier"; + +static CGFloat const RCTPerfMonitorBarHeight = 50; +static CGFloat const RCTPerfMonitorExpandHeight = 250; + +typedef BOOL (*RCTJSCSetOptionType)(const char *); + +static BOOL RCTJSCSetOption(const char *option) +{ + static RCTJSCSetOptionType setOption; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + /** + * JSC private C++ static method to toggle options at runtime + * + * JSC::Options::setOptions - JavaScriptCore/runtime/Options.h + */ + setOption = dlsym(RTLD_DEFAULT, "_ZN3JSC7Options9setOptionEPKc"); + + if (RCT_DEBUG && setOption == NULL) { + RCTLogWarn(@"The symbol used to enable JSC runtime options is not available in this iOS version"); + } + }); + + if (setOption) { + return setOption(option); + } else { + return NO; + } +} + +static vm_size_t RCTGetResidentMemorySize(void) +{ + struct task_basic_info info; + mach_msg_type_number_t size = sizeof(info); + kern_return_t kerr = task_info(mach_task_self(), + TASK_BASIC_INFO, + (task_info_t)&info, + &size); + if (kerr != KERN_SUCCESS) { + return 0; + } + + return info.resident_size; +} + +@class RCTDevMenuItem; + +@interface RCTPerfMonitor : NSObject + +@property (nonatomic, strong, readonly) RCTDevMenuItem *devMenuItem; +@property (nonatomic, strong, readonly) UIPanGestureRecognizer *gestureRecognizer; +@property (nonatomic, strong, readonly) UIView *container; +@property (nonatomic, strong, readonly) UILabel *memory; +@property (nonatomic, strong, readonly) UILabel *heap; +@property (nonatomic, strong, readonly) UILabel *views; +@property (nonatomic, strong, readonly) UITableView *metrics; +@property (nonatomic, strong, readonly) RCTFPSGraph *jsGraph; +@property (nonatomic, strong, readonly) RCTFPSGraph *uiGraph; +@property (nonatomic, strong, readonly) UILabel *jsGraphLabel; +@property (nonatomic, strong, readonly) UILabel *uiGraphLabel; + +@end + +@implementation RCTPerfMonitor { + RCTDevMenuItem *_devMenuItem; + UIPanGestureRecognizer *_gestureRecognizer; + UIView *_container; + UILabel *_memory; + UILabel *_heap; + UILabel *_views; + UILabel *_uiGraphLabel; + UILabel *_jsGraphLabel; + UITableView *_metrics; + + RCTFPSGraph *_uiGraph; + RCTFPSGraph *_jsGraph; + + CADisplayLink *_uiDisplayLink; + CADisplayLink *_jsDisplayLink; + + NSUInteger _heapSize; + + dispatch_queue_t _queue; + dispatch_io_t _io; + int _stderr; + int _pipe[2]; + NSString *_remaining; + + CGRect _storedMonitorFrame; + + NSArray *_perfLoggerMarks; +} + +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + +- (void)invalidate +{ + [self hide]; +} + +- (RCTDevMenuItem *)devMenuItem +{ + if (!_devMenuItem) { + __weak __typeof__(self) weakSelf = self; + _devMenuItem = + [RCTDevMenuItem toggleItemWithKey:RCTPerfMonitorKey + title:@"Show Perf Monitor" + selectedTitle:@"Hide Perf Monitor" + handler: + ^(BOOL selected) { + if (selected) { + [weakSelf show]; + } else { + [weakSelf hide]; + } + }]; + } + + return _devMenuItem; +} + +- (UIPanGestureRecognizer *)gestureRecognizer +{ + if (!_gestureRecognizer) { + _gestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self + action:@selector(gesture:)]; + } + + return _gestureRecognizer; +} + +- (UIView *)container +{ + if (!_container) { + _container = [[UIView alloc] initWithFrame:CGRectMake(10, 25, 180, RCTPerfMonitorBarHeight)]; + _container.backgroundColor = UIColor.whiteColor; + _container.layer.borderWidth = 2; + _container.layer.borderColor = [UIColor lightGrayColor].CGColor; + [_container addGestureRecognizer:self.gestureRecognizer]; + [_container addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(tap)]]; + } + + return _container; +} + +- (UILabel *)memory +{ + if (!_memory) { + _memory = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 44, RCTPerfMonitorBarHeight)]; + _memory.font = [UIFont systemFontOfSize:12]; + _memory.numberOfLines = 3; + _memory.textAlignment = NSTextAlignmentCenter; + } + + return _memory; +} + +- (UILabel *)heap +{ + if (!_heap) { + _heap = [[UILabel alloc] initWithFrame:CGRectMake(44, 0, 44, RCTPerfMonitorBarHeight)]; + _heap.font = [UIFont systemFontOfSize:12]; + _heap.numberOfLines = 3; + _heap.textAlignment = NSTextAlignmentCenter; + } + + return _heap; +} + +- (UILabel *)views +{ + if (!_views) { + _views = [[UILabel alloc] initWithFrame:CGRectMake(88, 0, 44, RCTPerfMonitorBarHeight)]; + _views.font = [UIFont systemFontOfSize:12]; + _views.numberOfLines = 3; + _views.textAlignment = NSTextAlignmentCenter; + } + + return _views; +} + +- (RCTFPSGraph *)uiGraph +{ + if (!_uiGraph) { + _uiGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(134, 14, 40, 30) + color:[UIColor lightGrayColor]]; + } + return _uiGraph; +} + +- (RCTFPSGraph *)jsGraph +{ + if (!_jsGraph) { + _jsGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(178, 14, 40, 30) + color:[UIColor lightGrayColor]]; + } + return _jsGraph; +} + +- (UILabel *)uiGraphLabel +{ + if (!_uiGraphLabel) { + _uiGraphLabel = [[UILabel alloc] initWithFrame:CGRectMake(134, 3, 40, 10)]; + _uiGraphLabel.font = [UIFont systemFontOfSize:11]; + _uiGraphLabel.textAlignment = NSTextAlignmentCenter; + _uiGraphLabel.text = @"UI"; + } + + return _uiGraphLabel; +} + +- (UILabel *)jsGraphLabel +{ + if (!_jsGraphLabel) { + _jsGraphLabel = [[UILabel alloc] initWithFrame:CGRectMake(178, 3, 38, 10)]; + _jsGraphLabel.font = [UIFont systemFontOfSize:11]; + _jsGraphLabel.textAlignment = NSTextAlignmentCenter; + _jsGraphLabel.text = @"JS"; + } + + return _jsGraphLabel; +} + +- (UITableView *)metrics +{ + if (!_metrics) { + _metrics = [[UITableView alloc] initWithFrame:CGRectMake( + 0, + RCTPerfMonitorBarHeight, + self.container.frame.size.width, + self.container.frame.size.height - RCTPerfMonitorBarHeight + )]; + _metrics.dataSource = self; + _metrics.delegate = self; + _metrics.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + [_metrics registerClass:[UITableViewCell class] forCellReuseIdentifier:RCTPerfMonitorCellIdentifier]; + } + + return _metrics; +} + +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + +- (void)setBridge:(RCTBridge *)bridge +{ + _bridge = bridge; + + [_bridge.devMenu addItem:self.devMenuItem]; +} + +- (void)show +{ + if (_container) { + return; + } + + [self.container addSubview:self.memory]; + [self.container addSubview:self.heap]; + [self.container addSubview:self.views]; + [self.container addSubview:self.uiGraph]; + [self.container addSubview:self.uiGraphLabel]; + + [self redirectLogs]; + + RCTJSCSetOption("logGC=1"); + + [self updateStats]; + + UIWindow *window = [UIApplication sharedApplication].delegate.window; + [window addSubview:self.container]; + + + _uiDisplayLink = [CADisplayLink displayLinkWithTarget:self + selector:@selector(threadUpdate:)]; + [_uiDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] + forMode:NSRunLoopCommonModes]; + + id executor = [_bridge valueForKey:@"javaScriptExecutor"]; + if ([executor isKindOfClass:NSClassFromString(@"RCTContextExecutor")]) { + self.container.frame = (CGRect) { + self.container.frame.origin, { + self.container.frame.size.width + 44, + self.container.frame.size.height + } + }; + [self.container addSubview:self.jsGraph]; + [self.container addSubview:self.jsGraphLabel]; + [executor executeBlockOnJavaScriptQueue:^{ + _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self + selector:@selector(threadUpdate:)]; + [_jsDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] + forMode:NSRunLoopCommonModes]; + }]; + } +} + +- (void)hide +{ + if (!_container) { + return; + } + + [self.container removeFromSuperview]; + _container = nil; + _jsGraph = nil; + _uiGraph = nil; + + RCTJSCSetOption("logGC=0"); + + [self stopLogs]; + + [_uiDisplayLink invalidate]; + [_jsDisplayLink invalidate]; + + _uiDisplayLink = nil; + _jsDisplayLink = nil; +} + +- (void)redirectLogs +{ + _stderr = dup(STDERR_FILENO); + + if (pipe(_pipe) != 0) { + return; + } + + dup2(_pipe[1], STDERR_FILENO); + close(_pipe[1]); + + __weak __typeof__(self) weakSelf = self; + _queue = dispatch_queue_create("com.facebook.react.RCTPerfMonitor", DISPATCH_QUEUE_SERIAL); + _io = dispatch_io_create( + DISPATCH_IO_STREAM, + _pipe[0], + _queue, + ^(__unused int error) {}); + + dispatch_io_set_low_water(_io, 20); + + dispatch_io_read( + _io, + 0, + SIZE_MAX, + _queue, + ^(__unused bool done, dispatch_data_t data, __unused int error) { + if (!data) { + return; + } + + dispatch_data_apply( + data, + ^bool( + __unused dispatch_data_t region, + __unused size_t offset, + const void *buffer, + size_t size + ) { + write(_stderr, buffer, size); + + NSString *log = [[NSString alloc] initWithBytes:buffer + length:size + encoding:NSUTF8StringEncoding]; + [weakSelf parse:log]; + return true; + }); + }); +} + +- (void)stopLogs +{ + dup2(_stderr, STDERR_FILENO); + dispatch_io_close(_io, 0); +} + +- (void)parse:(NSString *)log +{ + static NSRegularExpression *GCRegex; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *pattern = @"\\[GC: (Eden|Full)Collection, (?:Skipped copying|Did copy), ([\\d\\.]+) (\\wb), ([\\d.]+) (\\ws)\\]"; + GCRegex = [NSRegularExpression regularExpressionWithPattern:pattern + options:0 + error:nil]; + }); + + if (_remaining) { + log = [_remaining stringByAppendingString:log]; + _remaining = nil; + } + + NSArray *lines = [log componentsSeparatedByString:@"\n"]; + if (lines.count == 1) { // no newlines + _remaining = log; + return; + } + + for (NSString *line in lines) { + NSTextCheckingResult *match = [GCRegex firstMatchInString:line options:0 range:NSMakeRange(0, line.length)]; + if (match) { + NSString *heapSizeStr = [line substringWithRange:[match rangeAtIndex:2]]; + _heapSize = [heapSizeStr integerValue]; + } + } +} + +- (void)updateStats +{ + RCTSparseArray *views = [_bridge.uiManager valueForKey:@"viewRegistry"]; + NSUInteger viewCount = views.count; + NSUInteger visibleViewCount = 0; + for (UIView *view in views.allObjects) { + if (view.window || view.superview.window) { + visibleViewCount++; + } + } + + double mem = (double)RCTGetResidentMemorySize() / 1024 / 1024; + self.memory.text =[NSString stringWithFormat:@"RAM\n%.2lf\nMB", mem]; + self.heap.text = [NSString stringWithFormat:@"JSC\n%.2lf\nMB", (double)_heapSize / 1024]; + self.views.text = [NSString stringWithFormat:@"Views\n%lu\n%lu", (unsigned long)visibleViewCount, (unsigned long)viewCount]; + + __weak __typeof__(self) weakSelf = self; + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), + dispatch_get_main_queue(), + ^{ + __strong __typeof__(weakSelf) strongSelf = weakSelf; + if (strongSelf && strongSelf->_container.superview) { + [strongSelf updateStats]; + } + }); +} + +- (void)gesture:(UIPanGestureRecognizer *)gestureRecognizer +{ + CGPoint translation = [gestureRecognizer translationInView:self.container.superview]; + self.container.center = CGPointMake( + self.container.center.x + translation.x, + self.container.center.y + translation.y + ); + [gestureRecognizer setTranslation:CGPointMake(0, 0) + inView:self.container.superview]; +} + +- (void)tap +{ + if (CGRectIsEmpty(_storedMonitorFrame)) { + _storedMonitorFrame = CGRectMake(0, 20, self.container.window.frame.size.width, RCTPerfMonitorExpandHeight); + [self.container addSubview:self.metrics]; + [self loadPerformanceLoggerData]; + } + + [UIView animateWithDuration:.25 animations:^{ + CGRect tmp = self.container.frame; + self.container.frame = _storedMonitorFrame; + _storedMonitorFrame = tmp; + }]; +} + +- (void)threadUpdate:(CADisplayLink *)displayLink +{ + RCTFPSGraph *graph = displayLink == _jsDisplayLink ? _jsGraph : _uiGraph; + [graph onTick:displayLink.timestamp]; +} + +- (void)loadPerformanceLoggerData +{ + NSMutableArray *data = [NSMutableArray new]; + NSArray *times = RCTPerformanceLoggerOutput(); + NSUInteger i = 0; + for (NSString *label in RCTPerformanceLoggerLabels()) { + [data addObject:[NSString stringWithFormat:@"%@: %lldus", label, + [times[i+1] longLongValue] - [times[i] longLongValue]]]; + i += 2; + } + _perfLoggerMarks = [data copy]; +} + +#pragma mark - UITableViewDataSource + +- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView +{ + return 1; +} + +- (NSInteger)tableView:(__unused UITableView *)tableView + numberOfRowsInSection:(__unused NSInteger)section +{ + return _perfLoggerMarks.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:RCTPerfMonitorCellIdentifier + forIndexPath:indexPath]; + + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault + reuseIdentifier:RCTPerfMonitorCellIdentifier]; + } + + cell.textLabel.text = _perfLoggerMarks[indexPath.row]; + cell.textLabel.font = [UIFont systemFontOfSize:12]; + + return cell; +} + +#pragma mark - UITableViewDelegate + +- (CGFloat)tableView:(__unused UITableView *)tableView +heightForRowAtIndexPath:(__unused NSIndexPath *)indexPath +{ + return 20; +} + +@end + +#endif diff --git a/React/Base/RCTProfile.h b/React/Profiler/RCTProfile.h similarity index 74% rename from React/Base/RCTProfile.h rename to React/Profiler/RCTProfile.h index c184cae18..addddf3df 100644 --- a/React/Base/RCTProfile.h +++ b/React/Profiler/RCTProfile.h @@ -36,6 +36,8 @@ _Pragma("clang diagnostic pop") #define RCTProfileEndFlowEvent() \ _RCTProfileEndFlowEvent(__rct_profile_flow_id) +RCT_EXTERN dispatch_queue_t RCTProfileGetQueue(void); + RCT_EXTERN NSNumber *_RCTProfileBeginFlowEvent(void); RCT_EXTERN void _RCTProfileEndFlowEvent(NSNumber *); @@ -54,30 +56,51 @@ RCT_EXTERN void RCTProfileInit(RCTBridge *); * returned is compliant with google's trace event format - the format used * as input to trace-viewer */ -RCT_EXTERN NSString *RCTProfileEnd(RCTBridge *); +RCT_EXTERN void RCTProfileEnd(RCTBridge *, void (^)(NSString *)); /** * Collects the initial event information for the event and returns a reference ID */ -RCT_EXTERN void RCTProfileBeginEvent(uint64_t tag, - NSString *name, - NSDictionary *args); +RCT_EXTERN void _RCTProfileBeginEvent(NSThread *calleeThread, + NSTimeInterval time, + uint64_t tag, + NSString *name, + NSDictionary *args); +#define RCTProfileBeginEvent(...) { \ + NSThread *calleeThread = [NSThread currentThread]; \ + NSTimeInterval time = CACurrentMediaTime(); \ + dispatch_async(RCTProfileGetQueue(), ^{ \ + _RCTProfileBeginEvent(calleeThread, time, __VA_ARGS__); \ + }); \ +} /** * The ID returned by BeginEvent should then be passed into EndEvent, with the * rest of the event information. Just at this point the event will actually be * registered */ -RCT_EXTERN void RCTProfileEndEvent(uint64_t tag, - NSString *category, - NSDictionary *args); +RCT_EXTERN void _RCTProfileEndEvent(NSThread *calleeThread, + NSString *threadName, + NSTimeInterval time, + uint64_t tag, + NSString *category, + NSDictionary *args); + +#define RCTProfileEndEvent(...) { \ + NSThread *calleeThread = [NSThread currentThread]; \ + NSString *threadName = RCTCurrentThreadName(); \ + NSTimeInterval time = CACurrentMediaTime(); \ + dispatch_async(RCTProfileGetQueue(), ^{ \ + _RCTProfileEndEvent(calleeThread, threadName, time, __VA_ARGS__); \ + }); \ +} /** * Collects the initial event information for the event and returns a reference ID */ -RCT_EXTERN int RCTProfileBeginAsyncEvent(uint64_t tag, - NSString *name, - NSDictionary *args); +RCT_EXTERN NSUInteger RCTProfileBeginAsyncEvent(uint64_t tag, + NSString *name, + NSDictionary *args); /** * The ID returned by BeginEvent should then be passed into EndEvent, with the @@ -86,9 +109,10 @@ RCT_EXTERN int RCTProfileBeginAsyncEvent(uint64_t tag, */ RCT_EXTERN void RCTProfileEndAsyncEvent(uint64_t tag, NSString *category, - int cookie, + NSUInteger cookie, NSString *name, NSDictionary *args); + /** * An event that doesn't have a duration (i.e. Notification, VSync, etc) */ @@ -123,7 +147,7 @@ RCT_EXTERN void RCTProfileUnhookModules(RCTBridge *); * Send systrace or cpu profiling information to the packager * to present to the user */ -RCT_EXTERN void RCTProfileSendResult(RCTBridge *bridge, NSString *route, NSData *profielData); +RCT_EXTERN void RCTProfileSendResult(RCTBridge *bridge, NSString *route, NSData *profileData); /** * Systrace gluecode diff --git a/React/Base/RCTProfile.m b/React/Profiler/RCTProfile.m similarity index 65% rename from React/Base/RCTProfile.m rename to React/Profiler/RCTProfile.m index dba23b5b4..d0b11d996 100644 --- a/React/Base/RCTProfile.m +++ b/React/Profiler/RCTProfile.m @@ -9,6 +9,9 @@ #import "RCTProfile.h" +#import + +#import #import #import #import @@ -35,7 +38,10 @@ NSString *const RCTProfilePrefix = @"rct_profile_"; #pragma mark - Variables -static BOOL RCTProfileProfiling; +// This is actually a BOOL - but has to be compatible with OSAtomic +static volatile uint32_t RCTProfileProfiling; + +static BOOL RCTProfileHookedModules; static NSDictionary *RCTProfileInfo; static NSMutableDictionary *RCTProfileOngoingEvents; static NSTimeInterval RCTProfileStartTime; @@ -46,7 +52,6 @@ static NSUInteger RCTProfileEventID = 0; #define RCTProfileAddEvent(type, props...) \ [RCTProfileInfo[type] addObject:@{ \ @"pid": @([[NSProcessInfo processInfo] processIdentifier]), \ - @"tid": RCTCurrentThreadName(), \ props \ }]; @@ -55,11 +60,6 @@ if (!RCTProfileIsProfiling()) { \ return __VA_ARGS__; \ } -#define RCTProfileLock(...) \ -[_RCTProfileLock() lock]; \ -__VA_ARGS__ \ -[_RCTProfileLock() unlock] - #pragma mark - systrace glue code static RCTProfileCallbacks *callbacks; @@ -93,18 +93,6 @@ void RCTProfileRegisterCallbacks(RCTProfileCallbacks *cb) #pragma mark - Private Helpers -static NSLock *_RCTProfileLock() -{ - static dispatch_once_t token; - static NSLock *lock; - dispatch_once(&token, ^{ - lock = [NSLock new]; - lock.name = @"RCTProfileLock"; - }); - - return lock; -} - static NSNumber *RCTProfileTimestamp(NSTimeInterval timestamp) { return @((timestamp - RCTProfileStartTime) * 1e6); @@ -153,21 +141,11 @@ static NSDictionary *RCTProfileMergeArgs(NSDictionary *args0, NSDictionary *args #pragma mark - Module hooks -static const char *RCTProfileProxyClassName(Class); static const char *RCTProfileProxyClassName(Class class) { return [RCTProfilePrefix stringByAppendingString:NSStringFromClass(class)].UTF8String; } -static SEL RCTProfileProxySelector(SEL); -static SEL RCTProfileProxySelector(SEL selector) -{ - NSString *selectorName = NSStringFromSelector(selector); - return NSSelectorFromString([RCTProfilePrefix stringByAppendingString:selectorName]); -} - - -static dispatch_group_t RCTProfileGetUnhookGroup(void); static dispatch_group_t RCTProfileGetUnhookGroup(void) { static dispatch_group_t unhookGroup; @@ -179,45 +157,59 @@ static dispatch_group_t RCTProfileGetUnhookGroup(void) return unhookGroup; } -static void RCTProfileForwardInvocation(NSObject *, SEL, NSInvocation *); -static void RCTProfileForwardInvocation(NSObject *self, __unused SEL cmd, NSInvocation *invocation) +RCT_EXTERN IMP RCTProfileGetImplementation(id obj, SEL cmd); +IMP RCTProfileGetImplementation(id obj, SEL cmd) { - /** - * This is still not thread safe, but should reduce reasonably the number of crashes - */ - dispatch_group_wait(RCTProfileGetUnhookGroup(), DISPATCH_TIME_FOREVER); - - NSString *name = [NSString stringWithFormat:@"-[%@ %@]", NSStringFromClass([self class]), NSStringFromSelector(invocation.selector)]; - SEL newSel = RCTProfileProxySelector(invocation.selector); - - if ([object_getClass(self) instancesRespondToSelector:newSel]) { - invocation.selector = newSel; - RCTProfileBeginEvent(0, name, nil); - [invocation invoke]; - RCTProfileEndEvent(0, @"objc_call,modules,auto", nil); - } else if ([self respondsToSelector:invocation.selector]) { - [invocation invoke]; - } else { - // Use original selector to don't change error message - [self doesNotRecognizeSelector:invocation.selector]; - } + return class_getMethodImplementation([obj class], cmd); } -static IMP RCTProfileMsgForward(NSObject *, SEL); -static IMP RCTProfileMsgForward(NSObject *self, SEL selector) -{ - IMP imp = (IMP)_objc_msgForward; -#if !defined(__arm64__) - NSMethodSignature *signature = [self methodSignatureForSelector:selector]; - if (signature.methodReturnType[0] == _C_STRUCT_B && signature.methodReturnLength > 8) { - imp = (IMP)_objc_msgForward_stret; - } +/** + * For the profiling we have to execute some code before and after every + * function being profiled, the only way of doing that with pure Objective-C is + * by using `-forwardInvocation:`, which is slow and could skew the profile + * results. + * + * The alternative in assembly is much simpler, we just need to store all the + * state at the beginning of the function, start the profiler, restore all the + * state, call the actual function we want to profile and stop the profiler. + * + * The implementation can be found in RCTProfileTrampoline-.s where arch + * is one of: x86, x86_64, arm, arm64. + */ +#if defined(__x86__) || \ + defined(__x86_64__) || \ + defined(__arm__) || \ + defined(__arm64__) + + RCT_EXTERN void RCTProfileTrampoline(void); +#else + static void *RCTProfileTrampoline = NULL; #endif - return imp; + +RCT_EXTERN void RCTProfileTrampolineStart(id, SEL); +void RCTProfileTrampolineStart(id self, SEL cmd) +{ + NSString *name = [NSString stringWithFormat:@"-[%s %s]", class_getName([self class]), sel_getName(cmd)]; + RCTProfileBeginEvent(0, name, nil); +} + +RCT_EXTERN void RCTProfileTrampolineEnd(void); +void RCTProfileTrampolineEnd(void) +{ + RCTProfileEndEvent(0, @"objc_call,modules,auto", nil); } void RCTProfileHookModules(RCTBridge *bridge) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wtautological-pointer-compare" + if (RCTProfileTrampoline == NULL || RCTProfileHookedModules) { + return; + } +#pragma clang diagnostic pop + + RCTProfileHookedModules = YES; + for (RCTModuleData *moduleData in [bridge valueForKey:@"moduleDataByID"]) { [moduleData dispatchBlock:^{ Class moduleClass = moduleData.moduleClass; @@ -235,10 +227,9 @@ void RCTProfileHookModules(RCTBridge *bridge) if ([NSStringFromSelector(selector) hasPrefix:@"rct"] || [NSObject instancesRespondToSelector:selector]) { continue; } - IMP originalIMP = method_getImplementation(method); - const char *returnType = method_getTypeEncoding(method); - class_addMethod(proxyClass, selector, RCTProfileMsgForward(moduleData.instance, selector), returnType); - class_addMethod(proxyClass, RCTProfileProxySelector(selector), originalIMP, returnType); + const char *types = method_getTypeEncoding(method); + + class_addMethod(proxyClass, selector, (IMP)RCTProfileTrampoline, types); } free(methods); @@ -249,11 +240,6 @@ void RCTProfileHookModules(RCTBridge *bridge) class_replaceMethod(cls, @selector(class), imp_implementationWithBlock(^{ return moduleClass; }), method_getTypeEncoding(oldImp)); } - IMP originalFwd = class_replaceMethod(moduleClass, @selector(forwardInvocation:), (IMP)RCTProfileForwardInvocation, "v@:@"); - if (originalFwd != NULL) { - class_addMethod(proxyClass, RCTProfileProxySelector(@selector(forwardInvocation:)), originalFwd, "v@:@"); - } - objc_registerClassPair(proxyClass); object_setClass(moduleData.instance, proxyClass); }]; @@ -262,6 +248,12 @@ void RCTProfileHookModules(RCTBridge *bridge) void RCTProfileUnhookModules(RCTBridge *bridge) { + if (!RCTProfileHookedModules) { + return; + } + + RCTProfileHookedModules = NO; + dispatch_group_enter(RCTProfileGetUnhookGroup()); for (RCTModuleData *moduleData in [bridge valueForKey:@"moduleDataByID"]) { @@ -278,104 +270,136 @@ void RCTProfileUnhookModules(RCTBridge *bridge) #pragma mark - Public Functions +dispatch_queue_t RCTProfileGetQueue(void) +{ + static dispatch_queue_t queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("com.facebook.react.Profiler", DISPATCH_QUEUE_SERIAL); + }); + return queue; +} + BOOL RCTProfileIsProfiling(void) { - return RCTProfileProfiling; + return (BOOL)OSAtomicAnd32(1, &RCTProfileProfiling); } void RCTProfileInit(RCTBridge *bridge) { - RCTProfileHookModules(bridge); - RCTProfileProfiling = YES; + // TODO: enable assert JS thread from any file (and assert here) + + OSAtomicOr32(1, &RCTProfileProfiling); if (callbacks != NULL) { size_t buffer_size = 1 << 22; systrace_buffer = calloc(1, buffer_size); callbacks->start(~((uint64_t)0), systrace_buffer, buffer_size); } else { - RCTProfileLock( - RCTProfileStartTime = CACurrentMediaTime(); + NSTimeInterval time = CACurrentMediaTime(); + dispatch_async(RCTProfileGetQueue(), ^{ + RCTProfileStartTime = time; RCTProfileOngoingEvents = [NSMutableDictionary new]; RCTProfileInfo = @{ RCTProfileTraceEvents: [NSMutableArray new], RCTProfileSamples: [NSMutableArray new], }; - ); + }); } + RCTProfileHookModules(bridge); + [[NSNotificationCenter defaultCenter] postNotificationName:RCTProfileDidStartProfiling object:nil]; } -NSString *RCTProfileEnd(RCTBridge *bridge) +void RCTProfileEnd(RCTBridge *bridge, void (^callback)(NSString *)) { + // assert JavaScript thread here again + + if (!RCTProfileIsProfiling()) { + return; + } + + OSAtomicAnd32(0, &RCTProfileProfiling); + [[NSNotificationCenter defaultCenter] postNotificationName:RCTProfileDidEndProfiling object:nil]; - RCTProfileProfiling = NO; - - RCTProfileLock( - RCTProfileUnhookModules(bridge); - ); + RCTProfileUnhookModules(bridge); if (callbacks != NULL) { callbacks->stop(); - return @(systrace_buffer); + callback(@(systrace_buffer)); } else { - RCTProfileLock( + dispatch_async(RCTProfileGetQueue(), ^{ NSString *log = RCTJSONStringify(RCTProfileInfo, NULL); RCTProfileEventID = 0; RCTProfileInfo = nil; RCTProfileOngoingEvents = nil; - ); - return log; + callback(log); + }); } } -static NSMutableArray *RCTProfileGetThreadEvents(void) +static NSMutableArray *RCTProfileGetThreadEvents(NSThread *thread) { static NSString *const RCTProfileThreadEventsKey = @"RCTProfileThreadEventsKey"; - NSMutableArray *threadEvents = [NSThread currentThread].threadDictionary[RCTProfileThreadEventsKey]; + NSMutableArray *threadEvents = + thread.threadDictionary[RCTProfileThreadEventsKey]; if (!threadEvents) { - threadEvents = [[NSMutableArray alloc] init]; - [NSThread currentThread].threadDictionary[RCTProfileThreadEventsKey] = threadEvents; + threadEvents = [NSMutableArray new]; + thread.threadDictionary[RCTProfileThreadEventsKey] = threadEvents; } return threadEvents; } -void RCTProfileBeginEvent(uint64_t tag, NSString *name, NSDictionary *args) -{ +void _RCTProfileBeginEvent( + NSThread *calleeThread, + NSTimeInterval time, + uint64_t tag, + NSString *name, + NSDictionary *args +) { + CHECK(); + RCTAssertThread(RCTProfileGetQueue(), @"Must be called RCTProfile queue");; + if (callbacks != NULL) { callbacks->begin_section(tag, name.UTF8String, args.count, RCTProfileSystraceArgsFromNSDictionary(args)); return; } - NSMutableArray *events = RCTProfileGetThreadEvents(); + NSMutableArray *events = RCTProfileGetThreadEvents(calleeThread); [events addObject:@[ - RCTProfileTimestamp(CACurrentMediaTime()), + RCTProfileTimestamp(time), @(tag), name, RCTNullIfNil(args), ]]; } -void RCTProfileEndEvent( +void _RCTProfileEndEvent( + NSThread *calleeThread, + NSString *threadName, + NSTimeInterval time, uint64_t tag, NSString *category, NSDictionary *args ) { CHECK(); + RCTAssertThread(RCTProfileGetQueue(), @"Must be called RCTProfile queue");; + if (callbacks != NULL) { callbacks->end_section(tag, args.count, RCTProfileSystraceArgsFromNSDictionary(args)); return; } - NSMutableArray *events = RCTProfileGetThreadEvents(); + NSMutableArray *events = RCTProfileGetThreadEvents(calleeThread); NSArray *event = events.lastObject; [events removeLastObject]; @@ -385,62 +409,69 @@ void RCTProfileEndEvent( NSNumber *start = event[0]; - RCTProfileLock( - RCTProfileAddEvent(RCTProfileTraceEvents, - @"name": event[2], - @"cat": category, - @"ph": @"X", - @"ts": start, - @"dur": @(RCTProfileTimestamp(CACurrentMediaTime()).doubleValue - start.doubleValue), - @"args": RCTProfileMergeArgs(event[3], args), - ); + RCTProfileAddEvent(RCTProfileTraceEvents, + @"tid": threadName, + @"name": event[2], + @"cat": category, + @"ph": @"X", + @"ts": start, + @"dur": @(RCTProfileTimestamp(time).doubleValue - start.doubleValue), + @"args": RCTProfileMergeArgs(event[3], args), ); } -int RCTProfileBeginAsyncEvent( +NSUInteger RCTProfileBeginAsyncEvent( uint64_t tag, NSString *name, NSDictionary *args ) { CHECK(0); - static int eventID = 0; + static NSUInteger eventID = 0; + + NSTimeInterval time = CACurrentMediaTime(); + NSUInteger currentEventID = ++eventID; if (callbacks != NULL) { - callbacks->begin_async_section(tag, name.UTF8String, eventID, args.count, RCTProfileSystraceArgsFromNSDictionary(args)); + callbacks->begin_async_section(tag, name.UTF8String, (int)(currentEventID % INT_MAX), args.count, RCTProfileSystraceArgsFromNSDictionary(args)); } else { - RCTProfileLock( - RCTProfileOngoingEvents[@(eventID)] = @[ - RCTProfileTimestamp(CACurrentMediaTime()), + dispatch_async(RCTProfileGetQueue(), ^{ + RCTProfileOngoingEvents[@(currentEventID)] = @[ + RCTProfileTimestamp(time), name, RCTNullIfNil(args), ]; - ); + }); } - return eventID++; + return currentEventID; } void RCTProfileEndAsyncEvent( uint64_t tag, NSString *category, - int cookie, + NSUInteger cookie, NSString *name, NSDictionary *args ) { CHECK(); if (callbacks != NULL) { - callbacks->end_async_section(tag, name.UTF8String, cookie, args.count, RCTProfileSystraceArgsFromNSDictionary(args)); + callbacks->end_async_section(tag, name.UTF8String, (int)(cookie % INT_MAX), args.count, RCTProfileSystraceArgsFromNSDictionary(args)); return; } - RCTProfileLock( + NSTimeInterval time = CACurrentMediaTime(); + NSString *threadName = RCTCurrentThreadName(); + + dispatch_async(RCTProfileGetQueue(), ^{ NSArray *event = RCTProfileOngoingEvents[@(cookie)]; + if (event) { - NSNumber *endTimestamp = RCTProfileTimestamp(CACurrentMediaTime()); + NSNumber *endTimestamp = RCTProfileTimestamp(time); RCTProfileAddEvent(RCTProfileTraceEvents, + @"tid": threadName, @"name": event[1], @"cat": category, @"ph": @"X", @@ -450,7 +481,7 @@ void RCTProfileEndAsyncEvent( ); [RCTProfileOngoingEvents removeObjectForKey:@(cookie)]; } - ); + }); } void RCTProfileImmediateEvent( @@ -465,15 +496,19 @@ void RCTProfileImmediateEvent( return; } - RCTProfileLock( + NSTimeInterval time = CACurrentMediaTime(); + NSString *threadName = RCTCurrentThreadName(); + + dispatch_async(RCTProfileGetQueue(), ^{ RCTProfileAddEvent(RCTProfileTraceEvents, + @"tid": threadName, @"name": name, - @"ts": RCTProfileTimestamp(CACurrentMediaTime()), + @"ts": RCTProfileTimestamp(time), @"scope": @(scope), @"ph": @"i", @"args": RCTProfileGetMemoryUsage(), ); - ); + }); } NSNumber *_RCTProfileBeginFlowEvent(void) @@ -487,17 +522,23 @@ NSNumber *_RCTProfileBeginFlowEvent(void) return @0; } - RCTProfileLock( + NSTimeInterval time = CACurrentMediaTime(); + NSNumber *currentID = @(++flowID); + NSString *threadName = RCTCurrentThreadName(); + + dispatch_async(RCTProfileGetQueue(), ^{ RCTProfileAddEvent(RCTProfileTraceEvents, + @"tid": threadName, @"name": @"flow", - @"id": @(++flowID), + @"id": currentID, @"cat": @"flow", @"ph": @"s", - @"ts": RCTProfileTimestamp(CACurrentMediaTime()), + @"ts": RCTProfileTimestamp(time), ); - ); - return @(flowID); + }); + + return currentID; } void _RCTProfileEndFlowEvent(NSNumber *flowID) @@ -508,21 +549,25 @@ void _RCTProfileEndFlowEvent(NSNumber *flowID) return; } - RCTProfileLock( + NSTimeInterval time = CACurrentMediaTime(); + NSString *threadName = RCTCurrentThreadName(); + + dispatch_async(RCTProfileGetQueue(), ^{ RCTProfileAddEvent(RCTProfileTraceEvents, + @"tid": threadName, @"name": @"flow", @"id": flowID, @"cat": @"flow", @"ph": @"f", - @"ts": RCTProfileTimestamp(CACurrentMediaTime()), + @"ts": RCTProfileTimestamp(time), ); - ); + }); } void RCTProfileSendResult(RCTBridge *bridge, NSString *route, NSData *data) { if (![bridge.bundleURL.scheme hasPrefix:@"http"]) { - RCTLogError(@"Cannot update profile information"); + RCTLogError(@"Cannot upload profile information"); return; } diff --git a/React/Profiler/RCTProfileTrampoline-arm.S b/React/Profiler/RCTProfileTrampoline-arm.S new file mode 100644 index 000000000..4835d261a --- /dev/null +++ b/React/Profiler/RCTProfileTrampoline-arm.S @@ -0,0 +1,86 @@ +#include "RCTDefines.h" +#include "RCTMacros.h" + +#if RCT_DEV && defined(__arm__) + + .align 5 + .globl SYMBOL_NAME(RCTProfileTrampoline) +SYMBOL_NAME(RCTProfileTrampoline): + /** + * The explanation here is shorter, refer to the x86_64 implementation to a + * richer explanation + */ + + /** + * Save the parameter registers (r0-r3), r7 (frame pointer) and lr (link + * register (contains the address of the caller of RCTProfileTrampoline) + */ + push {r0-r3, r7, lr} + + /** + * Allocate memory to store values across function calls: 12-bytes are + * allocated to store 3 values: the previous value of the callee saved + * register used to save the pointer to the allocated memory, the caller of + * RCTProfileTrampoline and the address of the actual function we want to + * profile + */ + mov r0, #0xc + bl SYMBOL_NAME(malloc) + /** + * r4 is the callee saved register we'll use to refer to the allocated memory, + * store its initial value, so we can restore it later + */ + str r4, [r0] + mov r4, r0 + + /** + * void RCTProfileGetImplementation(id object, SEL selector) in RCTProfile.m + * + * Load the first 2 argumenters (self and _cmd) used to call + * RCTProfileTrampoline from the stack and put them on the appropriate registers. + */ + ldr r0, [sp] + ldr r1, [sp, #0x4] + bl SYMBOL_NAME(RCTProfileGetImplementation) + // store the actual function address in the allocated memory + str r0, [r4, #0x4] + + /** + * void RCTProfileGetImplementation(id object, SEL selector) in RCTProfile.m + * + * Load the first 2 arguments again to start the profiler + */ + ldr r0, [sp] + ldr r1, [sp, #0x4] + bl SYMBOL_NAME(RCTProfileTrampolineStart) + + /** + * Restore the state to call the actual function we want to profile: pop + * all the registers + */ + pop {r0-r3, r7, lr} + + // store lr (the caller) since it'll be overridden by `blx` (call) + str lr, [r4, #0x8] + ldr r12, [r4, #0x4] // load the function address + blx r12 // call it + push {r0} // save return value + + // void RCTProfileTrampolineEnd(void) in RCTProfile.m - just ends this profile + bl SYMBOL_NAME(RCTProfileTrampolineEnd) + + /** + * Save the value we still need from the allocated memory (caller address), + * restore r4 and free the allocated memory (put its address in r0) + */ + mov r0, r4 + ldr r1, [r4, #0x8] + ldr r4, [r4] + push {r1} // save the caller on the stack + bl SYMBOL_NAME(free) + + pop {lr} // pop the caller + pop {r0} // pop the return value + bx lr // jump to the calleer + +#endif diff --git a/React/Profiler/RCTProfileTrampoline-arm64.S b/React/Profiler/RCTProfileTrampoline-arm64.S new file mode 100644 index 000000000..92ea42a83 --- /dev/null +++ b/React/Profiler/RCTProfileTrampoline-arm64.S @@ -0,0 +1,118 @@ +#include "RCTDefines.h" +#include "RCTMacros.h" + +#if RCT_DEV && defined(__arm64__) + + .align 5 + .globl SYMBOL_NAME(RCTProfileTrampoline) +SYMBOL_NAME(RCTProfileTrampoline): + /** + * The explanation here is shorter, refer to the x86_64 implementation to a + * richer explanation + */ + + // Basic prolog: save the frame pointer and the link register (caller address) + stp fp, lr, [sp, #-16]! + mov fp, sp + + /** + * Store the value of all the parameter registers (x0-x8, q0-q7) so we can + * restore everything to the initial state at the time of the actual function + * call + */ + sub sp, sp, #(10*8 + 8*16) + stp q0, q1, [sp, #(0*16)] + stp q2, q3, [sp, #(2*16)] + stp q4, q5, [sp, #(4*16)] + stp q6, q7, [sp, #(6*16)] + stp x0, x1, [sp, #(8*16+0*8)] + stp x2, x3, [sp, #(8*16+2*8)] + stp x4, x5, [sp, #(8*16+4*8)] + stp x6, x7, [sp, #(8*16+6*8)] + str x8, [sp, #(8*16+8*8)] + + /** + * Allocate 16-bytes for the values that have to be preserved across the call + * to the actual function, since the stack has to be in the exact initial + * state. During its lifetimewe use it to store the initial value of the + * callee saved registers we use to point the memory, the actual address of + * the implementation and the caller address. + */ + mov x0, #0x10 + bl SYMBOL_NAME(malloc) + // store the initial value of r19, the callee saved register we'll use + str x19, [x0] + mov x19, x0 + + /** + * void RCTProfileGetImplementation(id object, SEL selector) + * + * Load the 2 first arguments from the stack, they are the same used to call + * this function + */ + ldp x0, x1, [sp, #(8*16+0*8)] + bl SYMBOL_NAME(RCTProfileGetImplementation) + str x0, [x19, #0x8] // store the actual function address + + /** + * void RCTProfileTrampolineStart(id, SEL) in RCTProfile.m + * + * start the profile, it takes the same first 2 arguments as above. + */ + ldp x0, x1, [sp, #(8*16+0*8)] + bl SYMBOL_NAME(RCTProfileTrampolineStart) + + // Restore all the parameter registers to the initial state. + ldp q0, q1, [sp, #(0*16)] + ldp q2, q3, [sp, #(2*16)] + ldp q4, q5, [sp, #(4*16)] + ldp q6, q7, [sp, #(6*16)] + ldp x0, x1, [sp, #(8*16+0*8)] + ldp x2, x3, [sp, #(8*16+2*8)] + ldp x4, x5, [sp, #(8*16+4*8)] + ldp x6, x7, [sp, #(8*16+6*8)] + ldr x8, [sp, #(8*16+8*8)] + + // Restore the stack pointer, frame pointer and link register + mov sp, fp + ldp fp, lr, [sp], #16 + + + ldr x9, [x19, #0x8] // Load the function + str lr, [x19, #0x8] // store the address of the caller + + blr x9 // call the actual function + + /** + * allocate 32-bytes on the stack, for the 2 return values + the caller + * address that has to preserved across the call to `free` + */ + sub sp, sp, #0x20 + str q0, [sp, #0x0] // 16-byte return value + str x0, [sp, #0x10] // 8-byte return value + + // void RCTProfileTrampolineEnd(void) in RCTProfile.m - just ends this profile + bl SYMBOL_NAME(RCTProfileTrampolineEnd) + + /** + * restore the callee saved registers, move the values we still need to the + * stack and free the allocated memory + */ + mov x0, x19 // move the address of the memory to x0, first argument + ldr x10, [x19, #0x8] // load the caller address + ldr x19, [x19] // restore x19 + str x10, [sp, #0x18] // store x10 on the stack space allocated above + bl SYMBOL_NAME(free) + + // Load both return values and link register from the stack + ldr q0, [sp, #0x0] + ldr x0, [sp, #0x10] + ldr lr, [sp, #0x18] + + // restore the stack pointer + add sp, sp, #0x20 + + // jump to the calleer, without a link + br lr + +#endif diff --git a/React/Profiler/RCTProfileTrampoline-x86.S b/React/Profiler/RCTProfileTrampoline-x86.S new file mode 100644 index 000000000..0ccd37b4f --- /dev/null +++ b/React/Profiler/RCTProfileTrampoline-x86.S @@ -0,0 +1,90 @@ +#include "RCTDefines.h" +#include "RCTMacros.h" + +#if RCT_DEV && defined(__i386__) + + .globl SYMBOL_NAME(RCTProfileTrampoline) +SYMBOL_NAME(RCTProfileTrampoline): + /** + * The x86 version is much simpler, since all the arguments are passed in the + * stack, so we just have to preserve the stack pointer (%esp) and the callee + * saved register used to keep the memory allocated + * + * The explanation here is also shorter, refer to the x86_64 implementation to + * a richer explanation + */ + + /** + * Allocate memory to save the caller of RCTProfileTrampoline (used afterwards + * to return at the end of the function) and the initial value for the callee + * saved register (%edi) that will be used to point to the memory allocated. + */ + subl $0x8, %esp // stack padding (16-byte alignment for function calls) + pushl $0xc // allocate 12-bytes + calll SYMBOL_NAME(malloc) + addl $0xc, %esp // restore stack (8-byte padding + 4-byte argument) + + /** + * actually store the values in the memory allocated + */ + movl %edi, 0x0(%eax) // previous value of edi + popl 0x4(%eax) // caller of RCTProfileTrampoline + + // save the pointer to the allocated memory in %edi + movl %eax, %edi + + /** + * void RCTProfileGetImplementation(id object, SEL selector) in RCTProfile.m + * + * Get the address of the actual C function we have to profile + */ + calll SYMBOL_NAME(RCTProfileGetImplementation) + movl %eax, 0x8(%edi) // Save it in the allocated memory + + /** + * void RCTProfileTrampolineStart(id, SEL) in RCTProfile.m + * + * start profile - the arguments are already in the right position in the + * stack since it takes the same first 2 arguments as the any ObjC function - + * "self" and "_cmd" + */ + calll SYMBOL_NAME(RCTProfileTrampolineStart) + + /** + * Call the actual function and save it's return value, since it should be the + * return value of RCTProfileTrampoline + */ + calll *0x8(%edi) + pushl %eax + + // Align stack and end profile + subl $0xc, %esp + calll SYMBOL_NAME(RCTProfileTrampolineEnd) + addl $0xc, %esp // restore the stack + + /** + * Move the values from the allocated memory to the stack, restore the + * value of %edi, and prepare to free the allocated memory. + */ + pushl 0x4(%edi) // caller of RCTProfileTrampoline + subl $0x4, %esp // Stack padding + pushl %edi // push the memory address + movl 0x0(%edi), %edi // restore the value of %edi + + /** + * Actually free the memory used to store the values across function calls, + * the stack has already been padded and the first and only argument, the + * memory address, is already in the bottom of the stack. + */ + calll SYMBOL_NAME(free) + addl $0x8, %esp + + /** + * pop the caller address to %ecx and the actual function return value to + * %eax, so it's the return value of RCTProfileTrampoline + */ + popl %ecx + popl %eax + jmpl *%ecx + +#endif diff --git a/React/Profiler/RCTProfileTrampoline-x86_64.S b/React/Profiler/RCTProfileTrampoline-x86_64.S new file mode 100644 index 000000000..2f9d17786 --- /dev/null +++ b/React/Profiler/RCTProfileTrampoline-x86_64.S @@ -0,0 +1,191 @@ +#include "RCTDefines.h" +#include "RCTMacros.h" + +#if RCT_DEV && defined(__x86_64__) + +/** + * Define both symbols for compatibility with other assemblers + */ + .globl SYMBOL_NAME(RCTProfileTrampoline) +SYMBOL_NAME(RCTProfileTrampoline): + + /** + * Saves all the state so we can restore it before calling the functions being + * profiled. Registers have the same value at the point of the function call, + * the only thing we can change is the return value, so we return to + * `RCTProfileTrampoline` rather than to its caller. + * + * Save all the parameters registers (%rdi, %rsi, %rdx, %rcx, %r8, %r9), they + * have the 6 first arguments of the function call, and %rax which in special + * cases might be a pointer used for struct returns. + * + * We have to save %r12 since its value should be preserved across function + * calls and we'll use it to keep the stack pointer + */ + subq $0x80+8, %rsp // 8 x 16-bytes xmm registers + 8-bytes alignment + movdqa %xmm0, 0x70(%rsp) + movdqa %xmm1, 0x60(%rsp) + movdqa %xmm2, 0x50(%rsp) + movdqa %xmm3, 0x40(%rsp) + movdqa %xmm4, 0x30(%rsp) + movdqa %xmm5, 0x20(%rsp) + movdqa %xmm6, 0x10(%rsp) + movdqa %xmm7, 0x00(%rsp) + pushq %rdi + pushq %rsi + pushq %rdx + pushq %rcx + pushq %r8 + pushq %r9 + pushq %rax + pushq %r12 + + /** + * Store the stack pointer in the callee saved register %r12 and align the + * stack - it has to 16-byte aligned at the point of the function call + */ + movq %rsp, %r12 + andq $-0x10, %rsp + + /** + * void RCTProfileGetImplementation(id object, SEL selector) + * + * This is a C function defined in `RCTProfile.m`, the object and the selector + * already have to be on %rdi and %rsi respectively, as in any ObjC call. + */ + callq SYMBOL_NAME(RCTProfileGetImplementation) + + // Restore/unalign the stack pointer, so we can access the registers we stored + movq %r12, %rsp + + /** + * pop %r12 before pushing %rax, which contains the address of the actual + * function we have to call, than we keep %r12 at the bottom of the stack to + * reference the stack pointer + */ + popq %r12 + pushq %rax + pushq %r12 + + // align stack + movq %rsp, %r12 + andq $-0x10, %rsp + + /** + * Allocate memory to save parent before start profiling: the address is put + * at the bottom of the stack at the function call, so ret can actually return + * to the caller. In this case it has the address of RCTProfileTrampoline's + * caller where we'll have to return to after we're finished. + * + * We can't store it on the stack or in any register, since we have to be in + * the exact same state we were at the moment we were called, so the solution + * is to allocate a tiny bit of memory to save this address + */ + + // allocate 16 bytes + movq $0x10, %rdi + callq SYMBOL_NAME(malloc) + + // store the initial value of calle saved registers %r13 and %r14 + movq %r13, 0x0(%rax) + movq %r14, 0x8(%rax) + + // mov the pointers we need to the callee saved registers + movq 0xd8(%rsp), %r13 // caller of RCTProfileTrampoline (0xd8 is stack top) + movq %rax, %r14 // allocated memory's address + + /** + * Move self and cmd back to the registers and call start profile: it uses + * the object and the selector to label the call in the profile. + */ + movq 0x40(%r12), %rdi // object + movq 0x38(%r12), %rsi // selector + + // void RCTProfileTrampolineStart(id, SEL) in RCTProfile.m + callq SYMBOL_NAME(RCTProfileTrampolineStart) + + // unalign the stack and restore %r12 + movq %r12, %rsp + popq %r12 + + // Restore registers for actual function call + popq %r11 + popq %rax + popq %r9 + popq %r8 + popq %rcx + popq %rdx + popq %rsi + popq %rdi + movdqa 0x00(%rsp), %xmm7 + movdqa 0x10(%rsp), %xmm6 + movdqa 0x20(%rsp), %xmm5 + movdqa 0x30(%rsp), %xmm4 + movdqa 0x40(%rsp), %xmm3 + movdqa 0x50(%rsp), %xmm2 + movdqa 0x60(%rsp), %xmm1 + movdqa 0x70(%rsp), %xmm0 + addq $0x80+8, %rsp + + /** + * delete parent caller (saved in %r13) `call` will add the new address so + * we return to RCTProfileTrampoline rather than to its caller + */ + addq $0x8, %rsp + + // call the actual function and save the return value + callq *%r11 + pushq %rax + subq $0x10+8, %rsp //16-bytes xmm register + 8-bytes for alignment + movdqa %xmm0, (%rsp) + + // align stack + pushq %r12 + movq %rsp, %r12 + andq $-0x10, %rsp + + // void RCTProfileTrampolineEnd(void) in RCTProfile.m - just ends this profile + callq SYMBOL_NAME(RCTProfileTrampolineEnd) + + // unalign stack and restore %r12 + movq %r12, %rsp + popq %r12 + + /** + * Restore the initial value of the callee saved registers, saved in the + * memory allocated. + */ + movq %r13, %rcx + movq %r14, %rdi + movq 0x0(%r14), %r13 + movq 0x8(%r14), %r14 + + /** + * Save caller address and actual function return (previously in the allocated + * memory) and align the stack + */ + pushq %rcx + pushq %r12 + movq %rsp, %r12 + andq $-0x10, %rsp + + // Free the memory allocated to stash callee saved registers + callq SYMBOL_NAME(free) + + // unalign stack and restore %r12 + movq %r12, %rsp + popq %r12 + + /** + * pop the caller address to %rcx and the actual function return value to + * %rax, so it's the return value of RCTProfileTrampoline + */ + popq %rcx + movdqa (%rsp), %xmm0 + addq $0x10+8, %rsp + popq %rax + + // jump to caller + jmpq *%rcx + +#endif diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index be9efcb83..d7383e604 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -49,12 +49,15 @@ 13E067571A70F44B002CDEE1 /* RCTView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067501A70F44B002CDEE1 /* RCTView.m */; }; 13E067591A70F44B002CDEE1 /* UIView+React.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+React.m */; }; 13F17A851B8493E5007D4C75 /* RCTRedBox.m in Sources */ = {isa = PBXBuildFile; fileRef = 13F17A841B8493E5007D4C75 /* RCTRedBox.m */; }; - 1403F2B31B0AE60700C2A9A4 /* RCTPerfStats.m in Sources */ = {isa = PBXBuildFile; fileRef = 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */; }; 14200DAA1AC179B3008EE6BA /* RCTJavaScriptLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */; }; 142014191B32094000CC17BA /* RCTPerformanceLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 142014171B32094000CC17BA /* RCTPerformanceLogger.m */; }; 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE21AAC4AE100FC20F4 /* RCTMap.m */; }; 14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */; }; - 146459261B06C49500B389AA /* RCTFPSGraph.m in Sources */ = {isa = PBXBuildFile; fileRef = 146459251B06C49500B389AA /* RCTFPSGraph.m */; }; + 1450FF861BCFF28A00208362 /* RCTProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 1450FF811BCFF28A00208362 /* RCTProfile.m */; settings = {ASSET_TAGS = (); }; }; + 1450FF871BCFF28A00208362 /* RCTProfileTrampoline-arm.S in Sources */ = {isa = PBXBuildFile; fileRef = 1450FF821BCFF28A00208362 /* RCTProfileTrampoline-arm.S */; settings = {ASSET_TAGS = (); }; }; + 1450FF881BCFF28A00208362 /* RCTProfileTrampoline-arm64.S in Sources */ = {isa = PBXBuildFile; fileRef = 1450FF831BCFF28A00208362 /* RCTProfileTrampoline-arm64.S */; settings = {ASSET_TAGS = (); }; }; + 1450FF891BCFF28A00208362 /* RCTProfileTrampoline-x86.S in Sources */ = {isa = PBXBuildFile; fileRef = 1450FF841BCFF28A00208362 /* RCTProfileTrampoline-x86.S */; settings = {ASSET_TAGS = (); }; }; + 1450FF8A1BCFF28A00208362 /* RCTProfileTrampoline-x86_64.S in Sources */ = {isa = PBXBuildFile; fileRef = 1450FF851BCFF28A00208362 /* RCTProfileTrampoline-x86_64.S */; settings = {ASSET_TAGS = (); }; }; 14C2CA711B3AC63800E6CBB2 /* RCTModuleMethod.m in Sources */ = {isa = PBXBuildFile; fileRef = 14C2CA701B3AC63800E6CBB2 /* RCTModuleMethod.m */; }; 14C2CA741B3AC64300E6CBB2 /* RCTModuleData.m in Sources */ = {isa = PBXBuildFile; fileRef = 14C2CA731B3AC64300E6CBB2 /* RCTModuleData.m */; }; 14C2CA761B3AC64F00E6CBB2 /* RCTFrameUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 14C2CA751B3AC64F00E6CBB2 /* RCTFrameUpdate.m */; }; @@ -62,7 +65,8 @@ 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F362081AABD06A001CE568 /* RCTSwitch.m */; }; 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F3620A1AABD06A001CE568 /* RCTSwitchManager.m */; }; 14F484561AABFCE100FDF6B9 /* RCTSliderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F484551AABFCE100FDF6B9 /* RCTSliderManager.m */; }; - 14F4D38B1AE1B7E40049C042 /* RCTProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F4D38A1AE1B7E40049C042 /* RCTProfile.m */; }; + 14F7A0EC1BDA3B3C003C6C10 /* RCTPerfMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F7A0EB1BDA3B3C003C6C10 /* RCTPerfMonitor.m */; settings = {ASSET_TAGS = (); }; }; + 14F7A0F01BDA714B003C6C10 /* RCTFPSGraph.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F7A0EF1BDA714B003C6C10 /* RCTFPSGraph.m */; settings = {ASSET_TAGS = (); }; }; 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A131AAE854800E7D092 /* RCTPicker.m */; }; 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A151AAE854800E7D092 /* RCTPickerManager.m */; }; 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */; }; @@ -192,8 +196,6 @@ 13EF7F441BC69646003F47DD /* RCTImageComponent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageComponent.h; sourceTree = ""; }; 13F17A831B8493E5007D4C75 /* RCTRedBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRedBox.h; sourceTree = ""; }; 13F17A841B8493E5007D4C75 /* RCTRedBox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRedBox.m; sourceTree = ""; }; - 1403F2B11B0AE60700C2A9A4 /* RCTPerfStats.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPerfStats.h; sourceTree = ""; }; - 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPerfStats.m; sourceTree = ""; }; 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = ""; }; 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJavaScriptLoader.m; sourceTree = ""; }; 142014171B32094000CC17BA /* RCTPerformanceLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPerformanceLogger.m; sourceTree = ""; }; @@ -203,8 +205,12 @@ 14435CE21AAC4AE100FC20F4 /* RCTMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMap.m; sourceTree = ""; }; 14435CE31AAC4AE100FC20F4 /* RCTMapManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMapManager.h; sourceTree = ""; }; 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMapManager.m; sourceTree = ""; }; - 146459241B06C49500B389AA /* RCTFPSGraph.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTFPSGraph.h; sourceTree = ""; }; - 146459251B06C49500B389AA /* RCTFPSGraph.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTFPSGraph.m; sourceTree = ""; }; + 1450FF801BCFF28A00208362 /* RCTProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTProfile.h; sourceTree = ""; }; + 1450FF811BCFF28A00208362 /* RCTProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTProfile.m; sourceTree = ""; }; + 1450FF821BCFF28A00208362 /* RCTProfileTrampoline-arm.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = "RCTProfileTrampoline-arm.S"; sourceTree = ""; }; + 1450FF831BCFF28A00208362 /* RCTProfileTrampoline-arm64.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = "RCTProfileTrampoline-arm64.S"; sourceTree = ""; }; + 1450FF841BCFF28A00208362 /* RCTProfileTrampoline-x86.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = "RCTProfileTrampoline-x86.S"; sourceTree = ""; }; + 1450FF851BCFF28A00208362 /* RCTProfileTrampoline-x86_64.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = "RCTProfileTrampoline-x86_64.S"; sourceTree = ""; }; 1482F9E61B55B927000ADFF3 /* RCTBridgeDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeDelegate.h; sourceTree = ""; }; 14C2CA6F1B3AC63800E6CBB2 /* RCTModuleMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTModuleMethod.h; sourceTree = ""; }; 14C2CA701B3AC63800E6CBB2 /* RCTModuleMethod.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleMethod.m; sourceTree = ""; }; @@ -218,8 +224,9 @@ 14F3620A1AABD06A001CE568 /* RCTSwitchManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSwitchManager.m; sourceTree = ""; }; 14F484541AABFCE100FDF6B9 /* RCTSliderManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSliderManager.h; sourceTree = ""; }; 14F484551AABFCE100FDF6B9 /* RCTSliderManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSliderManager.m; sourceTree = ""; }; - 14F4D3891AE1B7E40049C042 /* RCTProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTProfile.h; sourceTree = ""; }; - 14F4D38A1AE1B7E40049C042 /* RCTProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTProfile.m; sourceTree = ""; }; + 14F7A0EB1BDA3B3C003C6C10 /* RCTPerfMonitor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPerfMonitor.m; sourceTree = ""; }; + 14F7A0EE1BDA714B003C6C10 /* RCTFPSGraph.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTFPSGraph.h; sourceTree = ""; }; + 14F7A0EF1BDA714B003C6C10 /* RCTFPSGraph.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTFPSGraph.m; sourceTree = ""; }; 58114A121AAE854800E7D092 /* RCTPicker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPicker.h; sourceTree = ""; }; 58114A131AAE854800E7D092 /* RCTPicker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPicker.m; sourceTree = ""; }; 58114A141AAE854800E7D092 /* RCTPickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPickerManager.h; sourceTree = ""; }; @@ -230,6 +237,7 @@ 58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDatePickerManager.h; sourceTree = ""; }; 63F014BE1B02080B003B75D2 /* RCTPointAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointAnnotation.h; sourceTree = ""; }; 63F014BF1B02080B003B75D2 /* RCTPointAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPointAnnotation.m; sourceTree = ""; }; + 6A15FB0C1BDF663500531DFB /* RCTRootViewInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootViewInternal.h; sourceTree = ""; }; 830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = ""; }; 830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = ""; }; 830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = ""; }; @@ -422,6 +430,22 @@ path = Views; sourceTree = ""; }; + 1450FF7F1BCFF28A00208362 /* Profiler */ = { + isa = PBXGroup; + children = ( + 14F7A0EB1BDA3B3C003C6C10 /* RCTPerfMonitor.m */, + 14F7A0EE1BDA714B003C6C10 /* RCTFPSGraph.h */, + 14F7A0EF1BDA714B003C6C10 /* RCTFPSGraph.m */, + 1450FF801BCFF28A00208362 /* RCTProfile.h */, + 1450FF811BCFF28A00208362 /* RCTProfile.m */, + 1450FF821BCFF28A00208362 /* RCTProfileTrampoline-arm.S */, + 1450FF831BCFF28A00208362 /* RCTProfileTrampoline-arm64.S */, + 1450FF841BCFF28A00208362 /* RCTProfileTrampoline-x86.S */, + 1450FF851BCFF28A00208362 /* RCTProfileTrampoline-x86_64.S */, + ); + path = Profiler; + sourceTree = ""; + }; 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( @@ -447,6 +471,7 @@ 134FCB381A6E7F0800051CC8 /* Executors */, 13B07FC41A68125100A75B9A /* Layout */, 13B07FE01A69315300A75B9A /* Modules */, + 1450FF7F1BCFF28A00208362 /* Profiler */, 13B07FF31A6947C200A75B9A /* Views */, ); name = React; @@ -455,6 +480,7 @@ 83CBBA491A601E3B00E9B192 /* Base */ = { isa = PBXGroup; children = ( + 6A15FB0C1BDF663500531DFB /* RCTRootViewInternal.h */, 83CBBA4A1A601E3B00E9B192 /* RCTAssert.h */, 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */, 14C2CA771B3ACB0400E6CBB2 /* RCTBatchedBridge.m */, @@ -467,8 +493,6 @@ 13AF1F851AE6E777005F5298 /* RCTDefines.h */, 83CBBA651A601EF300E9B192 /* RCTEventDispatcher.h */, 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */, - 146459241B06C49500B389AA /* RCTFPSGraph.h */, - 146459251B06C49500B389AA /* RCTFPSGraph.m */, 1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */, 14C2CA751B3AC64F00E6CBB2 /* RCTFrameUpdate.m */, 83CBBA4C1A601E3B00E9B192 /* RCTInvalidating.h */, @@ -489,10 +513,6 @@ 14C2CA701B3AC63800E6CBB2 /* RCTModuleMethod.m */, 142014181B32094000CC17BA /* RCTPerformanceLogger.h */, 142014171B32094000CC17BA /* RCTPerformanceLogger.m */, - 1403F2B11B0AE60700C2A9A4 /* RCTPerfStats.h */, - 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */, - 14F4D3891AE1B7E40049C042 /* RCTProfile.h */, - 14F4D38A1AE1B7E40049C042 /* RCTProfile.m */, 830A229C1A66C68A008503DA /* RCTRootView.h */, 830A229D1A66C68A008503DA /* RCTRootView.m */, 83BEE46C1A6D19BC00B5863B /* RCTSparseArray.h */, @@ -614,15 +634,17 @@ 8385CF351B8B77CD00C6273E /* RCTKeyboardObserver.m in Sources */, 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */, 832348161A77A5AA00B55238 /* Layout.c in Sources */, - 14F4D38B1AE1B7E40049C042 /* RCTProfile.m in Sources */, 13513F3C1B1F43F400FCE529 /* RCTProgressViewManager.m in Sources */, + 14F7A0F01BDA714B003C6C10 /* RCTFPSGraph.m in Sources */, 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */, 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */, 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */, 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */, 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */, + 1450FF8A1BCFF28A00208362 /* RCTProfileTrampoline-x86_64.S in Sources */, + 14F7A0EC1BDA3B3C003C6C10 /* RCTPerfMonitor.m in Sources */, + 1450FF881BCFF28A00208362 /* RCTProfileTrampoline-arm64.S in Sources */, 13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */, - 146459261B06C49500B389AA /* RCTFPSGraph.m in Sources */, 14200DAA1AC179B3008EE6BA /* RCTJavaScriptLoader.m in Sources */, 137327EA1AA5CF210034F82E /* RCTTabBarManager.m in Sources */, 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */, @@ -654,6 +676,7 @@ 137327E81AA5CF210034F82E /* RCTTabBarItem.m in Sources */, 83A1FE8C1B62640A00BE0E65 /* RCTModalHostView.m in Sources */, 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */, + 1450FF871BCFF28A00208362 /* RCTProfileTrampoline-arm.S in Sources */, 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */, 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */, 13B0801A1A69489C00A75B9A /* RCTNavigator.m in Sources */, @@ -662,13 +685,14 @@ 13F17A851B8493E5007D4C75 /* RCTRedBox.m in Sources */, 83392EB31B6634E10013B15F /* RCTModalHostViewController.m in Sources */, 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */, + 1450FF891BCFF28A00208362 /* RCTProfileTrampoline-x86.S in Sources */, 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */, 13B0801C1A69489C00A75B9A /* RCTNavItem.m in Sources */, 1385D0341B665AAE000A309B /* RCTModuleMap.m in Sources */, - 1403F2B31B0AE60700C2A9A4 /* RCTPerfStats.m in Sources */, 83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */, 83A1FE8F1B62643A00BE0E65 /* RCTModalHostViewManager.m in Sources */, 13E0674A1A70F434002CDEE1 /* RCTUIManager.m in Sources */, + 1450FF861BCFF28A00208362 /* RCTProfile.m in Sources */, 13AB90C11B6FA36700713B4F /* RCTComponentData.m in Sources */, 13B0801B1A69489C00A75B9A /* RCTNavigatorManager.m in Sources */, ); diff --git a/React/Views/RCTComponent.h b/React/Views/RCTComponent.h index 215acb1d0..2940b4311 100644 --- a/React/Views/RCTComponent.h +++ b/React/Views/RCTComponent.h @@ -27,7 +27,7 @@ typedef void (^RCTBubblingEventBlock)(NSDictionary *body); - (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex; - (void)removeReactSubview:(id)subview; -- (NSArray *)reactSubviews; +- (NSArray> *)reactSubviews; - (id)reactSuperview; - (NSNumber *)reactTagAtPoint:(CGPoint)point; diff --git a/React/Views/RCTComponentData.m b/React/Views/RCTComponentData.m index c6fd94127..1f208150d 100644 --- a/React/Views/RCTComponentData.m +++ b/React/Views/RCTComponentData.m @@ -100,7 +100,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) SEL selector = NSSelectorFromString([NSString stringWithFormat:@"propConfig%@_%@", shadowView ? @"Shadow" : @"", name]); Class managerClass = [_manager class]; if ([managerClass respondsToSelector:selector]) { - NSArray *typeAndKeyPath = ((NSArray *(*)(id, SEL))objc_msgSend)(managerClass, selector); + NSArray *typeAndKeyPath = + ((NSArray *(*)(id, SEL))objc_msgSend)(managerClass, selector); type = NSSelectorFromString([typeAndKeyPath[0] stringByAppendingString:@":"]); keyPath = typeAndKeyPath.count > 1 ? typeAndKeyPath[1] : nil; } else { @@ -125,7 +126,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) // Disect keypath NSString *key = name; - NSArray *parts = [keyPath componentsSeparatedByString:@"."]; + NSArray *parts = [keyPath componentsSeparatedByString:@"."]; if (parts) { key = parts.lastObject; parts = [parts subarrayWithRange:(NSRange){0, parts.count - 1}]; @@ -312,9 +313,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) { Class managerClass = [_manager class]; - NSMutableArray *directEvents = [NSMutableArray new]; + NSMutableArray *directEvents = [NSMutableArray new]; if (RCTClassOverridesInstanceMethod(managerClass, @selector(customDirectEventTypes))) { - NSArray *events = [_manager customDirectEventTypes]; + NSArray *events = [_manager customDirectEventTypes]; if (RCT_DEBUG) { RCTAssert(!events || [events isKindOfClass:[NSArray class]], @"customDirectEventTypes must return an array, but %@ returned %@", @@ -325,9 +326,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) } } - NSMutableArray *bubblingEvents = [NSMutableArray new]; + NSMutableArray *bubblingEvents = [NSMutableArray new]; if (RCTClassOverridesInstanceMethod(managerClass, @selector(customBubblingEventTypes))) { - NSArray *events = [_manager customBubblingEventTypes]; + NSArray *events = [_manager customBubblingEventTypes]; if (RCT_DEBUG) { RCTAssert(!events || [events isKindOfClass:[NSArray class]], @"customBubblingEventTypes must return an array, but %@ returned %@", @@ -349,7 +350,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) NSRange nameRange = [methodName rangeOfString:@"_"]; if (nameRange.length) { NSString *name = [methodName substringFromIndex:nameRange.location + 1]; - NSString *type = ((NSArray *(*)(id, SEL))objc_msgSend)(managerClass, selector)[0]; + NSString *type = ((NSArray *(*)(id, SEL))objc_msgSend)(managerClass, selector)[0]; if (RCT_DEBUG && propTypes[name] && ![propTypes[name] isEqualToString:type]) { RCTLogError(@"Property '%@' of component '%@' redefined from '%@' " "to '%@'", name, _name, propTypes[name], type); diff --git a/React/Views/RCTMap.h b/React/Views/RCTMap.h index 65e6aec94..c9025d6d3 100644 --- a/React/Views/RCTMap.h +++ b/React/Views/RCTMap.h @@ -25,7 +25,7 @@ extern const CGFloat RCTMapZoomBoundBuffer; @property (nonatomic, assign) CGFloat maxDelta; @property (nonatomic, assign) UIEdgeInsets legalLabelInsets; @property (nonatomic, strong) NSTimer *regionChangeObserveTimer; -@property (nonatomic, strong) NSMutableArray *annotationIds; +@property (nonatomic, copy) NSArray *annotationIds; @property (nonatomic, copy) RCTBubblingEventBlock onChange; @property (nonatomic, copy) RCTBubblingEventBlock onPress; diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index 16208957b..238c7a4d3 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -47,11 +47,6 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; [_regionChangeObserveTimer invalidate]; } -- (void)reactSetFrame:(CGRect)frame -{ - self.frame = frame; -} - - (void)layoutSubviews { [super layoutSubviews]; @@ -114,9 +109,9 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; - (void)setAnnotations:(RCTPointAnnotationArray *)annotations { - NSMutableArray *newAnnotationIds = [NSMutableArray new]; - NSMutableArray *annotationsToDelete = [NSMutableArray new]; - NSMutableArray *annotationsToAdd = [NSMutableArray new]; + NSMutableArray *newAnnotationIds = [NSMutableArray new]; + NSMutableArray *annotationsToDelete = [NSMutableArray new]; + NSMutableArray *annotationsToAdd = [NSMutableArray new]; for (RCTPointAnnotation *annotation in annotations) { if (![annotation isKindOfClass:[RCTPointAnnotation class]]) { @@ -143,19 +138,19 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; } if (annotationsToDelete.count) { - [self removeAnnotations:annotationsToDelete]; + [self removeAnnotations:(NSArray> *)annotationsToDelete]; } if (annotationsToAdd.count) { - [self addAnnotations:annotationsToAdd]; + [self addAnnotations:(NSArray> *)annotationsToAdd]; } - NSMutableArray *newIds = [NSMutableArray new]; - for (RCTPointAnnotation *anno in self.annotations) { - if ([anno isKindOfClass:[MKUserLocation class]]) { + NSMutableArray *newIds = [NSMutableArray new]; + for (RCTPointAnnotation *annotation in self.annotations) { + if ([annotation isKindOfClass:[MKUserLocation class]]) { continue; } - [newIds addObject:anno.identifier]; + [newIds addObject:annotation.identifier]; } self.annotationIds = newIds; } diff --git a/React/Views/RCTMapManager.m b/React/Views/RCTMapManager.m index f4ee06f98..44dd0743e 100644 --- a/React/Views/RCTMapManager.m +++ b/React/Views/RCTMapManager.m @@ -37,6 +37,7 @@ RCT_EXPORT_MODULE() } RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL) +RCT_EXPORT_VIEW_PROPERTY(showsPointsOfInterest, BOOL) RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(rotateEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL) diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m index 9b755e515..c82ab5249 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -51,18 +51,18 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:coder) } } -- (NSArray *)reactSubviews +- (NSArray *> *)reactSubviews { - return [NSArray arrayWithObjects:_modalViewController.view, nil]; + return _modalViewController.view ? @[_modalViewController.view] : @[]; } -- (void)insertReactSubview:(UIView *)subview atIndex:(__unused NSInteger)atIndex +- (void)insertReactSubview:(UIView *)subview atIndex:(__unused NSInteger)atIndex { [subview addGestureRecognizer:_touchHandler]; _modalViewController.view = subview; } -- (void)removeReactSubview:(UIView *)subview +- (void)removeReactSubview:(UIView *)subview { RCTAssert(subview == _modalViewController.view, @"Cannot remove view other than modal view"); _modalViewController.view = nil; @@ -76,15 +76,22 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:coder) } } +- (void)didMoveToWindow +{ + [super didMoveToWindow]; + + if (!_isPresented && self.window) { + RCTAssert(self.reactViewController, @"Can't present modal view controller without a presenting view controller"); + [self.reactViewController presentViewController:_modalViewController animated:self.animated completion:nil]; + _isPresented = YES; + } +} + - (void)didMoveToSuperview { [super didMoveToSuperview]; - if (self.superview) { - RCTAssert(self.reactViewController, @"Can't present modal view controller without a presenting view controller"); - [self.reactViewController presentViewController:_modalViewController animated:self.animated completion:nil]; - _isPresented = YES; - } else { + if (_isPresented && !self.superview) { [self dismissModalViewController]; } } diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index 8b67222aa..460a5fc6e 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -200,8 +200,8 @@ NSInteger kNeverProgressed = -10000; // Previous views are only mainted in order to detect incorrect // addition/removal of views below the `requestedTopOfStack` -@property (nonatomic, copy, readwrite) NSArray *previousViews; -@property (nonatomic, readwrite, strong) NSMutableArray *currentViews; +@property (nonatomic, copy, readwrite) NSArray *previousViews; +@property (nonatomic, readwrite, strong) NSMutableArray *currentViews; @property (nonatomic, readwrite, strong) RCTNavigationController *navigationController; /** * Display link is used to get high frequency sample rate during @@ -335,6 +335,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)dealloc { _navigationController.delegate = nil; + [_navigationController removeFromParentViewController]; } - (UIViewController *)reactViewController @@ -399,7 +400,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) * `requestedTopOfStack` changes, there had better be enough subviews present * to satisfy the push/pop. */ -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex +- (void)insertReactSubview:(RCTNavItem *)view atIndex:(NSInteger)atIndex { RCTAssert([view isKindOfClass:[RCTNavItem class]], @"RCTNavigator only accepts RCTNavItem subviews"); RCTAssert( @@ -409,7 +410,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [_currentViews insertObject:view atIndex:atIndex]; } -- (NSArray *)reactSubviews +- (NSArray *)reactSubviews { return _currentViews; } @@ -417,10 +418,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)layoutSubviews { [super layoutSubviews]; + [self reactAddControllerToClosestParent:_navigationController]; _navigationController.view.frame = self.bounds; } -- (void)removeReactSubview:(UIView *)subview +- (void)removeReactSubview:(RCTNavItem *)subview { if (_currentViews.count <= 0 || subview == _currentViews[0]) { RCTLogError(@"Attempting to remove invalid RCT subview of RCTNavigator"); diff --git a/React/Views/RCTPicker.h b/React/Views/RCTPicker.h index 704fe7587..8fb39b5a5 100644 --- a/React/Views/RCTPicker.h +++ b/React/Views/RCTPicker.h @@ -9,6 +9,12 @@ #import +#import "UIView+React.h" + @interface RCTPicker : UIPickerView +@property (nonatomic, copy) NSArray *items; +@property (nonatomic, assign) NSInteger selectedIndex; +@property (nonatomic, copy) RCTBubblingEventBlock onChange; + @end diff --git a/React/Views/RCTPicker.m b/React/Views/RCTPicker.m index 1f256cc48..f979c2116 100644 --- a/React/Views/RCTPicker.m +++ b/React/Views/RCTPicker.m @@ -10,14 +10,8 @@ #import "RCTPicker.h" #import "RCTUtils.h" -#import "UIView+React.h" @interface RCTPicker() - -@property (nonatomic, copy) NSArray *items; -@property (nonatomic, assign) NSInteger selectedIndex; -@property (nonatomic, copy) RCTBubblingEventBlock onChange; - @end @implementation RCTPicker @@ -33,7 +27,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) -- (void)setItems:(NSArray *)items +- (void)setItems:(NSArray *)items { _items = [items copy]; [self setNeedsLayout]; diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index 1e085d265..1dfb9c1c9 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -117,7 +117,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) - (RCTScrollEvent *)coalesceWithEvent:(RCTScrollEvent *)newEvent { - NSArray *updatedChildFrames = [_userData[@"updatedChildFrames"] arrayByAddingObjectsFromArray:newEvent->_userData[@"updatedChildFrames"]]; + NSArray *updatedChildFrames = [_userData[@"updatedChildFrames"] arrayByAddingObjectsFromArray:newEvent->_userData[@"updatedChildFrames"]]; if (updatedChildFrames) { NSMutableDictionary *userData = [newEvent->_userData mutableCopy]; @@ -360,7 +360,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) RCTCustomScrollView *_scrollView; UIView *_contentView; NSTimeInterval _lastScrollDispatchTime; - NSMutableArray *_cachedChildFrames; + NSMutableArray *_cachedChildFrames; BOOL _allowNextScrollNoMatterWhat; CGRect _lastClippedToRect; } @@ -412,7 +412,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [subview removeFromSuperview]; } -- (NSArray *)reactSubviews +- (NSArray *> *)reactSubviews { return _contentView ? @[_contentView] : @[]; } @@ -564,7 +564,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove) (_scrollEventThrottle > 0 && _scrollEventThrottle < (now - _lastScrollDispatchTime))) { // Calculate changed frames - NSArray *childFrames = [self calculateChildFramesData]; + NSArray *childFrames = [self calculateChildFramesData]; // Dispatch event [_eventDispatcher sendScrollEventWithType:RCTScrollEventTypeMove @@ -579,9 +579,9 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove) RCT_FORWARD_SCROLL_EVENT(scrollViewDidScroll:scrollView); } -- (NSArray *)calculateChildFramesData +- (NSArray *)calculateChildFramesData { - NSMutableArray *updatedChildFrames = [NSMutableArray new]; + NSMutableArray *updatedChildFrames = [NSMutableArray new]; [[_contentView reactSubviews] enumerateObjectsUsingBlock: ^(UIView *subview, NSUInteger idx, __unused BOOL *stop) { diff --git a/React/Views/RCTScrollViewManager.m b/React/Views/RCTScrollViewManager.m index 18f5eaf1d..6470f10f1 100644 --- a/React/Views/RCTScrollViewManager.m +++ b/React/Views/RCTScrollViewManager.m @@ -16,7 +16,7 @@ @interface RCTScrollView (Private) -- (NSArray *)calculateChildFramesData; +- (NSArray *)calculateChildFramesData; @end @@ -83,13 +83,13 @@ RCT_EXPORT_METHOD(getContentSize:(nonnull NSNumber *)reactTag { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - UIView *view = viewRegistry[reactTag]; - if (!view) { - RCTLogError(@"Cannot find view with tag #%@", reactTag); + RCTScrollView *view = viewRegistry[reactTag]; + if (!view || ![view isKindOfClass:[RCTScrollView class]]) { + RCTLogError(@"Cannot find RCTScrollView with tag #%@", reactTag); return; } - CGSize size = ((RCTScrollView *)view).scrollView.contentSize; + CGSize size = view.scrollView.contentSize; callback(@[@{ @"width" : @(size.width), @"height" : @(size.height) @@ -102,20 +102,20 @@ RCT_EXPORT_METHOD(calculateChildFrames:(nonnull NSNumber *)reactTag { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - UIView *view = viewRegistry[reactTag]; - if (!view) { - RCTLogError(@"Cannot find view with tag #%@", reactTag); + RCTScrollView *view = viewRegistry[reactTag]; + if (!view || ![view isKindOfClass:[RCTScrollView class]]) { + RCTLogError(@"Cannot find RCTScrollView with tag #%@", reactTag); return; } - NSArray *childFrames = [((RCTScrollView *)view) calculateChildFramesData]; + NSArray *childFrames = [view calculateChildFramesData]; if (childFrames) { callback(@[childFrames]); } }]; } -- (NSArray *)customDirectEventTypes +- (NSArray *)customDirectEventTypes { return @[ @"scrollBeginDrag", diff --git a/React/Views/RCTSegmentedControl.h b/React/Views/RCTSegmentedControl.h index 500a236e9..296afb10e 100644 --- a/React/Views/RCTSegmentedControl.h +++ b/React/Views/RCTSegmentedControl.h @@ -13,7 +13,7 @@ @interface RCTSegmentedControl : UISegmentedControl -@property (nonatomic, copy) NSArray *values; +@property (nonatomic, copy) NSArray *values; @property (nonatomic, assign) NSInteger selectedIndex; @property (nonatomic, copy) RCTBubblingEventBlock onChange; diff --git a/React/Views/RCTSegmentedControl.m b/React/Views/RCTSegmentedControl.m index 1857bb0ad..4d10a3d20 100644 --- a/React/Views/RCTSegmentedControl.m +++ b/React/Views/RCTSegmentedControl.m @@ -25,7 +25,7 @@ return self; } -- (void)setValues:(NSArray *)values +- (void)setValues:(NSArray *)values { _values = [values copy]; [self removeAllSegments]; diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 9aa7f0299..1d697083a 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -11,6 +11,7 @@ #import "Layout.h" #import "RCTComponent.h" +#import "RCTRootView.h" @class RCTSparseArray; @@ -34,6 +35,9 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *viewRegistry); */ @interface RCTShadowView : NSObject +- (NSArray *)reactSubviews; +- (RCTShadowView *)reactSuperview; + @property (nonatomic, weak, readonly) RCTShadowView *superview; @property (nonatomic, assign, readonly) css_node_t *cssNode; @property (nonatomic, copy) NSString *viewName; @@ -64,6 +68,12 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *viewRegistry); - (void)setTopLeft:(CGPoint)topLeft; - (void)setSize:(CGSize)size; +/** + * Size flexibility type used to find size constraints. + * Default to RCTRootViewSizeFlexibilityNone + */ +@property (nonatomic, assign) RCTRootViewSizeFlexibility sizeFlexibility; + /** * Border. Defaults to { 0, 0, 0, 0 }. */ @@ -113,28 +123,27 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *viewRegistry); * The applierBlocks set contains RCTApplierBlock functions that must be applied * on the main thread in order to update the view. */ -- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks +- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties; /** * Process the updated properties and apply them to view. Shadow view classes * that add additional propagating properties should override this method. */ -- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks +- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties NS_REQUIRES_SUPER; /** * Calculate all views whose frame needs updating after layout has been calculated. * The viewsWithNewFrame set contains the reactTags of the views that need updating. */ -- (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame - parentConstraint:(CGSize)parentConstraint; +- (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame; /** * Recursively apply layout to children. */ - (void)applyLayoutNode:(css_node_t *)node - viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame absolutePosition:(CGPoint)absolutePosition NS_REQUIRES_SUPER; /** diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index f1b942b2d..ed4fef1d9 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -36,7 +36,7 @@ typedef NS_ENUM(unsigned int, meta_prop_t) { RCTUpdateLifecycle _propagationLifecycle; RCTUpdateLifecycle _textLifecycle; NSDictionary *_lastParentProperties; - NSMutableArray *_reactSubviews; + NSMutableArray *_reactSubviews; BOOL _recomputePadding; BOOL _recomputeMargin; BOOL _recomputeBorder; @@ -123,7 +123,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st // You'll notice that this is the same width we calculated for the parent view because we've taken its position into account. - (void)applyLayoutNode:(css_node_t *)node - viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame absolutePosition:(CGPoint)absolutePosition { if (!node->layout.should_update) { @@ -171,7 +171,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st } } -- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks +- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties { // TODO: we always refresh all propagated properties when propagation is @@ -201,7 +201,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st return parentProperties; } -- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks +- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties { if (_propagationLifecycle == RCTUpdateLifecycleComputed && [parentProperties isEqualToDictionary:_lastParentProperties]) { @@ -215,9 +215,32 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st } } -- (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame - parentConstraint:(__unused CGSize)parentConstraint + +- (void)applySizeConstraints { + switch (_sizeFlexibility) { + case RCTRootViewSizeFlexibilityNone: + break; + case RCTRootViewSizeFlexibilityWidth: + _cssNode->style.dimensions[CSS_WIDTH] = CSS_UNDEFINED; + break; + case RCTRootViewSizeFlexibilityHeight: + _cssNode->style.dimensions[CSS_HEIGHT] = CSS_UNDEFINED; + break; + case RCTRootViewSizeFlexibilityWidthAndHeight: + _cssNode->style.dimensions[CSS_WIDTH] = CSS_UNDEFINED; + _cssNode->style.dimensions[CSS_HEIGHT] = CSS_UNDEFINED; + break; + } +} + +- (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame +{ + RCTAssert(RCTIsReactRootView(self.reactTag), + @"The method has been called on a view with react tag %@, which is not a root view", self.reactTag); + + [self applySizeConstraints]; + [self fillCSSNode:_cssNode]; layoutNode(_cssNode, CSS_UNDEFINED, CSS_DIRECTION_INHERIT); [self applyLayoutNode:_cssNode viewsWithNewFrame:viewsWithNewFrame absolutePosition:CGPointZero]; @@ -245,6 +268,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st if ((self = [super init])) { _frame = CGRectMake(0, 0, CSS_UNDEFINED, CSS_UNDEFINED); + _sizeFlexibility = RCTRootViewSizeFlexibilityNone; for (unsigned int ii = 0; ii < META_PROP_COUNT; ii++) { _paddingMetaProps[ii] = CSS_UNDEFINED; @@ -343,7 +367,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st _cssNode->children_count = (int)_reactSubviews.count; } -- (NSArray *)reactSubviews +- (NSArray *)reactSubviews { return _reactSubviews; } diff --git a/React/Views/RCTSlider.h b/React/Views/RCTSlider.h index 664b0689b..38e471cde 100644 --- a/React/Views/RCTSlider.h +++ b/React/Views/RCTSlider.h @@ -13,6 +13,10 @@ @interface RCTSlider : UISlider -@property (nonatomic, copy) RCTBubblingEventBlock onChange; +@property (nonatomic, copy) RCTBubblingEventBlock onValueChange; +@property (nonatomic, copy) RCTBubblingEventBlock onSlidingComplete; + +@property (nonatomic, assign) float step; +@property (nonatomic, assign) float lastValue; @end diff --git a/React/Views/RCTSliderManager.m b/React/Views/RCTSliderManager.m index 60f65aa68..bef916c16 100644 --- a/React/Views/RCTSliderManager.m +++ b/React/Views/RCTSliderManager.m @@ -21,21 +21,47 @@ RCT_EXPORT_MODULE() - (UIView *)view { RCTSlider *slider = [RCTSlider new]; - [slider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged]; - [slider addTarget:self action:@selector(sliderTouchEnd:) forControlEvents:(UIControlEventTouchUpInside | - UIControlEventTouchUpOutside | - UIControlEventTouchCancel)]; + [slider addTarget:self action:@selector(sliderValueChanged:) + forControlEvents:UIControlEventValueChanged]; + [slider addTarget:self action:@selector(sliderTouchEnd:) + forControlEvents:(UIControlEventTouchUpInside | + UIControlEventTouchUpOutside | + UIControlEventTouchCancel)]; return slider; } static void RCTSendSliderEvent(RCTSlider *sender, BOOL continuous) { - if (sender.onChange) { - sender.onChange(@{ - @"value": @(sender.value), - @"continuous": @(continuous), - }); + float value = sender.value; + + if (sender.step > 0 && + sender.step <= (sender.maximumValue - sender.minimumValue)) { + + value = + MAX(sender.minimumValue, + MIN(sender.maximumValue, + sender.minimumValue + round((sender.value - sender.minimumValue) / sender.step) * sender.step + ) + ); + + [sender setValue:value animated:YES]; } + + if (continuous) { + if (sender.onValueChange && sender.lastValue != value) { + sender.onValueChange(@{ + @"value": @(value), + }); + } + } else { + if (sender.onSlidingComplete) { + sender.onSlidingComplete(@{ + @"value": @(value), + }); + } + } + + sender.lastValue = value; } - (void)sliderValueChanged:(RCTSlider *)sender @@ -49,10 +75,20 @@ static void RCTSendSliderEvent(RCTSlider *sender, BOOL continuous) } RCT_EXPORT_VIEW_PROPERTY(value, float); +RCT_EXPORT_VIEW_PROPERTY(step, float); RCT_EXPORT_VIEW_PROPERTY(minimumValue, float); RCT_EXPORT_VIEW_PROPERTY(maximumValue, float); RCT_EXPORT_VIEW_PROPERTY(minimumTrackTintColor, UIColor); RCT_EXPORT_VIEW_PROPERTY(maximumTrackTintColor, UIColor); -RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onValueChange, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onSlidingComplete, RCTBubblingEventBlock); +RCT_CUSTOM_VIEW_PROPERTY(disabled, BOOL, RCTSlider) +{ + if (json) { + view.enabled = !([RCTConvert BOOL:json]); + } else { + view.enabled = defaultView.enabled; + } +} @end diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index 13d60f113..fee6d1a60 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -26,7 +26,7 @@ { BOOL _tabsChanged; UITabBarController *_tabController; - NSMutableArray *_tabViews; + NSMutableArray *_tabViews; } - (instancetype)initWithFrame:(CGRect)frame @@ -50,14 +50,15 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)dealloc { _tabController.delegate = nil; + [_tabController removeFromParentViewController]; } -- (NSArray *)reactSubviews +- (NSArray *)reactSubviews { return _tabViews; } -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex +- (void)insertReactSubview:(RCTTabBarItem *)view atIndex:(NSInteger)atIndex { if (![view isKindOfClass:[RCTTabBarItem class]]) { RCTLogError(@"subview should be of type RCTTabBarItem"); @@ -67,7 +68,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) _tabsChanged = YES; } -- (void)removeReactSubview:(UIView *)subview +- (void)removeReactSubview:(RCTTabBarItem *)subview { if (_tabViews.count == 0) { RCTLogError(@"should have at least one view to remove a subview"); @@ -80,6 +81,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)layoutSubviews { [super layoutSubviews]; + [self reactAddControllerToClosestParent:_tabController]; _tabController.view.frame = self.bounds; } @@ -91,7 +93,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) if (_tabsChanged) { - NSMutableArray *viewControllers = [NSMutableArray array]; + NSMutableArray *viewControllers = [NSMutableArray array]; for (RCTTabBarItem *tab in [self reactSubviews]) { UIViewController *controller = tab.reactViewController; if (!controller) { @@ -104,7 +106,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) _tabsChanged = NO; } - [[self reactSubviews] enumerateObjectsUsingBlock: + [_tabViews enumerateObjectsUsingBlock: ^(RCTTabBarItem *tab, NSUInteger index, __unused BOOL *stop) { UIViewController *controller = _tabController.viewControllers[index]; controller.tabBarItem = tab.barItem; @@ -147,7 +149,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController { NSUInteger index = [tabBarController.viewControllers indexOfObject:viewController]; - RCTTabBarItem *tab = [self reactSubviews][index]; + RCTTabBarItem *tab = _tabViews[index]; if (tab.onPress) tab.onPress(nil); return NO; } diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 85ff2ecf4..3ba4d88d6 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -16,20 +16,6 @@ #import "RCTUtils.h" #import "UIView+React.h" -static UIView *RCTViewHitTest(UIView *view, CGPoint point, UIEvent *event) -{ - for (UIView *subview in [view.subviews reverseObjectEnumerator]) { - if (!subview.isHidden && subview.isUserInteractionEnabled && subview.alpha > 0) { - CGPoint convertedPoint = [subview convertPoint:point fromView:view]; - UIView *subviewHitTestView = [subview hitTest:convertedPoint withEvent:event]; - if (subviewHitTestView != nil) { - return subviewHitTestView; - } - } - } - return nil; -} - @implementation UIView (RCTViewUnmounting) - (void)react_remountAllSubviews @@ -106,7 +92,7 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) @implementation RCTView { - NSMutableArray *_reactSubviews; + NSMutableArray *> *_reactSubviews; UIColor *_backgroundColor; } @@ -150,18 +136,46 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]); + if(!canReceiveTouchEvents) { + return nil; + } + + // `hitSubview` is the topmost subview which was hit. The hit point can + // be outside the bounds of `view` (e.g., if -clipsToBounds is NO). + UIView *hitSubview = nil; + BOOL isPointInside = [self pointInside:point withEvent:event]; + BOOL needsHitSubview = !(_pointerEvents == RCTPointerEventsNone || _pointerEvents == RCTPointerEventsBoxOnly); + if (needsHitSubview && (![self clipsToBounds] || isPointInside)) { + // The default behaviour of UIKit is that if a view does not contain a point, + // then no subviews will be returned from hit testing, even if they contain + // the hit point. By doing hit testing directly on the subviews, we bypass + // the strict containment policy (i.e., UIKit guarantees that every ancestor + // of the hit view will return YES from -pointInside:withEvent:). See: + // - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html + for (UIView *subview in [self.subviews reverseObjectEnumerator]) { + CGPoint convertedPoint = [subview convertPoint:point fromView:self]; + hitSubview = [subview hitTest:convertedPoint withEvent:event]; + if (hitSubview != nil) { + break; + } + } + } + + UIView *hitView = (isPointInside ? self : nil); + switch (_pointerEvents) { case RCTPointerEventsNone: return nil; case RCTPointerEventsUnspecified: - return RCTViewHitTest(self, point, event) ?: [super hitTest:point withEvent:event]; + return hitSubview ?: hitView; case RCTPointerEventsBoxOnly: - return [super hitTest:point withEvent:event] ? self: nil; + return hitView; case RCTPointerEventsBoxNone: - return RCTViewHitTest(self, point, event); + return hitSubview; default: RCTLogError(@"Invalid pointer-events specified %zd on %@", _pointerEvents, self); - return [super hitTest:point withEvent:event]; + return hitSubview ?: hitView; } } @@ -399,7 +413,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) [subview removeFromSuperview]; } -- (NSArray *)reactSubviews +- (NSArray *> *)reactSubviews { // The _reactSubviews array is only used when we have hidden // offscreen views. If _reactSubviews is nil, we can assume diff --git a/React/Views/RCTViewManager.h b/React/Views/RCTViewManager.h index 35c2a4d4f..5825fa8cd 100644 --- a/React/Views/RCTViewManager.h +++ b/React/Views/RCTViewManager.h @@ -70,7 +70,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v * Note that this method is not inherited when you subclass a view module, and * you should not call [super customBubblingEventTypes] when overriding it. */ -- (NSArray *)customBubblingEventTypes; +- (NSArray *)customBubblingEventTypes; /** * DEPRECATED: declare properties of type RCTDirectEventBlock instead @@ -83,7 +83,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v * Note that this method is not inherited when you subclass a view module, and * you should not call [super customDirectEventTypes] when overriding it. */ -- (NSArray *)customDirectEventTypes; +- (NSArray *)customDirectEventTypes; /** * Called to notify manager that layout has finished, in case any calculated @@ -103,13 +103,13 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v * This handles the simple case, where JS and native property names match. */ #define RCT_EXPORT_VIEW_PROPERTY(name, type) \ -+ (NSArray *)propConfig_##name { return @[@#type]; } ++ (NSArray *)propConfig_##name { return @[@#type]; } /** * This macro maps a named property to an arbitrary key path in the view. */ #define RCT_REMAP_VIEW_PROPERTY(name, keyPath, type) \ -+ (NSArray *)propConfig_##name { return @[@#type, @#keyPath]; } ++ (NSArray *)propConfig_##name { return @[@#type, @#keyPath]; } /** * This macro can be used when you need to provide custom logic for setting @@ -124,6 +124,6 @@ RCT_REMAP_VIEW_PROPERTY(name, __custom__, type) \ * This macro is used to map properties to the shadow view, instead of the view. */ #define RCT_EXPORT_SHADOW_PROPERTY(name, type) \ -+ (NSArray *)propConfigShadow_##name { return @[@#type]; } ++ (NSArray *)propConfigShadow_##name { return @[@#type]; } @end diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index c2cf93676..256f1a0a5 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -69,7 +69,7 @@ RCT_EXPORT_MODULE() return [RCTShadowView new]; } -- (NSArray *)customBubblingEventTypes +- (NSArray *)customBubblingEventTypes { return @[ @@ -80,6 +80,7 @@ RCT_EXPORT_MODULE() @"blur", @"submitEditing", @"endEditing", + @"keyPress", // Touch events @"touchStart", @@ -89,7 +90,7 @@ RCT_EXPORT_MODULE() ]; } -- (NSArray *)customDirectEventTypes +- (NSArray *)customDirectEventTypes { return @[]; } diff --git a/React/Views/RCTWebView.h b/React/Views/RCTWebView.h index fdb192a39..3d514dd47 100644 --- a/React/Views/RCTWebView.h +++ b/React/Views/RCTWebView.h @@ -9,6 +9,8 @@ #import "RCTView.h" +@class RCTWebView; + /** * Special scheme used to pass messages to the injectedJavaScript * code without triggering a page load. Usage: @@ -17,8 +19,18 @@ */ extern NSString *const RCTJSNavigationScheme; +@protocol RCTWebViewDelegate + +- (BOOL)webView:(RCTWebView *)webView +shouldStartLoadForRequest:(NSMutableDictionary *)request + withCallback:(RCTDirectEventBlock)callback; + +@end + @interface RCTWebView : RCTView +@property (nonatomic, weak) id delegate; + @property (nonatomic, strong) NSURL *URL; @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; diff --git a/React/Views/RCTWebView.m b/React/Views/RCTWebView.m index 46d47c895..2c35bc453 100644 --- a/React/Views/RCTWebView.m +++ b/React/Views/RCTWebView.m @@ -25,6 +25,7 @@ NSString *const RCTJSNavigationScheme = @"react-js-navigation"; @property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; @property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; @property (nonatomic, copy) RCTDirectEventBlock onLoadingError; +@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest; @end @@ -119,7 +120,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (NSMutableDictionary *)baseEvent { - NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary: @{ + NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary:@{ @"url": _webView.request.URL.absoluteString ?: @"", @"loading" : @(_webView.loading), @"title": [_webView stringByEvaluatingJavaScriptFromString:@"document.title"], @@ -142,6 +143,22 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { + BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme]; + + // skip this for the JS Navigation handler + if (!isJSNavigation && _onShouldStartLoadWithRequest) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"url": (request.URL).absoluteString, + @"navigationType": @(navigationType) + }]; + if (![self.delegate webView:self + shouldStartLoadForRequest:event + withCallback:_onShouldStartLoadWithRequest]) { + return NO; + } + } + if (_onLoadingStart) { // We have this check to filter out iframe requests and whatnot BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; @@ -156,13 +173,12 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) } // JS Navigation handler - return ![request.URL.scheme isEqualToString:RCTJSNavigationScheme]; + return !isJSNavigation; } - (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error { if (_onLoadingError) { - if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { // NSURLErrorCancelled is reported when a page has a redirect OR if you load // a new URL in the WebView before the previous one came back. We can just @@ -172,7 +188,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) } NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary: @{ + [event addEntriesFromDictionary:@{ @"domain": error.domain, @"code": @(error.code), @"description": error.localizedDescription, @@ -184,11 +200,15 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)webViewDidFinishLoad:(UIWebView *)webView { if (_injectedJavaScript != nil) { - [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript]; - } + NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript]; + NSMutableDictionary *event = [self baseEvent]; + event[@"jsEvaluationValue"] = jsEvaluationValue; + + _onLoadingFinish(event); + } // we only need the final 'finishLoad' call so only fire the event when we're actually done loading. - if (_onLoadingFinish && !webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) { + else if (_onLoadingFinish && !webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) { _onLoadingFinish([self baseEvent]); } } diff --git a/React/Views/RCTWebViewManager.m b/React/Views/RCTWebViewManager.m index 8779a970b..7512000b0 100644 --- a/React/Views/RCTWebViewManager.m +++ b/React/Views/RCTWebViewManager.m @@ -14,13 +14,22 @@ #import "RCTUIManager.h" #import "RCTWebView.h" -@implementation RCTWebViewManager +@interface RCTWebViewManager () + +@end + +@implementation RCTWebViewManager { + NSConditionLock *_shouldStartLoadLock; + BOOL _shouldStartLoad; +} RCT_EXPORT_MODULE() - (UIView *)view { - return [RCTWebView new]; + RCTWebView *webView = [RCTWebView new]; + webView.delegate = self; + return webView; } RCT_REMAP_VIEW_PROPERTY(url, URL, NSURL); @@ -34,6 +43,7 @@ RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL); RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock); - (NSDictionary *)constantsToExport { @@ -86,4 +96,38 @@ RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag) }]; } +#pragma mark - Exported synchronous methods + +- (BOOL)webView:(__unused RCTWebView *)webView +shouldStartLoadForRequest:(NSMutableDictionary *)request + withCallback:(RCTDirectEventBlock)callback +{ + _shouldStartLoadLock = [[NSConditionLock alloc] initWithCondition:arc4random()]; + _shouldStartLoad = YES; + request[@"lockIdentifier"] = @(_shouldStartLoadLock.condition); + callback(request); + + // Block the main thread for a maximum of 250ms until the JS thread returns + if ([_shouldStartLoadLock lockWhenCondition:0 beforeDate:[NSDate dateWithTimeIntervalSinceNow:.25]]) { + BOOL returnValue = _shouldStartLoad; + [_shouldStartLoadLock unlock]; + _shouldStartLoadLock = nil; + return returnValue; + } else { + RCTLogWarn(@"Did not receive response to shouldStartLoad in time, defaulting to YES"); + return YES; + } +} + +RCT_EXPORT_METHOD(startLoadWithResult:(BOOL)result lockIdentifier:(NSInteger)lockIdentifier) +{ + if ([_shouldStartLoadLock tryLockWhenCondition:lockIdentifier]) { + _shouldStartLoad = result; + [_shouldStartLoadLock unlockWithCondition:0]; + } else { + RCTLogWarn(@"startLoadWithResult invoked with invalid lockIdentifier: " + "got %zd, expected %zd", lockIdentifier, _shouldStartLoadLock.condition); + } +} + @end diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h index 1c3a30933..6bfb6d80e 100644 --- a/React/Views/UIView+React.h +++ b/React/Views/UIView+React.h @@ -15,6 +15,9 @@ @interface UIView (React) +- (NSArray *> *)reactSubviews; +- (UIView *)reactSuperview; + /** * Used by the UIIManager to set the view frame. * May be overriden to disable animation, etc. diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m index 1cbbc16da..14c7a26c7 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -51,12 +51,12 @@ [subview removeFromSuperview]; } -- (NSArray *)reactSubviews +- (NSArray *> *)reactSubviews { return self.subviews; } -- (UIView *)reactSuperview +- (UIView *)reactSuperview { return self.superview; } @@ -78,8 +78,8 @@ return; } - self.layer.position = position; - self.layer.bounds = bounds; + self.center = position; + self.bounds = bounds; } - (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index 5c947bdc2..cee11c8ad 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -29,11 +29,17 @@ task downloadBoost(dependsOn: createNativeDepsDirectories, type: Download) { 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 prepareBoost(dependsOn: downloadBoost) { + inputs.files downloadBoost.dest, 'src/main/jni/third-party/boost/Android.mk' + outputs.dir "$thirdPartyNdkDir/boost" + doLast { + 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) { diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/BUCK b/ReactAndroid/src/main/java/com/facebook/csslayout/BUCK new file mode 100644 index 000000000..da7888de9 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/BUCK @@ -0,0 +1,15 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'csslayout', + srcs = glob(['**/*.java']), + deps = [ + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = ['PUBLIC'], +) + +project_config( + src_target = ':csslayout', +) diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java index 4fc69668a..a5519e129 100644 --- a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java @@ -7,7 +7,7 @@ */ // NOTE: this file is auto-copied from https://github.com/facebook/css-layout -// @generated SignedSource<> +// @generated SignedSource<<4b95f0548441afa1e91e957a93fa6f0b>> package com.facebook.csslayout; @@ -224,6 +224,13 @@ public class CSSNode { return FloatUtil.floatsEqual(f1, f2); } + /** + * Get this node's direction, as defined in the style. + */ + public CSSDirection getStyleDirection() { + return style.direction; + } + public void setDirection(CSSDirection direction) { if (style.direction != direction) { style.direction = direction; @@ -231,6 +238,13 @@ public class CSSNode { } } + /** + * Get this node's flex direction, as defined by style. + */ + public CSSFlexDirection getFlexDirection() { + return style.flexDirection; + } + public void setFlexDirection(CSSFlexDirection flexDirection) { if (style.flexDirection != flexDirection) { style.flexDirection = flexDirection; @@ -238,6 +252,13 @@ public class CSSNode { } } + /** + * Get this node's justify content, as defined by style. + */ + public CSSJustify getJustifyContent() { + return style.justifyContent; + } + public void setJustifyContent(CSSJustify justifyContent) { if (style.justifyContent != justifyContent) { style.justifyContent = justifyContent; @@ -245,6 +266,13 @@ public class CSSNode { } } + /** + * Get this node's align items, as defined by style. + */ + public CSSAlign getAlignItems() { + return style.alignItems; + } + public void setAlignItems(CSSAlign alignItems) { if (style.alignItems != alignItems) { style.alignItems = alignItems; @@ -252,6 +280,13 @@ public class CSSNode { } } + /** + * Get this node's align items, as defined by style. + */ + public CSSAlign getAlignSelf() { + return style.alignSelf; + } + public void setAlignSelf(CSSAlign alignSelf) { if (style.alignSelf != alignSelf) { style.alignSelf = alignSelf; @@ -259,6 +294,13 @@ public class CSSNode { } } + /** + * Get this node's position type, as defined by style. + */ + public CSSPositionType getPositionType() { + return style.positionType; + } + public void setPositionType(CSSPositionType positionType) { if (style.positionType != positionType) { style.positionType = positionType; @@ -273,6 +315,13 @@ public class CSSNode { } } + /** + * Get this node's flex, as defined by style. + */ + public float getFlex() { + return style.flex; + } + public void setFlex(float flex) { if (!valuesEqual(style.flex, flex)) { style.flex = flex; @@ -280,24 +329,52 @@ public class CSSNode { } } + /** + * Get this node's margin, as defined by style + default margin. + */ + public Spacing getMargin() { + return style.margin; + } + public void setMargin(int spacingType, float margin) { if (style.margin.set(spacingType, margin)) { dirty(); } } + /** + * Get this node's padding, as defined by style + default padding. + */ + public Spacing getPadding() { + return style.padding; + } + public void setPadding(int spacingType, float padding) { if (style.padding.set(spacingType, padding)) { dirty(); } } + /** + * Get this node's border, as defined by style. + */ + public Spacing getBorder() { + return style.border; + } + public void setBorder(int spacingType, float border) { if (style.border.set(spacingType, border)) { dirty(); } } + /** + * Get this node's position top, as defined by style. + */ + public float getPositionTop() { + return style.position[POSITION_TOP]; + } + public void setPositionTop(float positionTop) { if (!valuesEqual(style.position[POSITION_TOP], positionTop)) { style.position[POSITION_TOP] = positionTop; @@ -305,6 +382,13 @@ public class CSSNode { } } + /** + * Get this node's position bottom, as defined by style. + */ + public float getPositionBottom() { + return style.position[POSITION_BOTTOM]; + } + public void setPositionBottom(float positionBottom) { if (!valuesEqual(style.position[POSITION_BOTTOM], positionBottom)) { style.position[POSITION_BOTTOM] = positionBottom; @@ -312,6 +396,13 @@ public class CSSNode { } } + /** + * Get this node's position left, as defined by style. + */ + public float getPositionLeft() { + return style.position[POSITION_LEFT]; + } + public void setPositionLeft(float positionLeft) { if (!valuesEqual(style.position[POSITION_LEFT], positionLeft)) { style.position[POSITION_LEFT] = positionLeft; @@ -319,6 +410,13 @@ public class CSSNode { } } + /** + * Get this node's position right, as defined by style. + */ + public float getPositionRight() { + return style.position[POSITION_RIGHT]; + } + public void setPositionRight(float positionRight) { if (!valuesEqual(style.position[POSITION_RIGHT], positionRight)) { style.position[POSITION_RIGHT] = positionRight; @@ -326,6 +424,13 @@ public class CSSNode { } } + /** + * Get this node's width, as defined in the style. + */ + public float getStyleWidth() { + return style.dimensions[DIMENSION_WIDTH]; + } + public void setStyleWidth(float width) { if (!valuesEqual(style.dimensions[DIMENSION_WIDTH], width)) { style.dimensions[DIMENSION_WIDTH] = width; @@ -333,6 +438,13 @@ public class CSSNode { } } + /** + * Get this node's height, as defined in the style. + */ + public float getStyleHeight() { + return style.dimensions[DIMENSION_HEIGHT]; + } + public void setStyleHeight(float height) { if (!valuesEqual(style.dimensions[DIMENSION_HEIGHT], height)) { style.dimensions[DIMENSION_HEIGHT] = height; @@ -360,34 +472,6 @@ public class CSSNode { 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.dimensions[DIMENSION_WIDTH]; - } - - /** - * Get this node's height, as defined in the style. - */ - public float getStyleHeight() { - return style.dimensions[DIMENSION_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. */ diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/README b/ReactAndroid/src/main/java/com/facebook/csslayout/README index 17aba774e..4916c2218 100644 --- a/ReactAndroid/src/main/java/com/facebook/csslayout/README +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/README @@ -1,7 +1,7 @@ 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/4b4cd06be2f0cd2aea476e893c60443082826fb8 +HEAD at the time this code was synced: https://github.com/facebook/css-layout/commit/d3b702e1ad0925f8683ce3039be8e493abbf179b There is generated code in: - README (this file) diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook b/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook index 88a6140e2..b7b2a9e90 100644 --- a/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook @@ -1,7 +1,7 @@ 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/4b4cd06be2f0cd2aea476e893c60443082826fb8 +HEAD at the time this code was synced: https://github.com/facebook/css-layout/commit/d3b702e1ad0925f8683ce3039be8e493abbf179b There is generated code in: - README.facebook (this file) diff --git a/ReactAndroid/src/main/java/com/facebook/jni/BUCK b/ReactAndroid/src/main/java/com/facebook/jni/BUCK new file mode 100644 index 000000000..220371dba --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/jni/BUCK @@ -0,0 +1,17 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'jni', + srcs = glob(['**/*.java']), + deps = [ + react_native_dep('java/com/facebook/proguard/annotations:annotations'), + react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':jni', +) diff --git a/ReactAndroid/src/main/java/com/facebook/perftest/BUCK b/ReactAndroid/src/main/java/com/facebook/perftest/BUCK new file mode 100644 index 000000000..655817f6e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/perftest/BUCK @@ -0,0 +1,11 @@ +android_library( + name = 'perftest', + srcs = glob(['*.java']), + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':perftest', +) diff --git a/ReactAndroid/src/main/java/com/facebook/proguard/annotations/BUCK b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/BUCK new file mode 100644 index 000000000..27de0b88a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/BUCK @@ -0,0 +1,14 @@ +android_library( + name = 'annotations', + srcs = glob(['*.java']), + proguard_config = 'proguard_annotations.pro', + deps = [ + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':annotations', +) diff --git a/ReactAndroid/src/main/java/com/facebook/quicklog/BUCK b/ReactAndroid/src/main/java/com/facebook/quicklog/BUCK new file mode 100644 index 000000000..911bbd556 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/quicklog/BUCK @@ -0,0 +1,14 @@ +android_library( + name = 'quicklog', + srcs = glob(['*.java']), + exported_deps = [ + '//java/com/facebook/quicklog/identifiers:identifiers', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':quicklog', +) diff --git a/ReactAndroid/src/main/java/com/facebook/quicklog/identifiers/BUCK b/ReactAndroid/src/main/java/com/facebook/quicklog/identifiers/BUCK new file mode 100644 index 000000000..38d3b1bfa --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/quicklog/identifiers/BUCK @@ -0,0 +1,11 @@ +android_library( + name = 'identifiers', + srcs = glob(['*.java']), + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':identifiers', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/BUCK b/ReactAndroid/src/main/java/com/facebook/react/BUCK new file mode 100644 index 000000000..8102c187c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/BUCK @@ -0,0 +1,27 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'react', + srcs = glob(['*.java']), + deps = [ + '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/devsupport:devsupport'), + react_native_target('java/com/facebook/react/modules/core:core'), + react_native_target('java/com/facebook/react/modules/debug:debug'), + react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), + react_native_target('java/com/facebook/react/modules/toast:toast'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':react', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java index 83e820b59..7032fcced 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java @@ -20,6 +20,7 @@ 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.RCTNativeAppEventEmitter; import com.facebook.react.modules.core.Timing; import com.facebook.react.modules.debug.AnimationsDebugModule; import com.facebook.react.modules.debug.SourceCodeModule; @@ -74,6 +75,7 @@ import com.facebook.react.uimanager.events.RCTEventEmitter; DeviceEventManagerModule.RCTDeviceEventEmitter.class, JSTimersExecution.class, RCTEventEmitter.class, + RCTNativeAppEventEmitter.class, AppRegistry.class, ReactNative.class, DebugComponentOwnershipModule.RCTDebugComponentOwnership.class); diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index 37822b8d6..5ab2f725a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -14,8 +14,10 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; +import android.app.Activity; import android.app.Application; import android.content.Context; +import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.view.View; @@ -41,6 +43,7 @@ import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.bridge.queue.CatalystQueueConfigurationSpec; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.devsupport.DevServerHelper; import com.facebook.react.devsupport.DevSupportManager; import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; @@ -77,7 +80,7 @@ public class ReactInstanceManager { private @Nullable ReactContextInitParams mPendingReactContextInitParams; /* accessed from any thread */ - private final @Nullable String mBundleAssetName; /* name of JS bundle file in assets folder */ + private @Nullable String mJSBundleFile; /* path to JS bundle on file system */ private final @Nullable String mJSMainModuleName; /* path to JS bundle root on packager server */ private final List mPackages; private final DevSupportManager mDevSupportManager; @@ -87,6 +90,7 @@ public class ReactInstanceManager { private final Context mApplicationContext; private @Nullable DefaultHardwareBackBtnHandler mDefaultBackButtonImpl; private String mSourceUrl; + private @Nullable Activity mCurrentActivity; private final ReactInstanceDevCommandsHandler mDevInterface = new ReactInstanceDevCommandsHandler() { @@ -176,7 +180,7 @@ public class ReactInstanceManager { private ReactInstanceManager( Context applicationContext, - @Nullable String bundleAssetName, + @Nullable String jsBundleFile, @Nullable String jsMainModuleName, List packages, boolean useDeveloperSupport, @@ -185,7 +189,7 @@ public class ReactInstanceManager { initializeSoLoaderIfNecessary(applicationContext); mApplicationContext = applicationContext; - mBundleAssetName = bundleAssetName; + mJSBundleFile = jsBundleFile; mJSMainModuleName = jsMainModuleName; mPackages = packages; mUseDeveloperSupport = useDeveloperSupport; @@ -224,30 +228,53 @@ public class ReactInstanceManager { SoLoader.init(applicationContext, /* native exopackage */ false); } + public void setJSBundleFile(String jsBundleFile) { + mJSBundleFile = jsBundleFile; + } + /** * Trigger react context initialization asynchronously in a background async task. This enables * applications to pre-load the application JS, and execute global code before * {@link ReactRootView} is available and measured. + * + * Called from UI thread. */ public void createReactContextInBackground() { - if (mUseDeveloperSupport) { + if (mUseDeveloperSupport && mJSMainModuleName != null) { 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 + } else if (mJSBundleFile == null) { mDevSupportManager.handleReloadJS(); - return; + } else { + mDevSupportManager.isPackagerRunning( + new DevServerHelper.PackagerStatusCallback() { + @Override + public void onPackagerStatusFetched(final boolean packagerIsRunning) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (packagerIsRunning) { + mDevSupportManager.handleReloadJS(); + } else { + recreateReactContextInBackgroundFromBundleFile(); + } + } + }); + } + }); } + return; } - // Use JS file from assets + + recreateReactContextInBackgroundFromBundleFile(); + } + + private void recreateReactContextInBackgroundFromBundleFile() { recreateReactContextInBackground( new JSCJavaScriptExecutor(), - JSBundleLoader.createAssetLoader( - mApplicationContext.getAssets(), - mBundleAssetName)); + JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile)); } /** @@ -293,6 +320,7 @@ public class ReactInstanceManager { mDevSupportManager.setDevSupportEnabled(false); } + mCurrentActivity = null; if (mCurrentReactContext != null) { mCurrentReactContext.onPause(); } @@ -309,7 +337,7 @@ public class ReactInstanceManager { * @param defaultBackButtonImpl a {@link DefaultHardwareBackBtnHandler} from an Activity that owns * this instance of {@link ReactInstanceManager}. */ - public void onResume(DefaultHardwareBackBtnHandler defaultBackButtonImpl) { + public void onResume(Activity activity, DefaultHardwareBackBtnHandler defaultBackButtonImpl) { UiThreadUtil.assertOnUiThread(); mLifecycleState = LifecycleState.RESUMED; @@ -319,8 +347,9 @@ public class ReactInstanceManager { mDevSupportManager.setDevSupportEnabled(true); } + mCurrentActivity = activity; if (mCurrentReactContext != null) { - mCurrentReactContext.onResume(); + mCurrentReactContext.onResume(activity); } } @@ -336,6 +365,12 @@ public class ReactInstanceManager { } } + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mCurrentReactContext != null) { + mCurrentReactContext.onActivityResult(requestCode, resultCode, data); + } + } + public void showDevOptionsDialog() { UiThreadUtil.assertOnUiThread(); mDevSupportManager.showDevOptionsDialog(); @@ -533,6 +568,7 @@ public class ReactInstanceManager { } reactContext.initializeWithInstance(catalystInstance); + catalystInstance.runJSBundle(); return reactContext; } @@ -552,7 +588,7 @@ public class ReactInstanceManager { private void moveReactContextToCurrentLifecycleState(ReactApplicationContext reactContext) { if (mLifecycleState == LifecycleState.RESUMED) { - reactContext.onResume(); + reactContext.onResume(mCurrentActivity); } } @@ -563,7 +599,7 @@ public class ReactInstanceManager { private final List mPackages = new ArrayList<>(); - private @Nullable String mBundleAssetName; + private @Nullable String mJSBundleFile; private @Nullable String mJSMainModuleName; private @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener; private @Nullable Application mApplication; @@ -574,11 +610,21 @@ public class ReactInstanceManager { } /** - * Name of the JS budle file to be loaded from application's raw assets. + * Name of the JS bundle file to be loaded from application's raw assets. + * * Example: {@code "index.android.js"} */ public Builder setBundleAssetName(String bundleAssetName) { - mBundleAssetName = bundleAssetName; + return this.setJSBundleFile(bundleAssetName == null ? null : "assets://" + bundleAssetName); + } + + /** + * Path to the JS bundle file to be loaded from the file system. + * + * Example: {@code "assets://index.android.js" or "/sdcard/main.jsbundle} + */ + public Builder setJSBundleFile(String jsBundleFile) { + mJSBundleFile = jsBundleFile; return this; } @@ -638,21 +684,23 @@ public class ReactInstanceManager { * Before calling {@code build}, the following must be called: *
    *
  • {@link #setApplication} - *
  • {@link #setBundleAssetName} or {@link #setJSMainModuleName} + *
  • {@link #setJSBundleFile} 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"); + mUseDeveloperSupport || mJSBundleFile != null, + "JS Bundle File has to be provided when dev support is disabled"); + Assertions.assertCondition( - mBundleAssetName != null || mJSMainModuleName != null, - "Either BundleAssetName or MainModuleName needs to be provided"); + mJSMainModuleName != null || mJSBundleFile != null, + "Either MainModuleName or JS Bundle File needs to be provided"); + return new ReactInstanceManager( Assertions.assertNotNull( mApplication, "Application property has not been set with this builder"), - mBundleAssetName, + mJSBundleFile, mJSMainModuleName, mPackages, mUseDeveloperSupport, diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index f7aeb89b4..5e21647df 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -145,7 +145,7 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView mChildIsHandlingNativeGesture = false; mTargetTag = TouchTargetHelper.findTargetTagForTouch(ev.getY(), ev.getX(), this); eventDispatcher.dispatchEvent( - new TouchEvent(mTargetTag, SystemClock.uptimeMillis(),TouchEventType.START, ev)); + TouchEvent.obtain(mTargetTag, SystemClock.uptimeMillis(), 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. @@ -161,20 +161,20 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView // End of the gesture. We reset target tag to -1 and expect no further event associated with // this gesture. eventDispatcher.dispatchEvent( - new TouchEvent(mTargetTag, SystemClock.uptimeMillis(), TouchEventType.END, ev)); + TouchEvent.obtain(mTargetTag, SystemClock.uptimeMillis(), TouchEventType.END, ev)); mTargetTag = -1; } else if (action == MotionEvent.ACTION_MOVE) { // Update pointer position for current gesture eventDispatcher.dispatchEvent( - new TouchEvent(mTargetTag, SystemClock.uptimeMillis(), TouchEventType.MOVE, ev)); + TouchEvent.obtain(mTargetTag, SystemClock.uptimeMillis(), 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, SystemClock.uptimeMillis(), TouchEventType.START, ev)); + TouchEvent.obtain(mTargetTag, SystemClock.uptimeMillis(), TouchEventType.START, ev)); } else if (action == MotionEvent.ACTION_POINTER_UP) { // Exactly onw of the pointers goes up eventDispatcher.dispatchEvent( - new TouchEvent(mTargetTag, SystemClock.uptimeMillis(), TouchEventType.END, ev)); + TouchEvent.obtain(mTargetTag, SystemClock.uptimeMillis(), TouchEventType.END, ev)); } else if (action == MotionEvent.ACTION_CANCEL) { dispatchCancelEvent(ev); mTargetTag = -1; @@ -219,7 +219,7 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView !mChildIsHandlingNativeGesture, "Expected to not have already sent a cancel for this gesture"); Assertions.assertNotNull(eventDispatcher).dispatchEvent( - new TouchEvent( + TouchEvent.obtain( mTargetTag, SystemClock.uptimeMillis(), TouchEventType.CANCEL, diff --git a/ReactAndroid/src/main/java/com/facebook/react/animation/BUCK b/ReactAndroid/src/main/java/com/facebook/react/animation/BUCK new file mode 100644 index 000000000..6a3307d02 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animation/BUCK @@ -0,0 +1,18 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'animation', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC' + ], +) + +project_config( + src_target = ':animation', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ActivityEventListener.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ActivityEventListener.java new file mode 100644 index 000000000..3f45b4ce8 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ActivityEventListener.java @@ -0,0 +1,16 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.bridge; + +import android.content.Intent; + +/** + * Listener for receiving activity events. + */ +public interface ActivityEventListener { + + /** + * Called when host (activity/service) receives an {@link Activity#onActivityResult} call. + */ + void onActivityResult(int requestCode, int resultCode, Intent data); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java index 15d498df1..1772ef52a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java @@ -9,6 +9,8 @@ package com.facebook.react.bridge; +import javax.annotation.Nullable; + import android.os.Bundle; public class Arguments { @@ -139,4 +141,52 @@ public class Arguments { } return map; } + + /** + * Convert a {@link WritableMap} to a {@link Bundle}. + * @param readableMap the {@link WritableMap} to convert. + * @return the converted {@link Bundle}. + */ + @Nullable + public static Bundle toBundle(@Nullable ReadableMap readableMap) { + if (readableMap == null) { + return null; + } + + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + if (!iterator.hasNextKey()) { + return null; + } + + Bundle bundle = new Bundle(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableType readableType = readableMap.getType(key); + switch (readableType) { + case Null: + bundle.putString(key, null); + break; + case Boolean: + bundle.putBoolean(key, readableMap.getBoolean(key)); + break; + case Number: + // Can be int or double. + bundle.putDouble(key, readableMap.getDouble(key)); + break; + case String: + bundle.putString(key, readableMap.getString(key)); + break; + case Map: + bundle.putBundle(key, toBundle(readableMap.getMap(key))); + break; + case Array: + // TODO t8873322 + throw new UnsupportedOperationException("Arrays aren't supported yet."); + default: + throw new IllegalArgumentException("Could not convert object with key: " + key + "."); + } + } + + return bundle; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/BUCK b/ReactAndroid/src/main/java/com/facebook/react/bridge/BUCK new file mode 100644 index 000000000..9d8c59acd --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/BUCK @@ -0,0 +1,41 @@ +include_defs('//ReactAndroid/DEFS') + +# We package the JS files from the bundler and local directory into what we +# pretend is an ordinary JAR file. By putting them under the assets/ directory +# within the zip file and relying on Buck to merge its contents into the APK, +# our JS bundles arrive in a place accessible by the AssetManager at runtime. + +python_binary( + name = 'package_js', + main = 'package_js.py', + visibility = [ + 'PUBLIC', + ], +) + +android_library( + name = 'bridge', + srcs = glob(['**/*.java']), + deps = [ + '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', + react_native_target('java/com/facebook/react/common:common'), + react_native_target('jni/react/jni:jni'), + react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), + react_native_dep('java/com/facebook/systrace:systrace'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jackson:core'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/java/okhttp:okhttp-ws'), + ], + exported_deps = [ + react_native_dep('java/com/facebook/jni:jni'), + react_native_dep('java/com/facebook/proguard/annotations:annotations'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':bridge', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java index 5e4760f7c..061611e74 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java @@ -11,6 +11,7 @@ package com.facebook.react.bridge; import com.fasterxml.jackson.core.JsonGenerator; +import com.facebook.infer.annotation.Assertions; import com.facebook.systrace.Systrace; import javax.annotation.Nullable; @@ -46,70 +47,212 @@ import java.util.Map; * with the same name. */ public abstract class BaseJavaModule implements NativeModule { + // taken from Libraries/Utilities/MessageQueue.js + static final public String METHOD_TYPE_REMOTE = "remote"; + static final public String METHOD_TYPE_REMOTE_ASYNC = "remoteAsync"; + + private static abstract class ArgumentExtractor { + public int getJSArgumentsNeeded() { + return 1; + } + + public abstract @Nullable T extractArgument( + CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex); + } + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_BOOLEAN = + new ArgumentExtractor() { + @Override + public Boolean extractArgument( + CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) { + return jsArguments.getBoolean(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_DOUBLE = + new ArgumentExtractor() { + @Override + public Double extractArgument( + CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) { + return jsArguments.getDouble(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_FLOAT = + new ArgumentExtractor() { + @Override + public Float extractArgument( + CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) { + return (float) jsArguments.getDouble(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_INTEGER = + new ArgumentExtractor() { + @Override + public Integer extractArgument( + CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) { + return (int) jsArguments.getDouble(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_STRING = + new ArgumentExtractor() { + @Override + public String extractArgument( + CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) { + return jsArguments.getString(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_ARRAY = + new ArgumentExtractor() { + @Override + public ReadableNativeArray extractArgument( + CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) { + return jsArguments.getArray(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_MAP = + new ArgumentExtractor() { + @Override + public ReadableMap extractArgument( + CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) { + return jsArguments.getMap(atIndex); + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_CALLBACK = + new ArgumentExtractor() { + @Override + public @Nullable Callback extractArgument( + CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) { + if (jsArguments.isNull(atIndex)) { + return null; + } else { + int id = (int) jsArguments.getDouble(atIndex); + return new CallbackImpl(catalystInstance, id); + } + } + }; + + static final private ArgumentExtractor ARGUMENT_EXTRACTOR_PROMISE = + new ArgumentExtractor() { + @Override + public int getJSArgumentsNeeded() { + return 2; + } + + @Override + public Promise extractArgument( + CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) { + Callback resolve = ARGUMENT_EXTRACTOR_CALLBACK + .extractArgument(catalystInstance, jsArguments, atIndex); + Callback reject = ARGUMENT_EXTRACTOR_CALLBACK + .extractArgument(catalystInstance, jsArguments, atIndex + 1); + return new PromiseImpl(resolve, reject); + } + }; + private class JavaMethod implements NativeMethod { - private Method method; + + private Method mMethod; + private final ArgumentExtractor[] mArgumentExtractors; + private final Object[] mArguments; + private String mType = METHOD_TYPE_REMOTE; + private final int mJSArgumentsNeeded; public JavaMethod(Method method) { - this.method = method; + mMethod = method; + Class[] parameterTypes = method.getParameterTypes(); + mArgumentExtractors = buildArgumentExtractors(parameterTypes); + // Since native methods are invoked from a message queue executed on a single thread, it is + // save to allocate only one arguments object per method that can be reused across calls + mArguments = new Object[parameterTypes.length]; + mJSArgumentsNeeded = calculateJSArgumentsNeeded(); + } + + private ArgumentExtractor[] buildArgumentExtractors(Class[] paramTypes) { + ArgumentExtractor[] argumentExtractors = new ArgumentExtractor[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i += argumentExtractors[i].getJSArgumentsNeeded()) { + Class argumentClass = paramTypes[i]; + if (argumentClass == Boolean.class || argumentClass == boolean.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_BOOLEAN; + } else if (argumentClass == Integer.class || argumentClass == int.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_INTEGER; + } else if (argumentClass == Double.class || argumentClass == double.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_DOUBLE; + } else if (argumentClass == Float.class || argumentClass == float.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_FLOAT; + } else if (argumentClass == String.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_STRING; + } else if (argumentClass == Callback.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_CALLBACK; + } else if (argumentClass == Promise.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_PROMISE; + Assertions.assertCondition( + i == paramTypes.length - 1, "Promise must be used as last parameter only"); + mType = METHOD_TYPE_REMOTE_ASYNC; + } else if (argumentClass == ReadableMap.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_MAP; + } else if (argumentClass == ReadableArray.class) { + argumentExtractors[i] = ARGUMENT_EXTRACTOR_ARRAY; + } else { + throw new RuntimeException( + "Got unknown argument class: " + argumentClass.getSimpleName()); + } + } + return argumentExtractors; + } + + private int calculateJSArgumentsNeeded() { + int n = 0; + for (ArgumentExtractor extractor : mArgumentExtractors) { + n += extractor.getJSArgumentsNeeded(); + } + return n; + } + + private String getAffectedRange(int startIndex, int jsArgumentsNeeded) { + return jsArgumentsNeeded > 1 ? + "" + startIndex + "-" + (startIndex + jsArgumentsNeeded - 1) : "" + startIndex; } @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()) { + if (mJSArgumentsNeeded != parameters.size()) { throw new NativeArgumentsParseException( - BaseJavaModule.this.getName() + "." + method.getName() + " got " + parameters.size() + - " arguments, expected " + types.length); + BaseJavaModule.this.getName() + "." + mMethod.getName() + " got " + + parameters.size() + " arguments, expected " + mJSArgumentsNeeded); } - Object[] arguments = new Object[types.length]; - int i = 0; + int i = 0, jsArgumentsConsumed = 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()); - } + for (; i < mArgumentExtractors.length; i++) { + mArguments[i] = mArgumentExtractors[i].extractArgument( + catalystInstance, parameters, jsArgumentsConsumed); + jsArgumentsConsumed += mArgumentExtractors[i].getJSArgumentsNeeded(); } } catch (UnexpectedNativeTypeException e) { throw new NativeArgumentsParseException( e.getMessage() + " (constructing arguments for " + BaseJavaModule.this.getName() + - "." + method.getName() + " at argument index " + i + ")", + "." + mMethod.getName() + " at argument index " + + getAffectedRange(jsArgumentsConsumed, mArgumentExtractors[i].getJSArgumentsNeeded()) + + ")", e); } try { - method.invoke(BaseJavaModule.this, arguments); + mMethod.invoke(BaseJavaModule.this, mArguments); } catch (IllegalArgumentException ie) { throw new RuntimeException( - "Could not invoke " + BaseJavaModule.this.getName() + "." + method.getName(), ie); + "Could not invoke " + BaseJavaModule.this.getName() + "." + mMethod.getName(), ie); } catch (IllegalAccessException iae) { throw new RuntimeException( - "Could not invoke " + BaseJavaModule.this.getName() + "." + method.getName(), iae); + "Could not invoke " + BaseJavaModule.this.getName() + "." + mMethod.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 @@ -117,12 +260,22 @@ public abstract class BaseJavaModule implements NativeModule { throw (RuntimeException) ite.getCause(); } throw new RuntimeException( - "Could not invoke " + BaseJavaModule.this.getName() + "." + method.getName(), ite); + "Could not invoke " + BaseJavaModule.this.getName() + "." + mMethod.getName(), ite); } } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); } } + + /** + * Determines how the method is exported in JavaScript: + * METHOD_TYPE_REMOTE for regular methods + * METHOD_TYPE_REMOTE_ASYNC for methods that return a promise object to the caller. + */ + @Override + public String getType() { + return mType; + } } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java index 75cfb0588..96682eace 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java @@ -41,7 +41,8 @@ import com.fasterxml.jackson.core.JsonGenerator; @DoNotStrip public class CatalystInstance { - private static final int BRIDGE_SETUP_TIMEOUT_MS = 15000; + private static final int BRIDGE_SETUP_TIMEOUT_MS = 30000; + private static final int LOAD_JS_BUNDLE_TIMEOUT_MS = 30000; private static final AtomicInteger sNextInstanceIdForTrace = new AtomicInteger(1); @@ -52,6 +53,9 @@ public class CatalystInstance { private final String mJsPendingCallsTitleForTrace = "pending_js_calls_instance" + sNextInstanceIdForTrace.getAndIncrement(); private volatile boolean mDestroyed = false; + private final TraceListener mTraceListener; + private final JavaScriptModuleRegistry mJSModuleRegistry; + private final JSBundleLoader mJSBundleLoader; // Access from native modules thread private final NativeModuleRegistry mJavaRegistry; @@ -60,8 +64,7 @@ public class CatalystInstance { // Access from JS thread private @Nullable ReactBridge mBridge; - private @Nullable JavaScriptModuleRegistry mJSModuleRegistry; - private @Nullable TraceListener mTraceListener; + private boolean mJSBundleHasLoaded; private CatalystInstance( final CatalystQueueConfigurationSpec catalystQueueConfigurationSpec, @@ -73,19 +76,20 @@ public class CatalystInstance { mCatalystQueueConfiguration = CatalystQueueConfiguration.create( catalystQueueConfigurationSpec, new NativeExceptionHandler()); - mBridgeIdleListeners = new CopyOnWriteArrayList(); + mBridgeIdleListeners = new CopyOnWriteArrayList<>(); mJavaRegistry = registry; + mJSModuleRegistry = new JavaScriptModuleRegistry(CatalystInstance.this, jsModulesConfig); + mJSBundleLoader = jsBundleLoader; mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler; + mTraceListener = new JSProfilerTraceListener(); + Systrace.registerListener(mTraceListener); 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); - + initializeBridge(jsExecutor, jsModulesConfig); initLatch.countDown(); } }); @@ -101,65 +105,52 @@ public class CatalystInstance { private void initializeBridge( JavaScriptExecutor jsExecutor, - NativeModuleRegistry registry, - JavaScriptModulesConfig jsModulesConfig, - JSBundleLoader jsBundleLoader) { + JavaScriptModulesConfig jsModulesConfig) { mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread(); Assertions.assertCondition(mBridge == null, "initializeBridge should be called once"); + mBridge = new ReactBridge( jsExecutor, new NativeModulesReactCallback(), mCatalystQueueConfiguration.getNativeModulesQueueThread()); mBridge.setGlobalVariable( "__fbBatchedBridgeConfig", - buildModulesConfigJSONProperty(registry, jsModulesConfig)); + buildModulesConfigJSONProperty(mJavaRegistry, jsModulesConfig)); + } + + public void runJSBundle() { Systrace.beginSection( Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, - "CatalystInstance_initializeBridge"); + "CatalystInstance_runJSBundle"); + try { - jsBundleLoader.loadScript(mBridge); + final CountDownLatch initLatch = new CountDownLatch(1); + mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( + new Runnable() { + @Override + public void run() { + Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!"); + mJSBundleHasLoaded = true; + + incrementPendingJSCalls(); + + try { + mJSBundleLoader.loadScript(mBridge); + } catch (JSExecutionException e) { + mNativeModuleCallExceptionHandler.handleException(e); + } + + initLatch.countDown(); + } + }); + Assertions.assertCondition( + initLatch.await(LOAD_JS_BUNDLE_TIMEOUT_MS, TimeUnit.MILLISECONDS), + "Timed out loading JS!"); + } catch (InterruptedException e) { + throw new RuntimeException(e); } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); } - - mTraceListener = new TraceListener() { - @Override - public void onTraceStarted() { - mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( - new Runnable() { - @Override - public void run() { - mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread(); - - if (mDestroyed) { - return; - } - Assertions.assertNotNull(mBridge).setGlobalVariable( - "__BridgeProfilingIsProfiling", - "true"); - } - }); - } - - @Override - public void onTraceStopped() { - mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( - new Runnable() { - @Override - public void run() { - mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread(); - - if (mDestroyed) { - return; - } - Assertions.assertNotNull(mBridge).setGlobalVariable( - "__BridgeProfilingIsProfiling", - "false"); - } - }); - } - }; - Systrace.registerListener(mTraceListener); } /* package */ void callFunction( @@ -351,6 +342,7 @@ public class CatalystInstance { private void decrementPendingJSCalls() { int newPendingCalls = mPendingJSCalls.decrementAndGet(); + Assertions.assertCondition(newPendingCalls >= 0); boolean isNowIdle = newPendingCalls == 0; Systrace.traceCounter( Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, @@ -417,6 +409,44 @@ public class CatalystInstance { } } + private class JSProfilerTraceListener implements TraceListener { + @Override + public void onTraceStarted() { + mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( + new Runnable() { + @Override + public void run() { + mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread(); + + if (mDestroyed) { + return; + } + Assertions.assertNotNull(mBridge).setGlobalVariable( + "__BridgeProfilingIsProfiling", + "true"); + } + }); + } + + @Override + public void onTraceStopped() { + mCatalystQueueConfiguration.getJSQueueThread().runOnQueue( + new Runnable() { + @Override + public void run() { + mCatalystQueueConfiguration.getJSQueueThread().assertIsOnThread(); + + if (mDestroyed) { + return; + } + Assertions.assertNotNull(mBridge).setGlobalVariable( + "__BridgeProfilingIsProfiling", + "false"); + } + }); + } + } + public static class Builder { private @Nullable CatalystQueueConfigurationSpec mCatalystQueueConfigurationSpec; diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/InvalidIteratorException.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/InvalidIteratorException.java index eea4c1d07..2e5261b7f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/InvalidIteratorException.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/InvalidIteratorException.java @@ -12,7 +12,7 @@ package com.facebook.react.bridge; import com.facebook.proguard.annotations.DoNotStrip; /** - * Exception thrown by {@link ReadableMapKeySeyIterator#nextKey()} when the iterator tries + * Exception thrown by {@link ReadableMapKeySetIterator#nextKey()} when the iterator tries * to iterate over elements after the end of the key set. */ @DoNotStrip diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java index d703bbc4f..c784cd007 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java @@ -9,7 +9,7 @@ package com.facebook.react.bridge; -import android.content.res.AssetManager; +import android.content.Context; /** * A class that stores JS bundle information and allows {@link CatalystInstance} to load a correct @@ -22,18 +22,22 @@ public abstract class JSBundleLoader { * 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) { + public static JSBundleLoader createFileLoader( + final Context context, + final String fileName) { return new JSBundleLoader() { @Override public void loadScript(ReactBridge bridge) { - bridge.loadScriptFromAssets(assetManager, assetFileName); + if (fileName.startsWith("assets://")) { + bridge.loadScriptFromAssets(context.getAssets(), fileName.replaceFirst("assets://", "")); + } else { + bridge.loadScriptFromFile(fileName, fileName); + } } @Override public String getSourceUrl() { - return "file:///android_asset/" + assetFileName; + return fileName; } }; } @@ -51,7 +55,7 @@ public abstract class JSBundleLoader { return new JSBundleLoader() { @Override public void loadScript(ReactBridge bridge) { - bridge.loadScriptFromNetworkCached(sourceURL, cachedFileLocation); + bridge.loadScriptFromFile(cachedFileLocation, sourceURL); } @Override @@ -70,7 +74,7 @@ public abstract class JSBundleLoader { return new JSBundleLoader() { @Override public void loadScript(ReactBridge bridge) { - bridge.loadScriptFromNetworkCached(sourceURL, null); + bridge.loadScriptFromFile(null, sourceURL); } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSExecutionException.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSExecutionException.java new file mode 100644 index 000000000..96e8f1544 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSExecutionException.java @@ -0,0 +1,17 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.bridge; + +import com.facebook.proguard.annotations.DoNotStrip; + +/** + * Exception thrown when there is an error evaluating JS, e.g. a syntax error. + */ +@DoNotStrip +public class JSExecutionException extends RuntimeException { + + @DoNotStrip + public JSExecutionException(String detailMessage) { + super(detailMessage); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java index 02df61959..480e42182 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.core.JsonGenerator; public interface NativeModule { public static interface NativeMethod { void invoke(CatalystInstance catalystInstance, ReadableNativeArray parameters); + String getType(); } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java index f185118e8..72b3ad666 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java @@ -199,6 +199,7 @@ public class NativeModuleRegistry { MethodRegistration method = module.methods.get(i); jg.writeObjectFieldStart(method.name); jg.writeNumberField("methodID", i); + jg.writeStringField("type", method.method.getType()); jg.writeEndObject(); } jg.writeEndObject(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java new file mode 100644 index 000000000..c19907af8 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.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; + +/** + * Interface that represents a JavaScript Promise which can be passed to the native module as a + * method parameter. + * + * Methods annotated with {@link ReactMethod} that use {@link Promise} as type of the last parameter + * will be marked as "remoteAsync" and will return a promise when invoked from JavaScript. + */ +public interface Promise { + void resolve(Object value); + void reject(String reason); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.java new file mode 100644 index 000000000..688b08102 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/PromiseImpl.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. + */ + +/** + * Implementation of two javascript functions that can be used to resolve or reject a js promise. + */ +package com.facebook.react.bridge; + +import javax.annotation.Nullable; + +public class PromiseImpl implements Promise { + private @Nullable Callback mResolve; + private @Nullable Callback mReject; + + public PromiseImpl(@Nullable Callback resolve, @Nullable Callback reject) { + mResolve = resolve; + mReject = reject; + } + + @Override + public void resolve(Object value) { + if (mResolve != null) { + mResolve.invoke(value); + } + } + + @Override + public void reject(String reason) { + if (mReject != null) { + // The JavaScript side expects a map with at least the error message. + // It is possible to expose all kind of information. It will be available on the JS + // error instance. + // TODO(8850038): add more useful information, e.g. the native stack trace. + WritableNativeMap errorInfo = new WritableNativeMap(); + errorInfo.putString("message", reason); + mReject.invoke(errorInfo); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java index 624de7506..d84c7b844 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java @@ -65,7 +65,7 @@ public class ReactBridge extends Countable { * All native functions are not thread safe and appropriate queues should be used */ public native void loadScriptFromAssets(AssetManager assetManager, String assetName); - public native void loadScriptFromNetworkCached(String sourceURL, @Nullable String tempFileName); + public native void loadScriptFromFile(@Nullable String fileName, @Nullable String sourceURL); 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); diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java index 27363232b..47486d3c2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java @@ -13,13 +13,16 @@ import javax.annotation.Nullable; import java.util.concurrent.CopyOnWriteArraySet; +import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; +import android.content.Intent; +import android.os.Bundle; import android.view.LayoutInflater; +import com.facebook.infer.annotation.Assertions; 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 @@ -29,6 +32,8 @@ public class ReactContext extends ContextWrapper { private final CopyOnWriteArraySet mLifecycleEventListeners = new CopyOnWriteArraySet<>(); + private final CopyOnWriteArraySet mActivityEventListeners = + new CopyOnWriteArraySet<>(); private @Nullable CatalystInstance mCatalystInstance; private @Nullable LayoutInflater mInflater; @@ -36,6 +41,7 @@ public class ReactContext extends ContextWrapper { private @Nullable MessageQueueThread mNativeModulesMessageQueueThread; private @Nullable MessageQueueThread mJSMessageQueueThread; private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; + private @Nullable Activity mCurrentActivity; public ReactContext(Context base) { super(base); @@ -116,11 +122,20 @@ public class ReactContext extends ContextWrapper { mLifecycleEventListeners.remove(listener); } + public void addActivityEventListener(ActivityEventListener listener) { + mActivityEventListeners.add(listener); + } + + public void removeActivityEventListener(ActivityEventListener listener) { + mActivityEventListeners.remove(listener); + } + /** * Should be called by the hosting Fragment in {@link Fragment#onResume} */ - public void onResume() { + public void onResume(@Nullable Activity activity) { UiThreadUtil.assertOnUiThread(); + mCurrentActivity = activity; for (LifecycleEventListener listener : mLifecycleEventListeners) { listener.onHostResume(); } @@ -131,6 +146,7 @@ public class ReactContext extends ContextWrapper { */ public void onPause() { UiThreadUtil.assertOnUiThread(); + mCurrentActivity = null; for (LifecycleEventListener listener : mLifecycleEventListeners) { listener.onHostPause(); } @@ -149,6 +165,15 @@ public class ReactContext extends ContextWrapper { } } + /** + * Should be called by the hosting Fragment in {@link Fragment#onActivityResult} + */ + public void onActivityResult(int requestCode, int resultCode, Intent data) { + for (ActivityEventListener listener : mActivityEventListeners) { + listener.onActivityResult(requestCode, resultCode, data); + } + } + public void assertOnUiQueueThread() { Assertions.assertNotNull(mUiMessageQueueThread).assertIsOnThread(); } @@ -199,4 +224,13 @@ public class ReactContext extends ContextWrapper { throw e; } } + + /** + * Same as {@link Activity#startActivityForResult(Intent, int)}, this just redirects the call to + * the current activity. + */ + public void startActivityForResult(Intent intent, int code, Bundle bundle) { + Assertions.assertNotNull(mCurrentActivity); + mCurrentActivity.startActivityForResult(intent, code, bundle); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java index 5aa5adb43..b9e83f2d9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java @@ -24,5 +24,5 @@ public interface ReadableMap { ReadableArray getArray(String name); ReadableMap getMap(String name); ReadableType getType(String name); - ReadableMapKeySeyIterator keySetIterator(); + ReadableMapKeySetIterator keySetIterator(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMapKeySeyIterator.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMapKeySetIterator.java similarity index 92% rename from ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMapKeySeyIterator.java rename to ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMapKeySetIterator.java index 3218611d3..9710496b6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMapKeySeyIterator.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMapKeySetIterator.java @@ -15,7 +15,7 @@ import com.facebook.proguard.annotations.DoNotStrip; * Interface of a iterator for a {@link NativeMap}'s key set. */ @DoNotStrip -public interface ReadableMapKeySeyIterator { +public interface ReadableMapKeySetIterator { boolean hasNextKey(); String nextKey(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java index ac1eabbb1..07545e703 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java @@ -44,20 +44,20 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap { public native ReadableType getType(String name); @Override - public ReadableMapKeySeyIterator keySetIterator() { - return new ReadableNativeMapKeySeyIterator(this); + public ReadableMapKeySetIterator keySetIterator() { + return new ReadableNativeMapKeySetIterator(this); } /** * Implementation of a {@link ReadableNativeMap} iterator in native memory. */ @DoNotStrip - private static class ReadableNativeMapKeySeyIterator extends Countable - implements ReadableMapKeySeyIterator { + private static class ReadableNativeMapKeySetIterator extends Countable + implements ReadableMapKeySetIterator { private final ReadableNativeMap mReadableNativeMap; - public ReadableNativeMapKeySeyIterator(ReadableNativeMap readableNativeMap) { + public ReadableNativeMapKeySetIterator(ReadableNativeMap readableNativeMap) { mReadableNativeMap = readableNativeMap; initialize(mReadableNativeMap); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/BUCK b/ReactAndroid/src/main/java/com/facebook/react/common/BUCK new file mode 100644 index 000000000..d034ce6ec --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/BUCK @@ -0,0 +1,29 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'common', + srcs = glob(['**/*.java']), + deps = [ + ':build_config', + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC', + ], +) + +android_build_config( + name = 'build_config', + package = 'com.facebook.react', + visibility = [ + 'PUBLIC', + ], + values = [ + 'boolean IS_INTERNAL_BUILD = true', + ], +) + +project_config( + src_target = ':common', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/BUCK b/ReactAndroid/src/main/java/com/facebook/react/devsupport/BUCK new file mode 100644 index 000000000..06bf27bc4 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/BUCK @@ -0,0 +1,31 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'devsupport', + manifest = 'AndroidManifest.xml', + srcs = glob(['**/*.java']), + deps = [ + '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', + react_native_target('res:devsupport'), + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/modules/debug:debug'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/java/okhttp:okhttp'), + react_native_dep('third-party/java/okio:okio'), + ], + visibility = [ + react_native_target('java/com/facebook/react/...'), + '//instrumentation_tests/com/facebook/catalyst/...', + '//java/com/facebook/catalyst/...', + '//java/com/facebook/groups/treehouse/react/...', + '//java/com/facebook/fbreact/...', + '//javatests/com/facebook/catalyst/...', + '//javatests/com/facebook/react/...', + ], +) + +project_config( + src_target = ':devsupport', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java index b62d2d4b5..294e9047b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java @@ -20,6 +20,7 @@ import android.content.Context; import android.os.Build; import android.os.Handler; import android.text.TextUtils; +import android.util.Log; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; @@ -32,6 +33,7 @@ import com.squareup.okhttp.ConnectionPool; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; +import com.squareup.okhttp.ResponseBody; import okio.Okio; import okio.Sink; @@ -44,7 +46,7 @@ import okio.Sink; * - 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 class DevServerHelper { public static final String RELOAD_APP_EXTRA_JS_PROXY = "jsproxy"; private static final String RELOAD_APP_ACTION_SUFFIX = ".RELOAD_APP_ACTION"; @@ -62,6 +64,9 @@ import okio.Sink; private static final String ONCHANGE_ENDPOINT_URL_FORMAT = "http://%s/onchange"; private static final String WEBSOCKET_PROXY_URL_FORMAT = "ws://%s/debugger-proxy"; + private static final String PACKAGER_STATUS_URL_FORMAT = "http://%s/status"; + + private static final String PACKAGER_OK_STATUS = "packager-status:running"; 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; @@ -76,6 +81,10 @@ import okio.Sink; void onServerContentChanged(); } + public interface PackagerStatusCallback { + void onPackagerStatusFetched(boolean packagerIsRunning); + } + private final DevInternalSettings mSettings; private final OkHttpClient mClient; private final Handler mRestartOnChangePollingHandler; @@ -108,7 +117,7 @@ import okio.Sink; * @return the host to use when connecting to the bundle server from the host itself. */ private static String getHostForJSProxy() { - return "localhost"; + return DEVICE_LOCALHOST; } /** @@ -199,6 +208,54 @@ import okio.Sink; }); } + public void isPackagerRunning(final PackagerStatusCallback callback) { + String statusURL = createPacakgerStatusURL(getDebugServerHost()); + Request request = new Request.Builder() + .url(statusURL) + .build(); + + mClient.newCall(request).enqueue( + new Callback() { + @Override + public void onFailure(Request request, IOException e) { + Log.e(ReactConstants.TAG, "IOException requesting status from packager", e); + callback.onPackagerStatusFetched(false); + } + + @Override + public void onResponse(Response response) throws IOException { + if (!response.isSuccessful()) { + Log.e( + ReactConstants.TAG, + "Got non-success http code from packager when requesting status: " + + response.code()); + callback.onPackagerStatusFetched(false); + return; + } + ResponseBody body = response.body(); + if (body == null) { + Log.e( + ReactConstants.TAG, + "Got null body response from packager when requesting status"); + callback.onPackagerStatusFetched(false); + return; + } + if (!PACKAGER_OK_STATUS.equals(body.string())) { + Log.e( + ReactConstants.TAG, + "Got unexpected response from packager when requesting status: " + body.string()); + callback.onPackagerStatusFetched(false); + return; + } + callback.onPackagerStatusFetched(true); + } + }); + } + + private String createPacakgerStatusURL(String host) { + return String.format(PACKAGER_STATUS_URL_FORMAT, host); + } + public void stopPollingOnChangeEndpoint() { mOnChangePollingEnabled = false; mRestartOnChangePollingHandler.removeCallbacksAndMessages(null); diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java index 1fb477b13..11b5ffefe 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java @@ -367,10 +367,18 @@ public class DevSupportManager implements NativeModuleCallExceptionHandler { } public String getSourceMapUrl() { + if (mJSAppBundleName == null) { + return ""; + } + return mDevServerHelper.getSourceMapUrl(Assertions.assertNotNull(mJSAppBundleName)); } public String getSourceUrl() { + if (mJSAppBundleName == null) { + return ""; + } + return mDevServerHelper.getSourceUrl(Assertions.assertNotNull(mJSAppBundleName)); } @@ -467,6 +475,8 @@ public class DevSupportManager implements NativeModuleCallExceptionHandler { } public void handleReloadJS() { + UiThreadUtil.assertOnUiThread(); + // dismiss redbox if exists if (mRedBoxDialog != null) { mRedBoxDialog.dismiss(); @@ -488,6 +498,10 @@ public class DevSupportManager implements NativeModuleCallExceptionHandler { } } + public void isPackagerRunning(DevServerHelper.PackagerStatusCallback callback) { + mDevServerHelper.isPackagerRunning(callback); + } + 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 diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/common/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/common/BUCK new file mode 100644 index 000000000..d099b3c2e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/common/BUCK @@ -0,0 +1,20 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'common', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', + '//third-party/java/infer-annotations:infer-annotations', + '//third-party/java/jsr-305:jsr-305', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':common', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/core/BUCK new file mode 100644 index 000000000..5507ede15 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/BUCK @@ -0,0 +1,22 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'core', + srcs = glob(['**/*.java']), + deps = [ + '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/devsupport:devsupport'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':core', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/RCTNativeAppEventEmitter.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/RCTNativeAppEventEmitter.java new file mode 100644 index 000000000..2f678a97b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/RCTNativeAppEventEmitter.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.modules.core; + +import javax.annotation.Nullable; + +import com.facebook.react.bridge.JavaScriptModule; + +/** + * Module that handles global application events. + */ +public interface RCTNativeAppEventEmitter extends JavaScriptModule { + void emit(String eventName, @Nullable Object data); +} + diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/BUCK new file mode 100644 index 000000000..6f5d6b9b1 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/BUCK @@ -0,0 +1,21 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'debug', + srcs = glob(['**/*.java']), + deps = [ + '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':debug', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/BUCK new file mode 100644 index 000000000..18e1e514c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/BUCK @@ -0,0 +1,28 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'fresco', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/modules/common:common'), + react_native_target('java/com/facebook/react/modules/network:network'), + react_native_dep('java/com/facebook/systrace:systrace'), + '//libraries/fbcore/src/main/java/com/facebook/common/internal:internal', + '//libraries/fbcore/src/main/java/com/facebook/common/soloader:soloader', + '//libraries/fresco/drawee-backends/drawee-pipeline/src/main/java/com/facebook/drawee/backends/pipeline:pipeline', + '//libraries/fresco/imagepipeline-backends/imagepipeline-okhttp/src/main/java/com/facebook/imagepipeline/backends/okhttp:okhttp', + '//libraries/imagepipeline/src/main/java/com/facebook/cache/common:common', + '//libraries/imagepipeline/src/main/java/com/facebook/cache/disk:disk', + '//libraries/imagepipeline/src/main/java/com/facebook/imagepipeline/core:core', + '//libraries/soloader/java/com/facebook/soloader:soloader', + '//third-party/java/okhttp:okhttp', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':fresco', +) 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 index 9f278ab57..14b95c0ae 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java @@ -12,8 +12,10 @@ package com.facebook.react.modules.fresco; import java.util.HashSet; import android.content.Context; +import android.support.annotation.Nullable; import com.facebook.cache.common.CacheKey; +import com.facebook.cache.disk.DiskCacheConfig; import com.facebook.common.internal.AndroidPredicates; import com.facebook.common.soloader.SoLoaderShim; import com.facebook.drawee.backends.pipeline.Fresco; @@ -37,10 +39,26 @@ import com.squareup.okhttp.OkHttpClient; public class FrescoModule extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { + @Nullable private RequestListener mRequestListener; + @Nullable private DiskCacheConfig mDiskCacheConfig; + public FrescoModule(ReactApplicationContext reactContext) { super(reactContext); } + public FrescoModule(ReactApplicationContext reactContext, RequestListener listener) { + super(reactContext); + mRequestListener = listener; + } + + public FrescoModule( + ReactApplicationContext reactContext, + RequestListener listener, + DiskCacheConfig diskCacheConfig) { + super(reactContext); + mRequestListener = listener; + mDiskCacheConfig = diskCacheConfig; + } @Override public void initialize() { @@ -57,14 +75,24 @@ public class FrescoModule extends ReactContextBaseJavaModule implements HashSet requestListeners = new HashSet<>(); requestListeners.add(new SystraceRequestListener()); + if (mRequestListener != null) { + requestListeners.add(mRequestListener); + } Context context = this.getReactApplicationContext().getApplicationContext(); OkHttpClient okHttpClient = OkHttpClientProvider.getOkHttpClient(); - ImagePipelineConfig config = OkHttpImagePipelineConfigFactory - .newBuilder(context, okHttpClient) + ImagePipelineConfig.Builder builder = + OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient); + + builder .setDownsampleEnabled(false) - .setRequestListeners(requestListeners) - .build(); + .setRequestListeners(requestListeners); + + if (mDiskCacheConfig != null) { + builder.setMainDiskCacheConfig(mDiskCacheConfig); + } + + ImagePipelineConfig config = builder.build(); Fresco.initialize(context, config); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/network/BUCK new file mode 100644 index 000000000..2de89f0d3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/BUCK @@ -0,0 +1,24 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'network', + srcs = glob(['**/*.java']), + deps = [ + '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/modules/core:core'), + react_native_target('java/com/facebook/react/common:common'), + '//third-party/java/android/support/v4:lib-support-v4', + '//third-party/java/infer-annotations:infer-annotations', + '//third-party/java/jsr-305:jsr-305', + '//third-party/java/okhttp:okhttp', + '//third-party/java/okio:okio', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':network', +) 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 index b957173cb..3ebf4cd06 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java @@ -14,6 +14,7 @@ import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.GuardedAsyncTask; import com.facebook.react.bridge.ReactApplicationContext; @@ -21,7 +22,7 @@ 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.facebook.react.bridge.WritableMap; import com.squareup.okhttp.Headers; import com.squareup.okhttp.MediaType; @@ -73,6 +74,10 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { this(reactContext, defaultUserAgent, OkHttpClientProvider.getOkHttpClient()); } + public NetworkingModule(ReactApplicationContext reactContext, OkHttpClient client) { + this(reactContext, null, client); + } + @Override public String getName() { return "RCTNetworking"; @@ -180,7 +185,6 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { if (mShuttingDown) { return; } - // TODO(5472580) handle headers properly String responseBody; try { responseBody = response.body().string(); @@ -189,7 +193,22 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { callback.invoke(0, null, e.getMessage()); return; } - callback.invoke(response.code(), null, responseBody); + + WritableMap responseHeaders = Arguments.createMap(); + Headers headers = response.headers(); + for (int i = 0; i < headers.size(); i++) { + String headerName = headers.name(i); + // multiple values for the same header + if (responseHeaders.hasKey(headerName)) { + responseHeaders.putString( + headerName, + responseHeaders.getString(headerName) + ", " + headers.value(i)); + } else { + responseHeaders.putString(headerName, headers.value(i)); + } + } + + callback.invoke(response.code(), responseHeaders, responseBody); } }); } 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 index abd6ed274..35b615066 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java @@ -44,7 +44,7 @@ public final class AsyncStorageModule public AsyncStorageModule(ReactApplicationContext reactContext) { super(reactContext); - mReactDatabaseSupplier = new ReactDatabaseSupplier(reactContext); + mReactDatabaseSupplier = ReactDatabaseSupplier.getInstance(reactContext); } @Override @@ -68,23 +68,7 @@ public final class AsyncStorageModule // 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) { - FLog.d(ReactConstants.TAG, "Cleaned AsyncLocalStorage."); - return; - } - // Clearing the database has failed, delete it instead. - if (mReactDatabaseSupplier.deleteDatabase()) { - FLog.d(ReactConstants.TAG, "Deleted Local Database AsyncLocalStorage."); - return; - } - // Everything failed, crash the app - throw new RuntimeException("Clearing and deleting database failed: " + args[0]); - } - }); + mReactDatabaseSupplier.clearAndCloseDatabase(); } /** @@ -353,7 +337,7 @@ public final class AsyncStorageModule return; } try { - mReactDatabaseSupplier.get().delete(TABLE_CATALYST, null, null); + mReactDatabaseSupplier.clear(); callback.invoke(); } catch (Exception e) { FLog.w(ReactConstants.TAG, e.getMessage(), e); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/BUCK new file mode 100644 index 000000000..723db50f7 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/BUCK @@ -0,0 +1,21 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'storage', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/modules/common:common'), + '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', + '//third-party/java/infer-annotations:infer-annotations', + '//third-party/java/jsr-305:jsr-305', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':storage', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java index 2f127c658..ef27b332f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/ReactDatabaseSupplier.java @@ -16,7 +16,13 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; -// VisibleForTesting +import com.facebook.common.logging.FLog; +import com.facebook.react.common.ReactConstants; + +/** + * Database supplier of the database used by react native. This creates, opens and deletes the + * database as necessary. + */ public class ReactDatabaseSupplier extends SQLiteOpenHelper { // VisibleForTesting @@ -38,12 +44,20 @@ public class ReactDatabaseSupplier extends SQLiteOpenHelper { private Context mContext; private @Nullable SQLiteDatabase mDb; + private static @Nullable ReactDatabaseSupplier mReactDatabaseSupplierInstance; - public ReactDatabaseSupplier(Context context) { + private ReactDatabaseSupplier(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); mContext = context; } + public static ReactDatabaseSupplier getInstance(Context context) { + if (mReactDatabaseSupplierInstance == null) { + mReactDatabaseSupplierInstance = new ReactDatabaseSupplier(context); + } + return mReactDatabaseSupplierInstance; + } + @Override public void onCreate(SQLiteDatabase db) { db.execSQL(VERSION_TABLE_CREATE); @@ -102,11 +116,40 @@ public class ReactDatabaseSupplier extends SQLiteOpenHelper { return mDb; } - /* package */ synchronized boolean deleteDatabase() { + public synchronized void clearAndCloseDatabase() throws RuntimeException { + try { + clear(); + closeDatabase(); + FLog.d(ReactConstants.TAG, "Cleaned " + DATABASE_NAME); + } catch (Exception e) { + // Clearing the database has failed, delete it instead. + if (deleteDatabase()) { + FLog.d(ReactConstants.TAG, "Deleted Local Database " + DATABASE_NAME); + return; + } + // Everything failed, throw + throw new RuntimeException("Clearing and deleting database " + DATABASE_NAME + " failed"); + } + } + + /* package */ synchronized void clear() { + get().delete(TABLE_CATALYST, null, null); + } + + private synchronized boolean deleteDatabase() { + closeDatabase(); + return mContext.deleteDatabase(DATABASE_NAME); + } + + private synchronized void closeDatabase() { if (mDb != null && mDb.isOpen()) { mDb.close(); mDb = null; } - return mContext.deleteDatabase(DATABASE_NAME); + } + + // For testing purposes only! + public static void deleteInstance() { + mReactDatabaseSupplierInstance = null; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/BUCK new file mode 100644 index 000000000..deac058df --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/BUCK @@ -0,0 +1,19 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'systeminfo', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + '//third-party/java/infer-annotations:infer-annotations', + '//third-party/java/jsr-305:jsr-305', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':systeminfo', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/toast/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/toast/BUCK new file mode 100644 index 000000000..497ef5bd0 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/toast/BUCK @@ -0,0 +1,19 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'toast', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + '//third-party/java/infer-annotations:infer-annotations', + '//third-party/java/jsr-305:jsr-305', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':toast', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/BUCK new file mode 100644 index 000000000..92ab5d859 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/BUCK @@ -0,0 +1,22 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'websocket', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/modules/core:core'), + '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', + '//third-party/java/infer-annotations:infer-annotations', + '//third-party/java/jsr-305:jsr-305', + '//third-party/java/okhttp:okhttp-ws', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':websocket', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java index 0e5ce7e9b..dfa41638c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java @@ -131,8 +131,13 @@ public class WebSocketModule extends ReactContextBaseJavaModule { public void close(int code, String reason, int id) { WebSocket client = mWebSocketConnections.get(id); if (client == null) { - // This is a programmer error - throw new RuntimeException("Cannot close WebSocket. Unknown WebSocket id " + id); + // WebSocket is already closed + // Don't do anything, mirror the behaviour on web + FLog.w( + ReactConstants.TAG, + "Cannot close WebSocket. Unknown WebSocket id " + id); + + return; } try { client.close(code, reason); diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK new file mode 100644 index 000000000..7043f36df --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -0,0 +1,43 @@ +include_defs('//instrumentation_tests/DEFS') +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'shell', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('res:shell'), + react_native_target('java/com/facebook/react:react'), + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/devsupport:devsupport'), + react_native_target('java/com/facebook/react/views/drawer:drawer'), + react_native_target('java/com/facebook/react/views/image:image'), + react_native_target('java/com/facebook/react/views/progressbar:progressbar'), + react_native_target('java/com/facebook/react/views/scroll:scroll'), + react_native_target('java/com/facebook/react/views/switchview:switchview'), + react_native_target('java/com/facebook/react/views/text:text'), + react_native_target('java/com/facebook/react/views/textinput:textinput'), + react_native_target('java/com/facebook/react/views/toolbar:toolbar'), + react_native_target('java/com/facebook/react/views/view:view'), + react_native_target('java/com/facebook/react/views/viewpager:viewpager'), + react_native_target('java/com/facebook/react/modules/core:core'), + react_native_target('java/com/facebook/react/modules/debug:debug'), + react_native_target('java/com/facebook/react/modules/fresco:fresco'), + react_native_target('java/com/facebook/react/modules/network:network'), + react_native_target('java/com/facebook/react/modules/storage:storage'), + react_native_target('java/com/facebook/react/modules/toast:toast'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_target('java/com/facebook/react/modules/websocket:websocket'), + react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), + react_native_dep('third-party/java/android/support/v4:lib-support-v4'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':shell', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/touch/BUCK b/ReactAndroid/src/main/java/com/facebook/react/touch/BUCK new file mode 100644 index 000000000..44cea8fdc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/touch/BUCK @@ -0,0 +1,17 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'touch', + srcs = glob(['**/*.java']), + deps = [ + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC' + ], +) + +project_config( + src_target = ':touch', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BUCK b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BUCK new file mode 100644 index 000000000..128e87f67 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BUCK @@ -0,0 +1,25 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'uimanager', + srcs = glob(['**/*.java']), + deps = [ + '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', + react_native_target('java/com/facebook/react/animation:animation'), + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/csslayout:csslayout'), + react_native_target('java/com/facebook/react/touch:touch'), + react_native_dep('java/com/facebook/systrace:systrace'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/java/android/support/v4:lib-support-v4'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':uimanager', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index fbbae4c1b..3107223d3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -115,13 +115,13 @@ public abstract class BaseViewManager 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(ViewProps.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(ViewProps.BACKGROUND_COLOR)) { - final int backgroundColor = props.getInt(ViewProps.BACKGROUND_COLOR, Color.TRANSPARENT); - view.setBackgroundColor(backgroundColor); - } - 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/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index a41b7f726..9114a8cf6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -310,19 +310,7 @@ import com.facebook.react.touch.JSResponderHandler; 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); + viewManager.removeViewAt(viewToManage, indicesToRemove[i]); lastIndexToRemove = indexToRemove; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java index 03c53ca8b..d8ec7ae92 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java @@ -14,7 +14,7 @@ import javax.annotation.Nullable; import android.util.SparseBooleanArray; import com.facebook.infer.annotation.Assertions; -import com.facebook.react.bridge.ReadableMapKeySeyIterator; +import com.facebook.react.bridge.ReadableMapKeySetIterator; /** * Class responsible for optimizing the native view hierarchy while still respecting the final UI @@ -417,7 +417,7 @@ public class NativeViewHierarchyOptimizer { return false; } - ReadableMapKeySeyIterator keyIterator = props.mBackingMap.keySetIterator(); + ReadableMapKeySetIterator keyIterator = props.mBackingMap.keySetIterator(); while (keyIterator.hasNextKey()) { if (!ViewProps.isLayoutOnly(keyIterator.nextKey())) { return false; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/OnLayoutEvent.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/OnLayoutEvent.java index f553858bd..393ccdf8d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/OnLayoutEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/OnLayoutEvent.java @@ -9,6 +9,9 @@ package com.facebook.react.uimanager; +import android.os.SystemClock; +import android.support.v4.util.Pools; + import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; import com.facebook.react.uimanager.events.Event; @@ -19,10 +22,30 @@ import com.facebook.react.uimanager.events.RCTEventEmitter; */ /* package */ class OnLayoutEvent extends Event { - private final int mX, mY, mWidth, mHeight; + private static final Pools.SynchronizedPool EVENTS_POOL = + new Pools.SynchronizedPool<>(20); - protected OnLayoutEvent(int viewTag, int x, int y, int width, int height) { - super(viewTag, 0); + private int mX, mY, mWidth, mHeight; + + public static OnLayoutEvent obtain(int viewTag, int x, int y, int width, int height) { + OnLayoutEvent event = EVENTS_POOL.acquire(); + if (event == null) { + event = new OnLayoutEvent(); + } + event.init(viewTag, x, y, width, height); + return event; + } + + @Override + public void onDispose() { + EVENTS_POOL.release(this); + } + + private OnLayoutEvent() { + } + + protected void init(int viewTag, int x, int y, int width, int height) { + super.init(viewTag, SystemClock.uptimeMillis()); mX = x; mY = y; mWidth = width; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java index 5b23ceda7..106356c89 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java @@ -26,15 +26,21 @@ import com.facebook.react.common.ReactConstants; public class ReactChoreographer { public static enum CallbackType { + + /** + * For use by perf markers that need to happen immediately after draw + */ + PERF_MARKERS(0), + /** * For use by {@link com.facebook.react.uimanager.UIManagerModule} */ - DISPATCH_UI(0), + DISPATCH_UI(1), /** * Events that make JS do things. */ - TIMERS_EVENTS(1), + TIMERS_EVENTS(2), ; private final int mOrder; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java index 78a1d57af..9443d98ff 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java @@ -17,7 +17,7 @@ import java.util.Map; import com.facebook.csslayout.CSSNode; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableMapKeySeyIterator; +import com.facebook.react.bridge.ReadableMapKeySetIterator; /** * Base node class for representing virtual tree of React nodes. Shadow nodes are used primarily @@ -166,7 +166,7 @@ public class ReactShadowNode extends CSSNode { Map propSetters = ViewManagersPropertyCache.getNativePropSettersForShadowNodeClass(getClass()); ReadableMap propMap = props.mBackingMap; - ReadableMapKeySeyIterator iterator = propMap.keySetIterator(); + ReadableMapKeySetIterator iterator = propMap.keySetIterator(); while (iterator.hasNextKey()) { String key = iterator.nextKey(); ViewManagersPropertyCache.PropSetter setter = propSetters.get(key); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index dd9b937e1..896e13c01 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -806,7 +806,7 @@ public class UIManagerModule extends ReactContextBaseJavaModule implements // notify JS about layout event if requested if (cssNode.shouldNotifyOnLayout()) { mEventDispatcher.dispatchEvent( - new OnLayoutEvent( + OnLayoutEvent.obtain( tag, cssNode.getScreenX(), cssNode.getScreenY(), diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIProp.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIProp.java deleted file mode 100644 index c209811a8..000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIProp.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of 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"; - * } - * - * TODO(krzysztof): Kill this class once @ReactProp refactoring is done - */ -@Target(ElementType.FIELD) -@Retention(RUNTIME) -public @interface UIProp { - Type value(); - - public static enum Type { - BOOLEAN("boolean"), - NUMBER("number"), - STRING("String"), - MAP("Map"), - ARRAY("Array"), - COLOR("Color"); - - 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/ViewGroupManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java index 669d7b0b4..646f86772 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java @@ -44,8 +44,8 @@ public abstract class ViewGroupManager return parent.getChildAt(index); } - public void removeView(T parent, View child) { - parent.removeView(child); + public void removeViewAt(T parent, int index) { + parent.removeViewAt(index); } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java index 726c5f765..f9b5e1293 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java @@ -11,15 +11,13 @@ 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.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableMapKeySeyIterator; +import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.touch.CatalystInterceptingViewGroup; import com.facebook.react.touch.JSResponderHandler; @@ -30,27 +28,18 @@ import com.facebook.react.touch.JSResponderHandler; */ public abstract class ViewManager { - private static final Map> CLASS_PROP_CACHE = new HashMap<>(); - public final void updateProperties(T viewToUpdate, CatalystStylesDiffMap props) { Map propSetters = ViewManagersPropertyCache.getNativePropSettersForViewManagerClass(getClass()); ReadableMap propMap = props.mBackingMap; - ReadableMapKeySeyIterator iterator = propMap.keySetIterator(); - // TODO(krzysztof): Remove missingSetters code once all views are migrated to @ReactProp - boolean missingSetters = false; + ReadableMapKeySetIterator iterator = propMap.keySetIterator(); while (iterator.hasNextKey()) { String key = iterator.nextKey(); ViewManagersPropertyCache.PropSetter setter = propSetters.get(key); if (setter != null) { setter.updateViewProp(this, viewToUpdate, props); - } else { - missingSetters = true; } } - if (missingSetters) { - updateView(viewToUpdate, props); - } onAfterUpdateTransaction(viewToUpdate); } @@ -114,18 +103,6 @@ public abstract class ViewManager { 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 - * - * TODO(krzysztof) This method should be replaced by updateShadowNode and removed completely after - * all view managers adapt @ReactProp - */ - @Deprecated - protected void updateView(T root, CatalystStylesDiffMap props) { - } - /** * Callback that will be triggered after all properties are updated in current update transaction * (all @ReactProp handlers for properties updated in current transaction have been called). If @@ -227,42 +204,6 @@ public abstract class ViewManager { } public Map getNativeProps() { - // TODO(krzysztof): This method will just delegate to ViewManagersPropertyRegistry once - // refactoring is finished - Class cls = getClass(); - Map nativeProps = - ViewManagersPropertyCache.getNativePropsForView(cls, getShadowNodeClass()); - while (cls.getSuperclass() != null) { - Map props = getNativePropsForClass(cls); - for (Map.Entry entry : props.entrySet()) { - nativeProps.put(entry.getKey(), entry.getValue().toString()); - } - cls = cls.getSuperclass(); - } - return nativeProps; - } - - private Map getNativePropsForClass(Class cls) { - // TODO(krzysztof): Blow up this method once refactoring is finished - 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; + return ViewManagersPropertyCache.getNativePropsForView(getClass(), getShadowNodeClass()); } } 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 index 505fea794..58dd958a6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java @@ -11,15 +11,31 @@ package com.facebook.react.uimanager.events; /** * A UI event that can be dispatched to JS. + * + * For dispatching events {@link EventDispatcher#dispatchEvent} should be used. Once event object + * is passed to the EventDispatched it should no longer be used as EventDispatcher may decide + * to recycle that object (by calling {@link #dispose}). */ public abstract class Event { - private final int mViewTag; - private final long mTimestampMs; + private boolean mInitialized; + private int mViewTag; + private long mTimestampMs; + + protected Event() { + } protected Event(int viewTag, long timestampMs) { + init(viewTag, timestampMs); + } + + /** + * This method needs to be called before event is sent to event dispatcher. + */ + protected void init(int viewTag, long timestampMs) { mViewTag = viewTag; mTimestampMs = timestampMs; + mInitialized = true; } /** @@ -68,7 +84,16 @@ public abstract class Event { * 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() { + public void onDispose() { + } + + /*package*/ boolean isInitialized() { + return mInitialized; + } + + /*package*/ final void dispose() { + mInitialized = false; + onDispose(); } /** 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 index aa3315c16..35c0a2897 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java @@ -110,6 +110,7 @@ public class EventDispatcher implements LifecycleEventListener { * Sends the given Event to JS, coalescing eligible events if JS is backed up. */ public void dispatchEvent(Event event) { + Assertions.assertCondition(event.isInitialized(), "Dispatched event hasn't been initialized"); synchronized (mEventsStagingLock) { mEventStaging.add(event); } 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 index 26569f501..407b8c691 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java @@ -9,8 +9,13 @@ package com.facebook.react.uimanager.events; +import javax.annotation.Nullable; + +import android.support.v4.util.Pools; import android.view.MotionEvent; +import com.facebook.infer.annotation.Assertions; + /** * An event representing the start, end or movement of a touch. Corresponds to a single * {@link android.view.MotionEvent}. @@ -21,47 +26,76 @@ import android.view.MotionEvent; */ public class TouchEvent extends Event { - private final MotionEvent mMotionEvent; - private final TouchEventType mTouchEventType; - private final short mCoalescingKey; + private static final int TOUCH_EVENTS_POOL_SIZE = 3; - public TouchEvent( + private static final Pools.SynchronizedPool EVENTS_POOL = + new Pools.SynchronizedPool<>(TOUCH_EVENTS_POOL_SIZE); + + public static TouchEvent obtain( int viewTag, long timestampMs, TouchEventType touchEventType, MotionEvent motionEventToCopy) { - super(viewTag, timestampMs); - mTouchEventType = touchEventType; - mMotionEvent = MotionEvent.obtain(motionEventToCopy); + TouchEvent event = EVENTS_POOL.acquire(); + if (event == null) { + event = new TouchEvent(); + } + event.init(viewTag, timestampMs, touchEventType, motionEventToCopy); + return event; + } + + private @Nullable MotionEvent mMotionEvent; + private @Nullable TouchEventType mTouchEventType; + private short mCoalescingKey; + + private TouchEvent() { + } + + private void init( + int viewTag, + long timestampMs, + TouchEventType touchEventType, + MotionEvent motionEventToCopy) { + super.init(viewTag, timestampMs); short coalescingKey = 0; - int action = (mMotionEvent.getAction() & MotionEvent.ACTION_MASK); + int action = (motionEventToCopy.getAction() & MotionEvent.ACTION_MASK); switch (action) { case MotionEvent.ACTION_DOWN: - TouchEventCoalescingKeyHelper.addCoalescingKey(mMotionEvent.getDownTime()); + TouchEventCoalescingKeyHelper.addCoalescingKey(motionEventToCopy.getDownTime()); break; case MotionEvent.ACTION_UP: - TouchEventCoalescingKeyHelper.removeCoalescingKey(mMotionEvent.getDownTime()); + TouchEventCoalescingKeyHelper.removeCoalescingKey(motionEventToCopy.getDownTime()); break; case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_POINTER_UP: - TouchEventCoalescingKeyHelper.incrementCoalescingKey(mMotionEvent.getDownTime()); + TouchEventCoalescingKeyHelper.incrementCoalescingKey(motionEventToCopy.getDownTime()); break; case MotionEvent.ACTION_MOVE: - coalescingKey = TouchEventCoalescingKeyHelper.getCoalescingKey(mMotionEvent.getDownTime()); + coalescingKey = + TouchEventCoalescingKeyHelper.getCoalescingKey(motionEventToCopy.getDownTime()); break; case MotionEvent.ACTION_CANCEL: - TouchEventCoalescingKeyHelper.removeCoalescingKey(mMotionEvent.getDownTime()); + TouchEventCoalescingKeyHelper.removeCoalescingKey(motionEventToCopy.getDownTime()); break; default: throw new RuntimeException("Unhandled MotionEvent action: " + action); } + mTouchEventType = touchEventType; + mMotionEvent = MotionEvent.obtain(motionEventToCopy); mCoalescingKey = coalescingKey; } + @Override + public void onDispose() { + Assertions.assertNotNull(mMotionEvent).recycle(); + mMotionEvent = null; + EVENTS_POOL.release(this); + } + @Override public String getEventName() { - return mTouchEventType.getJSEventName(); + return Assertions.assertNotNull(mTouchEventType).getJSEventName(); } @Override @@ -69,7 +103,7 @@ public class TouchEvent extends Event { // 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) { + switch (Assertions.assertNotNull(mTouchEventType)) { case START: case END: case CANCEL: @@ -90,13 +124,8 @@ public class TouchEvent extends Event { public void dispatch(RCTEventEmitter rctEventEmitter) { TouchesHelper.sendTouchEvent( rctEventEmitter, - mTouchEventType, + Assertions.assertNotNull(mTouchEventType), getViewTag(), - mMotionEvent); - } - - @Override - public void dispose() { - mMotionEvent.recycle(); + Assertions.assertNotNull(mMotionEvent)); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/BUCK new file mode 100644 index 000000000..1bef74069 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/BUCK @@ -0,0 +1,22 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'drawer', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/csslayout:csslayout'), + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_target('java/com/facebook/react/views/scroll:scroll'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + '//third-party/java/android/support/v4:lib-support-v4', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':drawer', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/image/BUCK new file mode 100644 index 000000000..52c2e0b1e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/BUCK @@ -0,0 +1,29 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'image', + srcs = glob(['*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + '//libraries/drawee/src/main/java/com/facebook/drawee/controller:controller', + '//libraries/drawee/src/main/java/com/facebook/drawee/drawable:drawable', + '//libraries/drawee/src/main/java/com/facebook/drawee/generic:generic', + '//libraries/drawee/src/main/java/com/facebook/drawee/interfaces:interfaces', + '//libraries/drawee/src/main/java/com/facebook/drawee/view:view', + '//libraries/fbcore/src/main/java/com/facebook/common/references:references', + '//libraries/fbcore/src/main/java/com/facebook/common/util:util', + '//libraries/fresco/drawee-backends/drawee-pipeline/src/main/java/com/facebook/drawee/backends/pipeline:pipeline', + '//libraries/imagepipeline/src/main/java/com/facebook/imagepipeline/bitmaps:bitmaps', + '//libraries/imagepipeline/src/main/java/com/facebook/imagepipeline/common:common', + '//libraries/imagepipeline/src/main/java/com/facebook/imagepipeline/request:request', + '//third-party/java/jsr-305:jsr-305', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':image', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/BUCK new file mode 100644 index 000000000..ef7fc9609 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/BUCK @@ -0,0 +1,21 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'progressbar', + srcs = glob(['*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/csslayout:csslayout'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + '//third-party/java/infer-annotations:infer-annotations', + '//third-party/java/jsr-305:jsr-305', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':progressbar', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarContainerView.java b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarContainerView.java new file mode 100644 index 000000000..233981506 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarContainerView.java @@ -0,0 +1,84 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.views.progressbar; + +import javax.annotation.Nullable; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ProgressBar; + +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; + +/** + * Controls an enclosing ProgressBar. Exists so that the ProgressBar can be recreated if + * the style would change. + */ +/* package */ class ProgressBarContainerView extends FrameLayout { + private static final int MAX_PROGRESS = 1000; + + private @Nullable Integer mColor; + private boolean mIndeterminate = true; + private double mProgress; + private @Nullable ProgressBar mProgressBar; + + public ProgressBarContainerView(Context context) { + super(context); + } + + public void setStyle(@Nullable String styleName) { + int style = ReactProgressBarViewManager.getStyleFromString(styleName); + mProgressBar = new ProgressBar(getContext(), null, style); + mProgressBar.setMax(MAX_PROGRESS); + removeAllViews(); + addView( + mProgressBar, + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + } + + public void setColor(@Nullable Integer color) { + this.mColor = color; + } + + public void setIndeterminate(boolean indeterminate) { + mIndeterminate = indeterminate; + } + + public void setProgress(double progress) { + mProgress = progress; + } + + public void apply() { + if (mProgressBar == null) { + throw new JSApplicationIllegalArgumentException("setStyle() not called"); + } + + mProgressBar.setIndeterminate(mIndeterminate); + setColor(mProgressBar); + mProgressBar.setProgress((int) (mProgress * MAX_PROGRESS)); + } + + private void setColor(ProgressBar progressBar) { + Drawable drawable; + if (progressBar.isIndeterminate()) { + drawable = progressBar.getIndeterminateDrawable(); + } else { + drawable = progressBar.getProgressDrawable(); + } + + if (drawable == null) { + return; + } + + if (mColor != null) { + drawable.setColorFilter(mColor, PorterDuff.Mode.SRC_IN); + } else { + drawable.clearColorFilter(); + } + } +} 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 index 051bc165b..01aa4a0b8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java @@ -11,26 +11,24 @@ 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.BaseViewManager; import com.facebook.react.uimanager.ReactProp; import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.uimanager.UIProp; +import com.facebook.react.uimanager.ViewProps; /** - * 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. + * Manages instances of ProgressBar. ProgressBar is wrapped in a ProgressBarContainerView 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 - BaseViewManager { + BaseViewManager { /* package */ static final String PROP_STYLE = "styleAttr"; + /* package */ static final String PROP_INDETERMINATE = "indeterminate"; + /* package */ static final String PROP_PROGRESS = "progress"; /* package */ static final String REACT_CLASS = "AndroidProgressBar"; /* package */ static final String DEFAULT_STYLE = "Large"; @@ -41,19 +39,28 @@ public class ReactProgressBarViewManager extends } @Override - protected FrameLayout createViewInstance(ThemedReactContext context) { - return new FrameLayout(context); + protected ProgressBarContainerView createViewInstance(ThemedReactContext context) { + return new ProgressBarContainerView(context); } @ReactProp(name = PROP_STYLE) - public void setStyle(FrameLayout view, @Nullable String styleName) { - final int style = getStyleFromString(styleName); - view.removeAllViews(); - view.addView( - new ProgressBar(view.getContext(), null, style), - new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); + public void setStyle(ProgressBarContainerView view, @Nullable String styleName) { + view.setStyle(styleName); + } + + @ReactProp(name = ViewProps.COLOR, customType = "Color") + public void setColor(ProgressBarContainerView view, @Nullable Integer color) { + view.setColor(color); + } + + @ReactProp(name = PROP_INDETERMINATE) + public void setIndeterminate(ProgressBarContainerView view, boolean indeterminate) { + view.setIndeterminate(indeterminate); + } + + @ReactProp(name = PROP_PROGRESS) + public void setProgress(ProgressBarContainerView view, double progress) { + view.setProgress(progress); } @Override @@ -67,10 +74,15 @@ public class ReactProgressBarViewManager extends } @Override - public void updateExtraData(FrameLayout root, Object extraData) { + public void updateExtraData(ProgressBarContainerView root, Object extraData) { // do nothing } + @Override + protected void onAfterUpdateTransaction(ProgressBarContainerView view) { + view.apply(); + } + /* package */ static int getStyleFromString(@Nullable String styleStr) { if (styleStr == null) { throw new JSApplicationIllegalArgumentException( diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/BUCK new file mode 100644 index 000000000..54414c312 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/BUCK @@ -0,0 +1,25 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'recyclerview', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/touch:touch'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_target('java/com/facebook/react/views/scroll:scroll'), + react_native_target('java/com/facebook/react/views/view:view'), + '//third-party/android-support-v7/recyclerview:recyclerview', + '//third-party/java/android/support/v4:lib-support-v4', + '//third-party/java/infer-annotations:infer-annotations', + '//third-party/java/jsr-305:jsr-305', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':recyclerview', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollView.java index 22e080c83..4105d61b4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollView.java @@ -169,8 +169,10 @@ public class RecyclerViewBackedScrollView extends RecyclerView { notifyDataSetChanged(); } - public void removeView(View child) { - if (mViews.remove(child)) { + public void removeViewAt(int index) { + View child = mViews.get(index); + if (child != null) { + mViews.remove(index); mTopOffsetsFromLayout.remove(child); child.removeOnLayoutChangeListener(mChildLayoutChangeListener); mTotalChildrenHeight -= child.getMeasuredHeight(); @@ -223,30 +225,40 @@ public class RecyclerViewBackedScrollView extends RecyclerView { } } - @Override - protected void onScrollChanged(int l, int t, int oldl, int oldt) { - super.onScrollChanged(l, t, oldl, oldt); - - ReactListAdapter adapter = (ReactListAdapter) getAdapter(); - + private int calculateAbsoluteOffset() { int offsetY = 0; if (getChildCount() > 0) { View recyclerViewChild = getChildAt(0); int childPosition = getChildAdapterPosition(recyclerViewChild); - offsetY = adapter.getTopOffsetForItem(childPosition) - recyclerViewChild.getTop(); + offsetY = ((ReactListAdapter) getAdapter()).getTopOffsetForItem(childPosition) - + recyclerViewChild.getTop(); } + return offsetY; + } + + /*package*/ void scrollTo(int scrollX, int scrollY, boolean animated) { + int deltaY = scrollY - calculateAbsoluteOffset(); + if (animated) { + smoothScrollBy(0, deltaY); + } else { + scrollBy(0, deltaY); + } + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); - ScrollEvent event = new ScrollEvent( - getId(), - SystemClock.uptimeMillis(), - 0, /* offsetX = 0, horizontal scrolling only */ - offsetY, - getWidth(), - adapter.getTotalChildrenHeight(), - getWidth(), - getHeight()); ((ReactContext) getContext()).getNativeModule(UIManagerModule.class).getEventDispatcher() - .dispatchEvent(event); + .dispatchEvent(ScrollEvent.obtain( + getId(), + SystemClock.uptimeMillis(), + 0, /* offsetX = 0, horizontal scrolling only */ + calculateAbsoluteOffset(), + getWidth(), + ((ReactListAdapter) getAdapter()).getTotalChildrenHeight(), + getWidth(), + getHeight())); } public RecyclerViewBackedScrollView(Context context) { @@ -261,8 +273,8 @@ public class RecyclerViewBackedScrollView extends RecyclerView { ((ReactListAdapter) getAdapter()).addView(child, index); } - /*package*/ void removeViewFromAdapter(View child) { - ((ReactListAdapter) getAdapter()).removeView(child); + /*package*/ void removeViewFromAdapter(int index) { + ((ReactListAdapter) getAdapter()).removeViewAt(index); } /*package*/ View getChildAtFromAdapter(int index) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java index 4fd430a1c..8c0134dad 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java @@ -2,16 +2,21 @@ package com.facebook.react.views.recyclerview; +import javax.annotation.Nullable; + import android.view.View; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ViewGroupManager; +import com.facebook.react.views.scroll.ReactScrollViewCommandHelper; /** * View manager for {@link RecyclerViewBackedScrollView}. */ public class RecyclerViewBackedScrollViewManager extends - ViewGroupManager { + ViewGroupManager + implements ReactScrollViewCommandHelper.ScrollCommandHandler { private static final String REACT_CLASS = "AndroidRecyclerViewBackedScrollView"; @@ -43,7 +48,32 @@ public class RecyclerViewBackedScrollViewManager extends } @Override - public void removeView(RecyclerViewBackedScrollView parent, View child) { - parent.removeViewFromAdapter(child); + public void removeViewAt(RecyclerViewBackedScrollView parent, int index) { + parent.removeViewFromAdapter(index); + } + + /** + * Provides implementation of commands supported by {@link ReactScrollViewManager} + */ + @Override + public void receiveCommand( + RecyclerViewBackedScrollView view, + int commandId, + @Nullable ReadableArray args) { + ReactScrollViewCommandHelper.receiveCommand(this, view, commandId, args); + } + + @Override + public void scrollTo( + RecyclerViewBackedScrollView view, + ReactScrollViewCommandHelper.ScrollToCommandData data) { + view.scrollTo(data.mDestX, data.mDestY, true); + } + + @Override + public void scrollWithoutAnimationTo( + RecyclerViewBackedScrollView view, + ReactScrollViewCommandHelper.ScrollToCommandData data) { + view.scrollTo(data.mDestX, data.mDestY, false); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/BUCK new file mode 100644 index 000000000..d08719512 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/BUCK @@ -0,0 +1,23 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'scroll', + srcs = glob(['*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/views/view:view'), + react_native_target('java/com/facebook/react/touch:touch'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/java/android/support/v4:lib-support-v4'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':scroll', +) 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 index 698ea9262..e8ee9fff8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java @@ -12,6 +12,7 @@ package com.facebook.react.views.scroll; import javax.annotation.Nullable; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.uimanager.ReactProp; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ViewGroupManager; @@ -37,6 +38,11 @@ public class ReactHorizontalScrollViewManager return new ReactHorizontalScrollView(context); } + @ReactProp(name = "showsHorizontalScrollIndicator") + public void setShowsHorizontalScrollIndicator(ReactHorizontalScrollView view, boolean value) { + view.setHorizontalScrollBarEnabled(value); + } + @Override public void receiveCommand( ReactHorizontalScrollView scrollView, 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 index 586e89506..a5eb1421c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java @@ -60,14 +60,14 @@ public class ReactScrollViewCommandHelper { 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))); + int destX = Math.round(PixelUtil.toPixelFromDIP(args.getDouble(0))); + int destY = Math.round(PixelUtil.toPixelFromDIP(args.getDouble(1))); viewManager.scrollTo(scrollView, new ScrollToCommandData(destX, destY)); return; } case COMMAND_SCROLL_WITHOUT_ANIMATION_TO: { - int destX = Math.round(PixelUtil.toPixelFromDIP(args.getInt(0))); - int destY = Math.round(PixelUtil.toPixelFromDIP(args.getInt(1))); + int destX = Math.round(PixelUtil.toPixelFromDIP(args.getDouble(0))); + int destY = Math.round(PixelUtil.toPixelFromDIP(args.getDouble(1))); viewManager.scrollWithoutAnimationTo(scrollView, new ScrollToCommandData(destX, destY)); return; } 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 index c0b72def6..12f5606fc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java @@ -28,7 +28,7 @@ public class ReactScrollViewHelper { View contentView = scrollView.getChildAt(0); ReactContext reactContext = (ReactContext) scrollView.getContext(); reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent( - new ScrollEvent( + ScrollEvent.obtain( scrollView.getId(), SystemClock.uptimeMillis(), scrollX, 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 index 366173333..2c3363f8e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java @@ -47,11 +47,6 @@ public class ReactScrollViewManager view.setVerticalScrollBarEnabled(value); } - @ReactProp(name = "showsHorizontalScrollIndicator") - public void setShowsHorizontalScrollIndicator(ReactScrollView view, boolean value) { - view.setHorizontalScrollBarEnabled(value); - } - @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS) public void setRemoveClippedSubviews(ReactScrollView view, boolean removeClippedSubviews) { view.setRemoveClippedSubviews(removeClippedSubviews); 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 index 6d9220037..fdf8cd5a2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java @@ -9,6 +9,10 @@ package com.facebook.react.views.scroll; +import java.lang.Override; + +import android.support.v4.util.Pools; + import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; import com.facebook.react.uimanager.PixelUtil; @@ -20,16 +24,19 @@ import com.facebook.react.uimanager.events.RCTEventEmitter; */ public class ScrollEvent extends Event { + private static final Pools.SynchronizedPool EVENTS_POOL = + new Pools.SynchronizedPool<>(3); + 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; + private int mScrollX; + private int mScrollY; + private int mContentWidth; + private int mContentHeight; + private int mScrollViewWidth; + private int mScrollViewHeight; - public ScrollEvent( + public static ScrollEvent obtain( int viewTag, long timestampMs, int scrollX, @@ -38,7 +45,40 @@ public class ScrollEvent extends Event { int contentHeight, int scrollViewWidth, int scrollViewHeight) { - super(viewTag, timestampMs); + ScrollEvent event = EVENTS_POOL.acquire(); + if (event == null) { + event = new ScrollEvent(); + } + event.init( + viewTag, + timestampMs, + scrollX, + scrollY, + contentWidth, + contentHeight, + scrollViewWidth, + scrollViewHeight); + return event; + } + + @Override + public void onDispose() { + EVENTS_POOL.release(this); + } + + private ScrollEvent() { + } + + private void init( + int viewTag, + long timestampMs, + int scrollX, + int scrollY, + int contentWidth, + int contentHeight, + int scrollViewWidth, + int scrollViewHeight) { + super.init(viewTag, timestampMs); mScrollX = scrollX; mScrollY = scrollY; mContentWidth = contentWidth; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/BUCK new file mode 100644 index 000000000..6c353cbe8 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/BUCK @@ -0,0 +1,21 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'switchview', + srcs = glob(['*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/csslayout:csslayout'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + '//third-party/android-support-for-standalone-apps/v7/appcompat:appcompat-23.1', + '//third-party/android-support-for-standalone-apps/v7/appcompat:res-for-react-native', + '//third-party/java/jsr-305:jsr-305', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':switchview', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK new file mode 100644 index 000000000..cd0f60c1b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK @@ -0,0 +1,21 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'text', + srcs = glob(['*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/csslayout:csslayout'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':text', +) 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 index eac5ed6db..07a303e22 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java @@ -14,6 +14,7 @@ import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; +import android.content.res.AssetManager; import android.graphics.Paint; import android.graphics.Typeface; import android.text.TextPaint; @@ -21,33 +22,43 @@ 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(); + /** + * A {@link MetricAffectingSpan} that allows to change the style of the displayed font. + * CustomStyleSpan will try to load the fontFamily with the right style and weight from the + * assets. The custom fonts will have to be located in the res/assets folder of the application. + * The supported custom fonts extensions are .ttf and .otf. For each font family the bold, + * italic and bold_italic variants are supported. Given a "family" font family the files in the + * assets/fonts folder need to be family.ttf(.otf) family_bold.ttf(.otf) family_italic.ttf(.otf) + * and family_bold_italic.ttf(.otf). If the right font is not found in the assets folder + * CustomStyleSpan will fallback on the most appropriate default typeface depending on the style. + * Fonts are retrieved and cached using the {@link ReactFontManager} + */ + + private final AssetManager mAssetManager; private final int mStyle; private final int mWeight; private final @Nullable String mFontFamily; - public CustomStyleSpan(int fontStyle, int fontWeight, @Nullable String fontFamily) { + public CustomStyleSpan( + int fontStyle, + int fontWeight, + @Nullable String fontFamily, + AssetManager assetManager) { mStyle = fontStyle; mWeight = fontWeight; mFontFamily = fontFamily; + mAssetManager = assetManager; } @Override public void updateDrawState(TextPaint ds) { - apply(ds, mStyle, mWeight, mFontFamily); + apply(ds, mStyle, mWeight, mFontFamily, mAssetManager); } @Override public void updateMeasureState(TextPaint paint) { - apply(paint, mStyle, mWeight, mFontFamily); + apply(paint, mStyle, mWeight, mFontFamily, mAssetManager); } /** @@ -61,7 +72,7 @@ public class CustomStyleSpan extends MetricAffectingSpan { * Returns {@link Typeface#NORMAL} or {@link Typeface#BOLD}. */ public int getWeight() { - return (mWeight == ReactTextShadowNode.UNSET ? 0 : mWeight); + return (mWeight == ReactTextShadowNode.UNSET ? 0 : mWeight); } /** @@ -71,7 +82,12 @@ public class CustomStyleSpan extends MetricAffectingSpan { return mFontFamily; } - private static void apply(Paint paint, int style, int weight, @Nullable String family) { + private static void apply( + Paint paint, + int style, + int weight, + @Nullable String family, + AssetManager assetManager) { int oldStyle; Typeface typeface = paint.getTypeface(); if (typeface == null) { @@ -92,23 +108,14 @@ public class CustomStyleSpan extends MetricAffectingSpan { } if (family != null) { - typeface = getOrCreateTypeface(family, want); + typeface = ReactFontManager.getInstance().getTypeface(family, want, assetManager); } if (typeface != null) { - paint.setTypeface(Typeface.create(typeface, want)); + paint.setTypeface(typeface); } 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/ReactFontManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactFontManager.java new file mode 100644 index 000000000..10e2a4d7b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactFontManager.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + *

+ * This source code is licensed under the BSD-style license found in the LICENSE file in the root + * directory of 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.content.res.AssetManager; +import android.graphics.Typeface; +import android.util.SparseArray; + +/** + * Class responsible to load and cache Typeface objects. It will first try to load typefaces inside + * the assets/fonts folder and if it doesn't find the right Typeface in that folder will fall back + * on the best matching system Typeface The supported custom fonts extensions are .ttf and .otf. For + * each font family the bold, italic and bold_italic variants are supported. Given a "family" font + * family the files in the assets/fonts folder need to be family.ttf(.otf) family_bold.ttf(.otf) + * family_italic.ttf(.otf) and family_bold_italic.ttf(.otf) + */ +public class ReactFontManager { + + private static final String[] EXTENSIONS = { + "", + "_bold", + "_italic", + "_bold_italic"}; + private static final String[] FILE_EXTENSIONS = {".ttf", ".otf"}; + private static final String FONTS_ASSET_PATH = "fonts/"; + + private static ReactFontManager sReactFontManagerInstance; + + private Map mFontCache; + + private ReactFontManager() { + mFontCache = new HashMap<>(); + } + + public static ReactFontManager getInstance() { + if (sReactFontManagerInstance == null) { + sReactFontManagerInstance = new ReactFontManager(); + } + return sReactFontManagerInstance; + } + + public + @Nullable Typeface getTypeface( + String fontFamilyName, + int style, + AssetManager assetManager) { + FontFamily fontFamily = mFontCache.get(fontFamilyName); + if (fontFamily == null) { + fontFamily = new FontFamily(); + mFontCache.put(fontFamilyName, fontFamily); + } + + Typeface typeface = fontFamily.getTypeface(style); + if (typeface == null) { + typeface = createTypeface(fontFamilyName, style, assetManager); + if (typeface != null) { + fontFamily.setTypeface(style, typeface); + } + } + + return typeface; + } + + private static + @Nullable Typeface createTypeface( + String fontFamilyName, + int style, + AssetManager assetManager) { + String extension = EXTENSIONS[style]; + for (String fileExtension : FILE_EXTENSIONS) { + String fileName = new StringBuilder() + .append(FONTS_ASSET_PATH) + .append(fontFamilyName) + .append(extension) + .append(fileExtension) + .toString(); + try { + return Typeface.createFromAsset(assetManager, fileName); + } catch (RuntimeException e) { + // unfortunately Typeface.createFromAsset throws an exception instead of returning null + // if the typeface doesn't exist + } + } + + return Typeface.create(fontFamilyName, style); + } + + private static class FontFamily { + + private SparseArray mTypefaceSparseArray; + + private FontFamily() { + mTypefaceSparseArray = new SparseArray<>(4); + } + + public Typeface getTypeface(int style) { + return mTypefaceSparseArray.get(style); + } + + public void setTypeface(int style, Typeface typeface) { + mTypefaceSparseArray.put(style, typeface); + } + + } +} 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 index 83f556823..9635adfcd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java @@ -32,11 +32,6 @@ public class ReactRawTextManager extends ReactTextViewManager { 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) { } 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 index 1d8c8ec8f..9d9d74e8b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java @@ -43,16 +43,16 @@ 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 + * 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 LayoutShadowNode { @@ -100,12 +100,12 @@ public class ReactTextShadowNode extends LayoutShadowNode { buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops); } else { throw new IllegalViewOperationException("Unexpected view type nested under text node: " - + child.getClass()); + + child.getClass()); } ((ReactTextShadowNode) child).markUpdateSeen(); } int end = sb.length(); - if (end > start) { + if (end >= start) { if (textCSSNode.mIsColorSet) { ops.add(new SetSpanOperation(start, end, new ForegroundColorSpan(textCSSNode.mColor))); } @@ -128,7 +128,8 @@ public class ReactTextShadowNode extends LayoutShadowNode { new CustomStyleSpan( textCSSNode.mFontStyle, textCSSNode.mFontWeight, - textCSSNode.mFontFamily))); + textCSSNode.mFontFamily, + textCSSNode.getThemedContext().getAssets()))); } ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textCSSNode.getReactTag()))); } @@ -197,7 +198,7 @@ public class ReactTextShadowNode extends LayoutShadowNode { 0, boring, true); - } else { + } else { // Is used for multiline, boring text and the width is known. layout = new StaticLayout( text, 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 index 5021f522a..c374d4c9f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java @@ -19,11 +19,9 @@ import android.widget.TextView; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.uimanager.BaseViewManager; -import com.facebook.react.uimanager.CatalystStylesDiffMap; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactProp; import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.uimanager.UIProp; import com.facebook.react.uimanager.ViewDefaults; import com.facebook.react.uimanager.ViewProps; import com.facebook.react.common.annotations.VisibleForTesting; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK new file mode 100644 index 000000000..4c7142f68 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK @@ -0,0 +1,23 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'textinput', + srcs = glob(['*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/views/text:text'), + react_native_target('java/com/facebook/csslayout:csslayout'), + react_native_target('java/com/facebook/react/modules/core:core'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':textinput', +) 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 index 79a67f864..4c5f8011c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -20,12 +20,17 @@ import android.text.InputType; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextWatcher; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; import android.view.Gravity; import android.view.KeyEvent; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import com.facebook.infer.annotation.Assertions; +import com.facebook.react.views.text.CustomStyleSpan; +import com.facebook.react.views.text.ReactTagSpan; /** * A wrapper around the EditText that lets us better control what happens when an EditText gets @@ -204,6 +209,15 @@ public class ReactEditText extends EditText { private void manageSpans(SpannableStringBuilder spannableStringBuilder) { Object[] spans = getText().getSpans(0, length(), Object.class); for (int spanIdx = 0; spanIdx < spans.length; spanIdx++) { + // Remove all styling spans we might have previously set + if (ForegroundColorSpan.class.isInstance(spans[spanIdx]) || + BackgroundColorSpan.class.isInstance(spans[spanIdx]) || + AbsoluteSizeSpan.class.isInstance(spans[spanIdx]) || + CustomStyleSpan.class.isInstance(spans[spanIdx]) || + ReactTagSpan.class.isInstance(spans[spanIdx])) { + getText().removeSpan(spans[spanIdx]); + } + if ((getText().getSpanFlags(spans[spanIdx]) & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) != Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) { continue; 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 index f7363441b..268b5c465 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java @@ -16,8 +16,9 @@ import com.facebook.react.uimanager.events.RCTEventEmitter; /** * Event emitted by EditText native view when text changes. + * VisibleForTesting from {@link TextInputEventsTestCase}. */ -/* package */ class ReactTextChangedEvent extends Event { +public class ReactTextChangedEvent extends Event { public static final String EVENT_NAME = "topChange"; 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 index f2cbc8b91..3d8f4ccbb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEvent.java @@ -16,8 +16,9 @@ import com.facebook.react.uimanager.events.RCTEventEmitter; /** * Event emitted by EditText native view when text changes. + * VisibleForTesting from {@link TextInputEventsTestCase}. */ -/* package */ class ReactTextInputEvent extends Event { +public class ReactTextInputEvent extends Event { public static final String EVENT_NAME = "topTextInput"; 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 index e7c28d556..190d9f107 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -24,6 +24,7 @@ import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.TextView; +import android.text.InputFilter; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.JSApplicationCausedNativeException; @@ -35,7 +36,6 @@ import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactProp; 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.ViewProps; import com.facebook.react.uimanager.events.EventDispatcher; @@ -54,6 +54,7 @@ public class ReactTextInputManager extends private static final String KEYBOARD_TYPE_EMAIL_ADDRESS = "email-address"; private static final String KEYBOARD_TYPE_NUMERIC = "numeric"; + private static final InputFilter[] EMPTY_FILTERS = new InputFilter[0]; @Override public String getName() { @@ -200,6 +201,17 @@ public class ReactTextInputManager extends view.setLines(numLines); } + @ReactProp(name = "maxLength") + public void setMaxLength(ReactEditText view, @Nullable Integer maxLength) { + if (maxLength == null) { + view.setFilters(EMPTY_FILTERS); + } else { + InputFilter[] filterArray = new InputFilter[1]; + filterArray[0] = new InputFilter.LengthFilter(maxLength); + view.setFilters(filterArray); + } + } + @ReactProp(name = "autoCorrect") public void setAutoCorrect(ReactEditText view, @Nullable Boolean autoCorrect) { // clear auto correct flags, set SUGGESTIONS or NO_SUGGESTIONS depending on value 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 index 2cf434fe0..7ac6181c5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java @@ -63,7 +63,7 @@ public class ReactTextInputShadowNode extends ReactTextShadowNode implements setDefaultPadding(Spacing.TOP, mEditText.getPaddingTop()); setDefaultPadding(Spacing.RIGHT, mEditText.getPaddingRight()); setDefaultPadding(Spacing.BOTTOM, mEditText.getPaddingBottom()); - mComputedPadding = spacingToFloatArray(getStylePadding()); + mComputedPadding = spacingToFloatArray(getPadding()); } @Override @@ -76,12 +76,12 @@ public class ReactTextInputShadowNode extends ReactTextShadowNode implements TypedValue.COMPLEX_UNIT_PX, mFontSize == UNSET ? (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP)) : mFontSize); - mComputedPadding = spacingToFloatArray(getStylePadding()); + mComputedPadding = spacingToFloatArray(getPadding()); 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))); + (int) Math.ceil(getPadding().get(Spacing.LEFT)), + (int) Math.ceil(getPadding().get(Spacing.TOP)), + (int) Math.ceil(getPadding().get(Spacing.RIGHT)), + (int) Math.ceil(getPadding().get(Spacing.BOTTOM))); if (mNumberOfLines != UNSET) { editText.setLines(mNumberOfLines); @@ -120,7 +120,7 @@ public class ReactTextInputShadowNode extends ReactTextShadowNode implements @Override public void setPadding(int spacingType, float padding) { super.setPadding(spacingType, padding); - mComputedPadding = spacingToFloatArray(getStylePadding()); + mComputedPadding = spacingToFloatArray(getPadding()); markUpdated(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/BUCK new file mode 100644 index 000000000..35a06f331 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/BUCK @@ -0,0 +1,33 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'toolbar', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/csslayout:csslayout'), + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_target('java/com/facebook/react/common:common'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + '//libraries/imagepipeline/src/main/java/com/facebook/imagepipeline/bitmaps:bitmaps', + '//libraries/imagepipeline/src/main/java/com/facebook/imagepipeline/common:common', + '//libraries/imagepipeline/src/main/java/com/facebook/imagepipeline/request:request', + '//libraries/imagepipeline/src/main/java/com/facebook/imagepipeline/image:image', + '//libraries/drawee/src/main/java/com/facebook/drawee/controller:controller', + '//libraries/drawee/src/main/java/com/facebook/drawee/drawable:drawable', + '//libraries/drawee/src/main/java/com/facebook/drawee/generic:generic', + '//libraries/drawee/src/main/java/com/facebook/drawee/interfaces:interfaces', + '//libraries/drawee/src/main/java/com/facebook/drawee/view:view', + '//libraries/fresco/drawee-backends/drawee-pipeline/src/main/java/com/facebook/drawee/backends/pipeline:pipeline', + '//third-party/android-support-for-standalone-apps/v7/appcompat:appcompat-23.1', + '//third-party/android-support-for-standalone-apps/v7/appcompat:res-for-react-native', + '//third-party/java/android/support/v4:lib-support-v4', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':toolbar', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbar.java b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbar.java index 91741c1c6..5fe30921a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbar.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbar.java @@ -6,6 +6,7 @@ import javax.annotation.Nullable; import android.content.Context; import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.support.v7.widget.Toolbar; import android.view.Menu; @@ -36,6 +37,7 @@ public class ReactToolbar extends Toolbar { private final DraweeHolder mLogoHolder; private final DraweeHolder mNavIconHolder; + private final DraweeHolder mOverflowIconHolder; private final MultiDraweeHolder mActionsHolder = new MultiDraweeHolder<>(); @@ -69,6 +71,21 @@ public class ReactToolbar extends Toolbar { } }; + private final ControllerListener mOverflowIconControllerListener = + new BaseControllerListener() { + @Override + public void onFinalImageSet( + String id, + @Nullable final ImageInfo imageInfo, + @Nullable Animatable animatable) { + if (imageInfo != null) { + final DrawableWithIntrinsicSize overflowIconDrawable = + new DrawableWithIntrinsicSize(mOverflowIconHolder.getTopLevelDrawable(), imageInfo); + setOverflowIcon(overflowIconDrawable); + } + } + }; + private static class ActionIconControllerListener extends BaseControllerListener { private final MenuItem mItem; private final DraweeHolder mHolder; @@ -94,6 +111,7 @@ public class ReactToolbar extends Toolbar { mLogoHolder = DraweeHolder.create(createDraweeHierarchy(), context); mNavIconHolder = DraweeHolder.create(createDraweeHierarchy(), context); + mOverflowIconHolder = DraweeHolder.create(createDraweeHierarchy(), context); } private final Runnable mLayoutRunnable = new Runnable() { @@ -136,19 +154,20 @@ public class ReactToolbar extends Toolbar { @Override public void onFinishTemporaryDetach() { super.onFinishTemporaryDetach(); - mLogoHolder.onAttach(); - mNavIconHolder.onAttach(); + attachDraweeHolders(); } private void detachDraweeHolders() { mLogoHolder.onDetach(); mNavIconHolder.onDetach(); + mOverflowIconHolder.onDetach(); mActionsHolder.onDetach(); } private void attachDraweeHolders() { mLogoHolder.onAttach(); mNavIconHolder.onAttach(); + mOverflowIconHolder.onAttach(); mActionsHolder.onAttach(); } @@ -184,6 +203,22 @@ public class ReactToolbar extends Toolbar { } } + /* package */ void setOverflowIconSource(@Nullable ReadableMap source) { + String uri = source != null ? source.getString("uri") : null; + if (uri == null) { + setOverflowIcon(null); + } else if (uri.startsWith("http://") || uri.startsWith("https://")) { + DraweeController controller = Fresco.newDraweeControllerBuilder() + .setUri(Uri.parse(uri)) + .setControllerListener(mOverflowIconControllerListener) + .setOldController(mOverflowIconHolder.getController()) + .build(); + mOverflowIconHolder.setController(controller); + } else { + setOverflowIcon(getDrawableByName(uri)); + } + } + /* package */ void setActions(@Nullable ReadableArray actions) { Menu menu = getMenu(); menu.clear(); @@ -247,4 +282,8 @@ public class ReactToolbar extends Toolbar { getContext().getPackageName()); } + private Drawable getDrawableByName(String name) { + return getResources().getDrawable(getDrawableResourceByName(name)); + } + } 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 index 93fe8a61d..de1038a98 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java @@ -59,6 +59,11 @@ public class ReactToolbarManager extends ViewGroupManager { view.setNavIconSource(navIcon); } + @ReactProp(name = "overflowIcon") + public void setOverflowIcon(ReactToolbar view, @Nullable ReadableMap overflowIcon) { + view.setOverflowIconSource(overflowIcon); + } + @ReactProp(name = "subtitle") public void setSubtitle(ReactToolbar view, @Nullable String subtitle) { view.setSubtitle(subtitle); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/view/BUCK new file mode 100644 index 000000000..428372baa --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/BUCK @@ -0,0 +1,22 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'view', + srcs = glob(['*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/csslayout:csslayout'), + react_native_target('java/com/facebook/react/touch:touch'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':view', +) 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 index 2af6bb254..6131f0051 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -110,20 +110,16 @@ public class ReactViewGroup extends ViewGroup implements // No-op since UIManagerModule handles actually laying out children. } + @Override + public void requestLayout() { + // No-op, terminate `requestLayout` here, UIManagerModule handles laying out children and + // `layout` is called on all RN-managed views by `NativeViewHierarchyManager` + } + @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); - } + if (color == Color.TRANSPARENT && mReactBackgroundDrawable == null) { + // don't do anything, no need to allocate ReactBackgroundDrawable for transparent background } else { getOrCreateReactViewBackground().setColor(color); } 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 index 33fd63b57..712d7dc3d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -195,16 +195,16 @@ public class ReactViewManager extends ViewGroupManager { } @Override - public void removeView(ReactViewGroup parent, View child) { + public void removeViewAt(ReactViewGroup parent, int index) { boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); if (removeClippedSubviews) { + View child = getChildAt(parent, index); if (child.getParent() != null) { parent.removeView(child); } parent.removeViewWithSubviewClippingEnabled(child); } else { - parent.removeView(child); + parent.removeViewAt(index); } } - } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/viewpager/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/viewpager/BUCK new file mode 100644 index 000000000..8b0b881fa --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/viewpager/BUCK @@ -0,0 +1,21 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'viewpager', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_target('java/com/facebook/react/views/scroll:scroll'), + '//third-party/java/android/support/v4:lib-support-v4', + '//third-party/java/jsr-305:jsr-305', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':viewpager', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/viewpager/ReactViewPager.java b/ReactAndroid/src/main/java/com/facebook/react/views/viewpager/ReactViewPager.java index ad669a7c6..43944c2ba 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/viewpager/ReactViewPager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/viewpager/ReactViewPager.java @@ -49,6 +49,19 @@ import com.facebook.react.uimanager.events.NativeGestureUtil; setOffscreenPageLimit(mViews.size()); } + void removeViewAt(int index) { + mViews.remove(index); + notifyDataSetChanged(); + + // TODO(7323049): Remove this workaround once we figure out a way to re-layout some views on + // request + setOffscreenPageLimit(mViews.size()); + } + + View getViewAt(int index) { + return mViews.get(index); + } + @Override public int getCount() { return mViews.size(); @@ -120,11 +133,23 @@ import com.facebook.react.uimanager.events.NativeGestureUtil; return false; } - /* package */ void addViewToAdapter(View child, int index) { + /*package*/ void addViewToAdapter(View child, int index) { getAdapter().addView(child, index); } - /* package */ void setCurrentItemFromJs(int item) { + /*package*/ void removeViewFromAdapter(int index) { + getAdapter().removeViewAt(index); + } + + /*package*/ int getViewCountInAdapter() { + return getAdapter().getCount(); + } + + /*package*/ View getViewFromAdapter(int index) { + return getAdapter().getViewAt(index); + } + + /*package*/ void setCurrentItemFromJs(int item) { mIsCurrentItemFromJs = true; setCurrentItem(item); mIsCurrentItemFromJs = false; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/viewpager/ReactViewPagerManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/viewpager/ReactViewPagerManager.java index 05fd1e76d..0002a7876 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/viewpager/ReactViewPagerManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/viewpager/ReactViewPagerManager.java @@ -58,4 +58,19 @@ public class ReactViewPagerManager extends ViewGroupManager { public void addView(ReactViewPager parent, View child, int index) { parent.addViewToAdapter(child, index); } + + @Override + public int getChildCount(ReactViewPager parent) { + return parent.getViewCountInAdapter(); + } + + @Override + public View getChildAt(ReactViewPager parent, int index) { + return parent.getViewFromAdapter(index); + } + + @Override + public void removeViewAt(ReactViewPager parent, int index) { + parent.removeViewFromAdapter(index); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/BUCK b/ReactAndroid/src/main/java/com/facebook/soloader/BUCK new file mode 100644 index 000000000..322ba2204 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/BUCK @@ -0,0 +1,20 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'soloader', + srcs = glob(['*.java']), + proguard_config = 'soloader.pro', + deps = [ + react_native_dep('third-party/java/jsr-305:jsr-305'), + # Be very careful adding new dependencies here, because this code + # has to run very early in the app startup process. + # Definitely do *not* depend on lib-base or guava. + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':soloader', +) diff --git a/ReactAndroid/src/main/java/com/facebook/systrace/BUCK b/ReactAndroid/src/main/java/com/facebook/systrace/BUCK new file mode 100644 index 000000000..6d222bb2a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/systrace/BUCK @@ -0,0 +1,11 @@ +android_library( + name = 'systrace', + srcs = glob(['*.java']), + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':systrace', +) diff --git a/ReactAndroid/src/main/java/com/facebook/systrace/Systrace.java b/ReactAndroid/src/main/java/com/facebook/systrace/Systrace.java index e3642c478..20867d7e2 100644 --- a/ReactAndroid/src/main/java/com/facebook/systrace/Systrace.java +++ b/ReactAndroid/src/main/java/com/facebook/systrace/Systrace.java @@ -9,8 +9,12 @@ package com.facebook.systrace; +import android.os.Build; +import android.os.Trace; + /** - * Systrace stub. + * Systrace stub that mostly does nothing but delegates to Trace for beginning/ending sections. + * The internal version of this file has not been opensourced yet. */ public class Systrace { @@ -50,9 +54,15 @@ public class Systrace { } public static void beginSection(long tag, final String sectionName) { + if (Build.VERSION.SDK_INT >= 18) { + Trace.beginSection(sectionName); + } } public static void endSection(long tag) { + if (Build.VERSION.SDK_INT >= 18) { + Trace.endSection(); + } } public static void beginAsyncSection( diff --git a/ReactAndroid/src/main/jni/react/BUCK b/ReactAndroid/src/main/jni/react/BUCK new file mode 100644 index 000000000..14e7e8ffe --- /dev/null +++ b/ReactAndroid/src/main/jni/react/BUCK @@ -0,0 +1,57 @@ +include_defs('//ReactAndroid/DEFS') + +# We depend on JSC, support the same platforms +SUPPORTED_PLATFORMS = '^android-(armv7|x86)$' + +cxx_library( + name = 'react', + soname = 'libreactnative.so', + header_namespace = 'react', + supported_platforms_regex = SUPPORTED_PLATFORMS, + force_static = True, + srcs = [ + 'Bridge.cpp', + 'Value.cpp', + 'MethodCall.cpp', + 'JSCHelpers.cpp', + 'JSCExecutor.cpp', + 'JSCTracing.cpp', + 'JSCPerfLogging.cpp', + 'JSCLegacyProfiler.cpp', + ], + headers = [ + 'JSCTracing.h', + 'JSCPerfLogging.h', + 'JSCLegacyProfiler.h', + ], + exported_headers = [ + 'Bridge.h', + 'Executor.h', + 'JSCExecutor.h', + 'JSCHelpers.h', + 'MethodCall.h', + 'Value.h', + ], + preprocessor_flags = [ + '-DLOG_TAG="ReactNative"', + '-DWITH_JSC_EXTRA_TRACING=1', + '-DWITH_FBSYSTRACE=1', + ], + compiler_flags = [ + '-Wall', + '-std=c++11', + '-fexceptions', + '-fvisibility=hidden', + ], + visibility = [ + react_native_target('jni/react/jni:jni'), + ], + deps = [ + '//native/fb:fb', + '//xplat/fbsystrace:fbsystrace', + '//native/jni:jni', + '//native/third-party/jsc:jsc', + '//native/third-party/jsc:jsc_legacy_profiler', + '//xplat/folly:json', + ], +) diff --git a/ReactAndroid/src/main/jni/react/JSCExecutor.cpp b/ReactAndroid/src/main/jni/react/JSCExecutor.cpp index c58d42dbf..ca7afa29a 100644 --- a/ReactAndroid/src/main/jni/react/JSCExecutor.cpp +++ b/ReactAndroid/src/main/jni/react/JSCExecutor.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "Value.h" #ifdef WITH_JSC_EXTRA_TRACING @@ -24,6 +25,8 @@ using fbsystrace::FbSystraceSection; // Add native performance markers support #include +using namespace facebook::jni; + namespace facebook { namespace react { @@ -53,6 +56,17 @@ static JSValueRef evaluateScriptWithJSC( JSValueProtect(ctx, exn); std::string exceptionText = Value(ctx, exn).toString().str(); FBLOGE("Got JS Exception: %s", exceptionText.c_str()); + auto line = Value(ctx, JSObjectGetProperty(ctx, + JSValueToObject(ctx, exn, nullptr), + JSStringCreateWithUTF8CString("line"), nullptr + )); + std::ostringstream lineInfo; + if (line != nullptr && line.isNumber()) { + lineInfo << " (line " << line.asInteger() << " in the generated bundle)"; + } else { + lineInfo << " (no line info)"; + } + throwNewJavaException("com/facebook/react/bridge/JSExecutionException", (exceptionText + lineInfo.str()).c_str()); } return result; } @@ -194,7 +208,7 @@ static JSValueRef nativeLoggingHook( // The lowest log level we get from JS is 0. We shift and cap it to be // in the range the Android logging method expects. logLevel = std::min( - static_cast(level + ANDROID_LOG_VERBOSE), + static_cast(level + ANDROID_LOG_DEBUG), ANDROID_LOG_FATAL); } if (argumentCount > 0) { diff --git a/ReactAndroid/src/main/jni/react/jni/BUCK b/ReactAndroid/src/main/jni/react/jni/BUCK new file mode 100644 index 000000000..b70ecf239 --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/BUCK @@ -0,0 +1,53 @@ +include_defs('//ReactAndroid/DEFS') + +# We depend on JSC, support the same platforms +SUPPORTED_PLATFORMS = '^android-(armv7|x86)$' + +cxx_library( + name = 'jni', + soname = 'libreactnativejni.so', + header_namespace = 'react/jni', + supported_platforms_regex = SUPPORTED_PLATFORMS, + srcs = [ + 'NativeArray.cpp', + 'OnLoad.cpp', + 'ProxyExecutor.cpp', + 'JSLoader.cpp', + ], + headers = [ + 'ProxyExecutor.h', + 'JSLoader.h', + ], + exported_headers = [ + 'NativeArray.h', + 'ReadableNativeArray.h', + ], + preprocessor_flags = [ + '-DLOG_TAG="ReactNativeJNI"', + '-DWITH_FBSYSTRACE=1', + ], + compiler_flags = [ + '-Wall', + '-Werror', + '-fexceptions', + '-std=c++11', + '-fvisibility=hidden', + '-frtti', + ], + visibility = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('jni/react/...'), + react_native_dep('native/react/...') + ], + deps = [ + react_native_target('jni/react:react'), + '//native/jni:jni', + '//native/third-party/jsc:jsc', + '//native/third-party/jsc:jsc_legacy_profiler', + '//xplat/folly:json', + ], +) + +project_config( + src_target = ':jni', +) diff --git a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp index 2ee64145d..bc35feeb2 100644 --- a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp @@ -50,11 +50,11 @@ struct NativeMap : public Countable { folly::dynamic map = folly::dynamic::object; }; -struct ReadableNativeMapKeySeyIterator : public Countable { +struct ReadableNativeMapKeySetIterator : public Countable { folly::dynamic::const_item_iterator iterator; RefPtr mapRef; - ReadableNativeMapKeySeyIterator(folly::dynamic::const_item_iterator&& it, + ReadableNativeMapKeySetIterator(folly::dynamic::const_item_iterator&& it, const RefPtr& mapRef_) : iterator(std::move(it)) , mapRef(mapRef_) {} @@ -490,19 +490,19 @@ namespace iterator { static void initialize(JNIEnv* env, jobject obj, jobject nativeMapObj) { auto nativeMap = extractRefPtr(env, nativeMapObj); - auto mapIterator = createNew( + auto mapIterator = createNew( nativeMap->map.items().begin(), nativeMap); setCountableForJava(env, obj, std::move(mapIterator)); } static jboolean hasNextKey(JNIEnv* env, jobject obj) { - auto nativeIterator = extractRefPtr(env, obj); + auto nativeIterator = extractRefPtr(env, obj); return ((nativeIterator->iterator != nativeIterator->mapRef.get()->map.items().end()) ? JNI_TRUE : JNI_FALSE); } static jstring getNextKey(JNIEnv* env, jobject obj) { - auto nativeIterator = extractRefPtr(env, obj); + auto nativeIterator = extractRefPtr(env, obj); if (JNI_FALSE == hasNextKey(env, obj)) { throwNewJavaException("com/facebook/react/bridge/InvalidIteratorException", "No such element exists"); @@ -520,14 +520,14 @@ namespace runnable { static jclass gNativeRunnableClass; static jmethodID gNativeRunnableCtor; -static jobject createNativeRunnable(JNIEnv* env, decltype(NativeRunnable::callable)&& callable) { - jobject jRunnable = env->NewObject(gNativeRunnableClass, gNativeRunnableCtor); +static LocalReference createNativeRunnable(JNIEnv* env, decltype(NativeRunnable::callable)&& callable) { + LocalReference jRunnable{env->NewObject(gNativeRunnableClass, gNativeRunnableCtor)}; if (env->ExceptionCheck()) { return nullptr; } auto nativeRunnable = createNew(); nativeRunnable->callable = std::move(callable); - setCountableForJava(env, jRunnable, std::move(nativeRunnable)); + setCountableForJava(env, jRunnable.get(), std::move(nativeRunnable)); return jRunnable; } @@ -602,8 +602,8 @@ static void dispatchCallbacksToJava(const RefPtr& weakCallback, } }, std::move(calls)); - jobject jNativeRunnable = runnable::createNativeRunnable(env, std::move(runnableFunction)); - queue::enqueueNativeRunnableOnQueue(env, callbackQueueThread, jNativeRunnable); + auto jNativeRunnable = runnable::createNativeRunnable(env, std::move(runnableFunction)); + queue::enqueueNativeRunnableOnQueue(env, callbackQueueThread, jNativeRunnable.get()); } static void create(JNIEnv* env, jobject obj, jobject executor, jobject callback, @@ -618,6 +618,19 @@ static void create(JNIEnv* env, jobject obj, jobject executor, jobject callback, setCountableForJava(env, obj, std::move(bridge)); } +static void executeApplicationScript( + const RefPtr& bridge, + const std::string script, + const std::string sourceUri) { + try { + // Execute the application script and collect/dispatch any native calls that might have occured + bridge->executeApplicationScript(script, sourceUri); + bridge->executeJSCall("BatchedBridge", "flushedQueue", std::vector()); + } catch (...) { + translatePendingCppExceptionToJavaException(); + } +} + static void loadScriptFromAssets(JNIEnv* env, jobject obj, jobject assetManager, jstring assetName) { jclass markerClass = env->FindClass("com/facebook/react/bridge/ReactMarker"); @@ -625,7 +638,7 @@ static void loadScriptFromAssets(JNIEnv* env, jobject obj, jobject assetManager, auto bridge = extractRefPtr(env, obj); auto assetNameStr = fromJString(env, assetName); - env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_start")); + env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromAssets_start")); auto script = react::loadScriptFromAssets(env, assetManager, assetNameStr); #ifdef WITH_FBSYSTRACE FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "reactbridge_jni_" @@ -633,30 +646,33 @@ static void loadScriptFromAssets(JNIEnv* env, jobject obj, jobject assetManager, "assetName", assetNameStr); #endif - env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_read")); - bridge->executeApplicationScript(script, assetNameStr); - env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_done")); + env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromAssets_read")); + executeApplicationScript(bridge, script, assetNameStr); + if (env->ExceptionCheck()) { + return; + } + env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromAssets_done")); } -static void loadScriptFromNetworkCached(JNIEnv* env, jobject obj, jstring sourceURL, - jstring tempFileName) { +static void loadScriptFromFile(JNIEnv* env, jobject obj, jstring fileName, jstring sourceURL) { jclass markerClass = env->FindClass("com/facebook/react/bridge/ReactMarker"); auto bridge = jni::extractRefPtr(env, obj); - std::string script = ""; - env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_start")); - if (tempFileName != NULL) { - script = react::loadScriptFromFile(jni::fromJString(env, tempFileName)); - } + auto fileNameStr = fileName == NULL ? "" : fromJString(env, fileName); + env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromFile_start")); + auto script = fileName == NULL ? "" : react::loadScriptFromFile(fileNameStr); #ifdef WITH_FBSYSTRACE - auto sourceURLStr = fromJString(env, sourceURL); + auto sourceURLStr = sourceURL == NULL ? fileNameStr : fromJString(env, sourceURL); FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "reactbridge_jni_" "executeApplicationScript", "sourceURL", sourceURLStr); #endif - env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_read")); - bridge->executeApplicationScript(script, jni::fromJString(env, sourceURL)); - env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_exec")); + env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromFile_read")); + executeApplicationScript(bridge, script, jni::fromJString(env, sourceURL)); + if (env->ExceptionCheck()) { + return; + } + env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromFile_exec")); } static void callFunction(JNIEnv* env, jobject obj, jint moduleId, jint methodId, @@ -779,7 +795,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { map::writable::mergeMap) }); - registerNatives("com/facebook/react/bridge/ReadableNativeMap$ReadableNativeMapKeySeyIterator", { + registerNatives("com/facebook/react/bridge/ReadableNativeMap$ReadableNativeMapKeySetIterator", { makeNativeMethod("initialize", "(Lcom/facebook/react/bridge/ReadableNativeMap;)V", map::iterator::initialize), makeNativeMethod("hasNextKey", map::iterator::hasNextKey), @@ -808,7 +824,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { makeNativeMethod( "loadScriptFromAssets", "(Landroid/content/res/AssetManager;Ljava/lang/String;)V", bridge::loadScriptFromAssets), - makeNativeMethod("loadScriptFromNetworkCached", bridge::loadScriptFromNetworkCached), + makeNativeMethod("loadScriptFromFile", bridge::loadScriptFromFile), makeNativeMethod("callFunction", bridge::callFunction), makeNativeMethod("invokeCallback", bridge::invokeCallback), makeNativeMethod("setGlobalVariable", bridge::setGlobalVariable), diff --git a/ReactAndroid/src/main/jni/react/perftests/BUCK b/ReactAndroid/src/main/jni/react/perftests/BUCK new file mode 100644 index 000000000..6358592fb --- /dev/null +++ b/ReactAndroid/src/main/jni/react/perftests/BUCK @@ -0,0 +1,21 @@ +include_defs('//ReactAndroid/DEFS') + +cxx_library( + name = 'perftests', + srcs = [ 'OnLoad.cpp' ], + soname = 'libreactnativetests.so', + preprocessor_flags = [ + '-DLOG_TAG=\"ReactPerftests\"', + ], + visibility = [ + '//instrumentation_tests/com/facebook/catalyst/...', + ], + deps = [ + '//native:base', + '//native/jni:jni', + ], +) + +project_config( + src_target = ':perftests', +) diff --git a/ReactAndroid/src/main/res/BUCK b/ReactAndroid/src/main/res/BUCK new file mode 100644 index 000000000..90f796a13 --- /dev/null +++ b/ReactAndroid/src/main/res/BUCK @@ -0,0 +1,19 @@ +include_defs('//ReactAndroid/DEFS') + +android_resource( + name = 'devsupport', + res = 'devsupport', + package = 'com.facebook.react', + visibility = [ + react_native_target('java/com/facebook/react/devsupport/...'), + ], +) + +android_resource( + name = 'shell', + res = 'shell', + package = 'com.facebook.react', + visibility = [ + 'PUBLIC', + ], +) diff --git a/ReactAndroid/src/main/res/devsupport/layout/redbox_item_frame.xml b/ReactAndroid/src/main/res/devsupport/layout/redbox_item_frame.xml index 45923c1c7..ad23cbf47 100644 --- a/ReactAndroid/src/main/res/devsupport/layout/redbox_item_frame.xml +++ b/ReactAndroid/src/main/res/devsupport/layout/redbox_item_frame.xml @@ -2,14 +2,17 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - android:padding="8dp" + android:paddingTop="8dp" + android:paddingBottom="8dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" > diff --git a/ReactAndroid/src/main/res/devsupport/layout/redbox_item_title.xml b/ReactAndroid/src/main/res/devsupport/layout/redbox_item_title.xml index 95ef0c926..8091fb4d3 100644 --- a/ReactAndroid/src/main/res/devsupport/layout/redbox_item_title.xml +++ b/ReactAndroid/src/main/res/devsupport/layout/redbox_item_title.xml @@ -2,9 +2,9 @@ android:id="@+id/catalyst_redbox_title" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="8dp" + android:padding="16dp" android:gravity="center_vertical" android:textColor="@android:color/white" - android:textSize="14sp" + android:textSize="16sp" android:textStyle="bold" /> diff --git a/ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml b/ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml index faa9a90f0..02c07ba8c 100644 --- a/ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml +++ b/ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml @@ -15,6 +15,7 @@ android:id="@+id/rn_redbox_reloadjs" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_margin="8dp" android:text="@string/catalyst_reloadjs" /> diff --git a/ReactAndroid/src/main/res/devsupport/values/styles.xml b/ReactAndroid/src/main/res/devsupport/values/styles.xml index 86409058f..72f87df96 100644 --- a/ReactAndroid/src/main/res/devsupport/values/styles.xml +++ b/ReactAndroid/src/main/res/devsupport/values/styles.xml @@ -2,7 +2,7 @@