From 253461cb51c807d8abdb2c0567c77f3e2cd69ba9 Mon Sep 17 00:00:00 2001 From: Leonardo YongUk Kim Date: Tue, 26 May 2015 14:12:28 -0700 Subject: [PATCH 0001/2013] Use singlequote instead of doublequoe on Sample.android.js --- Libraries/Sample/Sample.android.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Sample/Sample.android.js b/Libraries/Sample/Sample.android.js index 57cd72400..2024dbdae 100644 --- a/Libraries/Sample/Sample.android.js +++ b/Libraries/Sample/Sample.android.js @@ -10,7 +10,7 @@ var warning = require('warning'); var Sample = { test: function() { - warning("Not yet implemented for Android."); + warning('Not yet implemented for Android.'); } }; From 01d93d294dffa8db4a40d8fe5677180d6895f55c Mon Sep 17 00:00:00 2001 From: Leonardo YongUk Kim Date: Tue, 26 May 2015 18:25:00 -0700 Subject: [PATCH 0002/2013] Remove invariant of Sample.ios.js that is not used --- Libraries/Sample/Sample.ios.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Libraries/Sample/Sample.ios.js b/Libraries/Sample/Sample.ios.js index 884e5305c..fd94afe0a 100644 --- a/Libraries/Sample/Sample.ios.js +++ b/Libraries/Sample/Sample.ios.js @@ -5,7 +5,6 @@ 'use strict'; var NativeSample = require('NativeModules').Sample; -var invariant = require('invariant'); /** * High-level docs for the Sample iOS API can be written here. From 7bb0ff535c828d5720f4ad321fe217e5271abec6 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Thu, 3 Sep 2015 12:02:39 -0700 Subject: [PATCH 0003/2013] [ReactNative][Packager] Fix source maps for minified sources Summary: The packager was ignoring minification for source map requests. --- packager/react-packager/src/Bundler/Bundle.js | 4 ++++ packager/react-packager/src/Server/index.js | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packager/react-packager/src/Bundler/Bundle.js b/packager/react-packager/src/Bundler/Bundle.js index b7920ebb8..88431cbf6 100644 --- a/packager/react-packager/src/Bundler/Bundle.js +++ b/packager/react-packager/src/Bundler/Bundle.js @@ -186,6 +186,10 @@ class Bundle { options = options || {}; + if (options.minify) { + return this.getMinifiedSourceAndMap().map; + } + if (this._shouldCombineSourceMaps) { return this._getCombinedSourceMaps(options); } diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index d727fd52a..7d65d0a2e 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -370,7 +370,9 @@ class Server { res.end(bundleSource); Activity.endEvent(startReqEventId); } else if (requestType === 'map') { - var sourceMap = JSON.stringify(p.getSourceMap()); + var sourceMap = JSON.stringify(p.getSourceMap({ + minify: options.minify, + })); res.setHeader('Content-Type', 'application/json'); res.end(sourceMap); Activity.endEvent(startReqEventId); From 836e4c03fc001b84477acc066ff9510bc9d89d78 Mon Sep 17 00:00:00 2001 From: Chace Liang Date: Thu, 3 Sep 2015 12:19:15 -0700 Subject: [PATCH 0004/2013] [RN][Accessibility] Expose accessibilityTraits and accessibilityComponentType props to Touchable* component --- .../Touchable/TouchableHighlight.js | 2 ++ .../Components/Touchable/TouchableOpacity.js | 2 ++ .../Touchable/TouchableWithoutFeedback.js | 10 +++++++++- Libraries/Components/View/View.js | 19 +++++++++++++------ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index 27ddcdd4e..1850a278c 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -209,6 +209,8 @@ var TouchableHighlight = React.createClass({ return ( Date: Thu, 3 Sep 2015 13:00:09 -0700 Subject: [PATCH 0005/2013] [RN] New AnimationExample --- Examples/UIExplorer/AnimatedExample.js | 230 ++++++++++++++++++ .../AnExApp.js | 0 .../AnExBobble.js | 0 .../AnExChained.js | 0 .../AnExScroll.js | 0 .../AnExSet.js | 0 .../AnExSlides.md | 0 .../AnExTilt.js | 0 Examples/UIExplorer/TimerExample.js | 41 +--- Examples/UIExplorer/UIExplorerButton.js | 56 +++++ Examples/UIExplorer/UIExplorerList.ios.js | 3 +- Libraries/Animated/Easing.js | 6 + 12 files changed, 301 insertions(+), 35 deletions(-) create mode 100644 Examples/UIExplorer/AnimatedExample.js rename Examples/UIExplorer/{AnimationExample => AnimatedGratuitousApp}/AnExApp.js (100%) rename Examples/UIExplorer/{AnimationExample => AnimatedGratuitousApp}/AnExBobble.js (100%) rename Examples/UIExplorer/{AnimationExample => AnimatedGratuitousApp}/AnExChained.js (100%) rename Examples/UIExplorer/{AnimationExample => AnimatedGratuitousApp}/AnExScroll.js (100%) rename Examples/UIExplorer/{AnimationExample => AnimatedGratuitousApp}/AnExSet.js (100%) rename Examples/UIExplorer/{AnimationExample => AnimatedGratuitousApp}/AnExSlides.md (100%) rename Examples/UIExplorer/{AnimationExample => AnimatedGratuitousApp}/AnExTilt.js (100%) create mode 100644 Examples/UIExplorer/UIExplorerButton.js diff --git a/Examples/UIExplorer/AnimatedExample.js b/Examples/UIExplorer/AnimatedExample.js new file mode 100644 index 000000000..e417cacbe --- /dev/null +++ b/Examples/UIExplorer/AnimatedExample.js @@ -0,0 +1,230 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Animated, + Easing, + StyleSheet, + Text, + View, +} = React; +var UIExplorerButton = require('./UIExplorerButton'); + +exports.framework = 'React'; +exports.title = 'Animated - Examples'; +exports.description = 'Animated provides a powerful ' + + 'and easy-to-use API for building modern, ' + + 'interactive user experiences.'; + +exports.examples = [ + { + title: 'FadeInView', + description: 'Uses a simple timing animation to ' + + 'bring opacity from 0 to 1 when the component ' + + 'mounts.', + render: function() { + class FadeInView extends React.Component { + constructor(props) { + super(props); + this.state = { + fadeAnim: new Animated.Value(0), // opacity 0 + }; + } + componentDidMount() { + Animated.timing( // Uses easing functions + this.state.fadeAnim, // The value to drive + { + toValue: 1, // Target + duration: 2000, // Configuration + }, + ).start(); // Don't forget start! + } + render() { + return ( + + {this.props.children} + + ); + } + } + class FadeInExample extends React.Component { + constructor(props) { + super(props); + this.state = { + show: true, + }; + } + render() { + return ( + + { + this.setState((state) => ( + {show: !state.show} + )); + }}> + Press to {this.state.show ? + 'Hide' : 'Show'} + + {this.state.show && + + FadeInView + + } + + ); + } + } + return ; + }, + }, + { + title: 'Transform Bounce', + description: 'One `Animated.Value` is driven by a ' + + 'spring with custom constants and mapped to an ' + + 'ordered set of transforms. Each transform has ' + + 'an interpolation to convert the value into the ' + + 'right range and units.', + render: function() { + this.anim = this.anim || new Animated.Value(0); + return ( + + { + Animated.spring(this.anim, { + toValue: 0, // Returns to the start + velocity: 3, // Velocity makes it move + tension: -10, // Slow + friction: 1, // Oscillate a lot + }).start(); }}> + Press to Fling it! + + + Transforms! + + + ); + }, + }, + { + title: 'Composite Animations with Easing', + description: 'Sequence, parallel, delay, and ' + + 'stagger with different easing functions.', + render: function() { + this.anims = this.anims || [1,2,3].map( + () => new Animated.Value(0) + ); + return ( + + { + var timing = Animated.timing; + Animated.sequence([ // One after the other + timing(this.anims[0], { + toValue: 200, + easing: Easing.linear, + }), + Animated.delay(400), // Use with sequence + timing(this.anims[0], { + toValue: 0, + easing: Easing.elastic(2), // Springy + }), + Animated.delay(400), + Animated.stagger(200, + this.anims.map((anim) => timing( + anim, {toValue: 200} + )).concat( + this.anims.map((anim) => timing( + anim, {toValue: 0} + ))), + ), + Animated.delay(400), + Animated.parallel([ + Easing.inOut(Easing.quad), // Symmetric + Easing.back(1.5), // Goes backwards first + Easing.ease // Default bezier + ].map((easing, ii) => ( + timing(this.anims[ii], { + toValue: 320, easing, duration: 3000, + }) + ))), + Animated.delay(400), + Animated.stagger(200, + this.anims.map((anim) => timing(anim, { + toValue: 0, + easing: Easing.bounce, // Like a ball + duration: 2000, + })), + ), + ]).start(); }}> + Press to Animate + + {['Composite', 'Easing', 'Animations!'].map( + (text, ii) => ( + + {text} + + ) + )} + + ); + }, + }, + { + title: 'Continuous Interactions', + description: 'Gesture events, chaining, 2D ' + + 'values, interrupting and transitioning ' + + 'animations, etc.', + render: () => ( + Checkout the Gratuitous Animation App! + ), + } +]; + +var styles = StyleSheet.create({ + content: { + backgroundColor: 'deepskyblue', + borderWidth: 1, + borderColor: 'dodgerblue', + padding: 20, + margin: 20, + borderRadius: 10, + alignItems: 'center', + }, +}); diff --git a/Examples/UIExplorer/AnimationExample/AnExApp.js b/Examples/UIExplorer/AnimatedGratuitousApp/AnExApp.js similarity index 100% rename from Examples/UIExplorer/AnimationExample/AnExApp.js rename to Examples/UIExplorer/AnimatedGratuitousApp/AnExApp.js diff --git a/Examples/UIExplorer/AnimationExample/AnExBobble.js b/Examples/UIExplorer/AnimatedGratuitousApp/AnExBobble.js similarity index 100% rename from Examples/UIExplorer/AnimationExample/AnExBobble.js rename to Examples/UIExplorer/AnimatedGratuitousApp/AnExBobble.js diff --git a/Examples/UIExplorer/AnimationExample/AnExChained.js b/Examples/UIExplorer/AnimatedGratuitousApp/AnExChained.js similarity index 100% rename from Examples/UIExplorer/AnimationExample/AnExChained.js rename to Examples/UIExplorer/AnimatedGratuitousApp/AnExChained.js diff --git a/Examples/UIExplorer/AnimationExample/AnExScroll.js b/Examples/UIExplorer/AnimatedGratuitousApp/AnExScroll.js similarity index 100% rename from Examples/UIExplorer/AnimationExample/AnExScroll.js rename to Examples/UIExplorer/AnimatedGratuitousApp/AnExScroll.js diff --git a/Examples/UIExplorer/AnimationExample/AnExSet.js b/Examples/UIExplorer/AnimatedGratuitousApp/AnExSet.js similarity index 100% rename from Examples/UIExplorer/AnimationExample/AnExSet.js rename to Examples/UIExplorer/AnimatedGratuitousApp/AnExSet.js diff --git a/Examples/UIExplorer/AnimationExample/AnExSlides.md b/Examples/UIExplorer/AnimatedGratuitousApp/AnExSlides.md similarity index 100% rename from Examples/UIExplorer/AnimationExample/AnExSlides.md rename to Examples/UIExplorer/AnimatedGratuitousApp/AnExSlides.md diff --git a/Examples/UIExplorer/AnimationExample/AnExTilt.js b/Examples/UIExplorer/AnimatedGratuitousApp/AnExTilt.js similarity index 100% rename from Examples/UIExplorer/AnimationExample/AnExTilt.js rename to Examples/UIExplorer/AnimatedGratuitousApp/AnExTilt.js diff --git a/Examples/UIExplorer/TimerExample.js b/Examples/UIExplorer/TimerExample.js index b5923b350..8ef94cafe 100644 --- a/Examples/UIExplorer/TimerExample.js +++ b/Examples/UIExplorer/TimerExample.js @@ -18,27 +18,12 @@ var React = require('react-native'); var { AlertIOS, - StyleSheet, Text, TouchableHighlight, View, } = React; var TimerMixin = require('react-timer-mixin'); - -var Button = React.createClass({ - render: function() { - return ( - - - {this.props.children} - - - ); - }, -}); +var UIExplorerButton = require('./UIExplorerButton'); var TimerTester = React.createClass({ mixins: [TimerMixin], @@ -52,9 +37,9 @@ var TimerTester = React.createClass({ render: function() { var args = 'fn' + (this.props.dt !== undefined ? ', ' + this.props.dt : ''); return ( - + ); }, @@ -112,18 +97,6 @@ var TimerTester = React.createClass({ }, }); -var styles = StyleSheet.create({ - button: { - borderColor: 'gray', - borderRadius: 8, - borderWidth: 1, - padding: 10, - margin: 5, - alignItems: 'center', - justifyContent: 'center', - }, -}); - exports.framework = 'React'; exports.title = 'Timers, TimerMixin'; exports.description = 'The TimerMixin provides timer functions for executing ' + @@ -183,9 +156,9 @@ exports.examples = [ if (this.state.showTimer) { var timer = [ , - + ]; var toggleText = 'Unmount timer'; } else { @@ -195,9 +168,9 @@ exports.examples = [ return ( {timer} - + ); }, diff --git a/Examples/UIExplorer/UIExplorerButton.js b/Examples/UIExplorer/UIExplorerButton.js new file mode 100644 index 000000000..082fe86ba --- /dev/null +++ b/Examples/UIExplorer/UIExplorerButton.js @@ -0,0 +1,56 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + Text, + TouchableHighlight, +} = React; + +var UIExplorerButton = React.createClass({ + propTypes: { + onPress: React.PropTypes.func, + }, + render: function() { + return ( + + + {this.props.children} + + + ); + }, +}); + +var styles = StyleSheet.create({ + button: { + borderColor: 'dimgray', + borderRadius: 8, + borderWidth: 1, + padding: 10, + margin: 5, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'lightgrey', + }, +}); + +module.exports = UIExplorerButton; diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index d45f4ab70..436982dfe 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -60,7 +60,8 @@ var APIS = [ require('./ActionSheetIOSExample'), require('./AdSupportIOSExample'), require('./AlertIOSExample'), - require('./AnimationExample/AnExApp'), + require('./AnimatedExample'), + require('./AnimatedGratuitousApp/AnExApp'), require('./AppStateIOSExample'), require('./AsyncStorageExample'), require('./BorderExample'), diff --git a/Libraries/Animated/Easing.js b/Libraries/Animated/Easing.js index fe40b20b2..0b7b9099d 100644 --- a/Libraries/Animated/Easing.js +++ b/Libraries/Animated/Easing.js @@ -123,12 +123,18 @@ class Easing { return easing; } + /** + * Runs an easing function backwards. + */ static out( easing: (t: number) => number, ): (t: number) => number { return (t) => 1 - easing(1 - t); } + /** + * Makes any easing function symmetrical. + */ static inOut( easing: (t: number) => number, ): (t: number) => number { From e0505fe43ee92ba51a4d641fce63bd73b597910c Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Thu, 3 Sep 2015 12:57:09 -0700 Subject: [PATCH 0006/2013] Added missing init override to RCTRootView --- React/Base/RCTRootView.m | 1 + 1 file changed, 1 insertion(+) diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 930aaa13c..400842288 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -246,6 +246,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) return self; } +RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder:(nonnull NSCoder *)aDecoder) - (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex From 54f91bd951da51bfe908130d71e3469d65d01f1d Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Thu, 3 Sep 2015 14:42:47 -0700 Subject: [PATCH 0007/2013] [react-packager] Socket Server should not die while there active connections Summary: Saw an issue with a build because of an ENONT error: https://fb.facebook.com/groups/716936458354972/permalink/923628747685741/ My hypothesis: 1. We issue a ping to the socket (in SocketInterface/index.js) a decides if the available socket is alive 2. We see that it's alive but by the time we actually connect to it the server would've died Solution: 1. The server shouldn't die as long as there are clients connected to it (currently it only stay alive as long as there are jobs) 2. The "ping" should only disconnect once the client is connected 3. Finally, have a better error message than ENOENT --- .../src/SocketInterface/SocketClient.js | 6 +++++- .../src/SocketInterface/SocketServer.js | 7 ++++++- packager/react-packager/src/SocketInterface/index.js | 12 ++++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packager/react-packager/src/SocketInterface/SocketClient.js b/packager/react-packager/src/SocketInterface/SocketClient.js index e20e5b896..6ccfd4819 100644 --- a/packager/react-packager/src/SocketInterface/SocketClient.js +++ b/packager/react-packager/src/SocketInterface/SocketClient.js @@ -29,7 +29,11 @@ class SocketClient { this._sock = net.connect(sockPath); this._ready = new Promise((resolve, reject) => { this._sock.on('connect', () => resolve(this)); - this._sock.on('error', (e) => reject(e)); + this._sock.on('error', (e) => { + e.message = `Error connecting to server on ${sockPath}` + + `with error: ${e.message}`; + reject(e); + }); }); this._resolvers = Object.create(null); diff --git a/packager/react-packager/src/SocketInterface/SocketServer.js b/packager/react-packager/src/SocketInterface/SocketServer.js index b292b2bfa..abdc094b7 100644 --- a/packager/react-packager/src/SocketInterface/SocketServer.js +++ b/packager/react-packager/src/SocketInterface/SocketServer.js @@ -35,6 +35,8 @@ class SocketServer { process.on('exit', () => fs.unlinkSync(sockPath)); }); }); + + this._numConnections = 0; this._server.on('connection', (sock) => this._handleConnection(sock)); // Disable the file watcher. @@ -50,10 +52,13 @@ class SocketServer { _handleConnection(sock) { debug('connection to server', process.pid); + this._numConnections++; + sock.on('close', () => this._numConnections--); const bunser = new bser.BunserBuf(); sock.on('data', (buf) => bunser.append(buf)); bunser.on('value', (m) => this._handleMessage(sock, m)); + bunser.on('error', (e) => console.error(e)); } _handleMessage(sock, m) { @@ -116,7 +121,7 @@ class SocketServer { _dieEventually() { clearTimeout(this._deathTimer); this._deathTimer = setTimeout(() => { - if (this._jobs <= 0) { + if (this._jobs <= 0 && this._numConnections <= 0) { debug('server dying', process.pid); process.exit(); } diff --git a/packager/react-packager/src/SocketInterface/index.js b/packager/react-packager/src/SocketInterface/index.js index 19b39bf09..f3dce21a0 100644 --- a/packager/react-packager/src/SocketInterface/index.js +++ b/packager/react-packager/src/SocketInterface/index.js @@ -42,8 +42,16 @@ const SocketInterface = { if (fs.existsSync(sockPath)) { var sock = net.connect(sockPath); sock.on('connect', () => { - sock.end(); - resolve(SocketClient.create(sockPath)); + SocketClient.create(sockPath).then( + client => { + sock.end(); + resolve(client); + }, + error => { + sock.end(); + reject(error); + } + ); }); sock.on('error', (e) => { try { From cc1a9fa3f3a555ecb53f259079f04938e1cfca43 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Thu, 3 Sep 2015 19:29:04 -0700 Subject: [PATCH 0008/2013] [RN] Document Animated.js --- Libraries/Animated/Animated.js | 311 +++++++++++++++++++++++++++++---- 1 file changed, 275 insertions(+), 36 deletions(-) diff --git a/Libraries/Animated/Animated.js b/Libraries/Animated/Animated.js index ec4977f3b..1a357f033 100644 --- a/Libraries/Animated/Animated.js +++ b/Libraries/Animated/Animated.js @@ -503,6 +503,12 @@ type ValueListenerCallback = (state: {value: number}) => void; var _uniqueId = 1; +/** + * Standard value for driving animations. One `Animated.Value` can drive + * multiple properties in a synchronized fashion, but can only be driven by one + * mechanism at a time. Using a new mechanism (e.g. starting a new animation, + * or calling `setValue`) will stop any previous ones. + */ class AnimatedValue extends AnimatedWithChildren { _value: number; _offset: number; @@ -526,6 +532,10 @@ class AnimatedValue extends AnimatedWithChildren { return this._value + this._offset; } + /** + * Directly set the value. This will stop any animations running on the value + * and udpate all the bound properties. + */ setValue(value: number): void { if (this._animation) { this._animation.stop(); @@ -534,15 +544,29 @@ class AnimatedValue extends AnimatedWithChildren { this._updateValue(value); } + /** + * Sets an offset that is applied on top of whatever value is set, whether via + * `setValue`, an animation, or `Animated.event`. Useful for compensating + * things like the start of a pan gesture. + */ setOffset(offset: number): void { this._offset = offset; } + /** + * Merges the offset value into the base value and resets the offset to zero. + * The final output of the value is unchanged. + */ flattenOffset(): void { this._value += this._offset; this._offset = 0; } + /** + * Adds an asynchronous listener to the value so you can observe updates from + * animations or whathaveyou. This is useful because there is no way to + * syncronously read the value because it might be driven natively. + */ addListener(callback: ValueListenerCallback): string { var id = String(_uniqueId++); this._listeners[id] = callback; @@ -557,6 +581,30 @@ class AnimatedValue extends AnimatedWithChildren { this._listeners = {}; } + /** + * Stops any running animation or tracking. `callback` is invoked with the + * final value after stopping the animation, which is useful for updating + * state to match the animation position with layout. + */ + stopAnimation(callback?: ?(value: number) => void): void { + this.stopTracking(); + this._animation && this._animation.stop(); + this._animation = null; + callback && callback(this.__getValue()); + } + + /** + * Interpolates the value before updating the property, e.g. mapping 0-1 to + * 0-10. + */ + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, Interpolation.create(config)); + } + + /** + * Typically only used internally, but could be used by a custom Animation + * class. + */ animate(animation: Animation, callback: ?EndCallback): void { var handle = InteractionManager.createInteractionHandle(); var previousAnimation = this._animation; @@ -576,27 +624,22 @@ class AnimatedValue extends AnimatedWithChildren { ); } - stopAnimation(callback?: ?(value: number) => void): void { - this.stopTracking(); - this._animation && this._animation.stop(); - this._animation = null; - callback && callback(this.__getValue()); - } - + /** + * Typically only used internally. + */ stopTracking(): void { this._tracking && this._tracking.__detach(); this._tracking = null; } + /** + * Typically only used internally. + */ track(tracking: Animated): void { this.stopTracking(); this._tracking = tracking; } - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, Interpolation.create(config)); - } - _updateValue(value: number): void { this._value = value; _flush(this); @@ -607,6 +650,45 @@ class AnimatedValue extends AnimatedWithChildren { } type ValueXYListenerCallback = (value: {x: number; y: number}) => void; + +/** + * 2D Value for driving 2D animations, such as pan gestures. Almost identical + * API to normal `Animated.Value`, but multiplexed. Contains two regular + * `Animated.Value`s under the hood. Example: + * + *```javascript + * class DraggableView extends React.Component { + * constructor(props) { + * super(props); + * this.state = { + * pan: new Animated.ValueXY(), // inits to zero + * }; + * this.state.panResponder = PanResponder.create({ + * onStartShouldSetPanResponder: () => true, + * onPanResponderMove: Animated.event([null, { + * dx: this.state.pan.x, // x,y are Animated.Value + * dy: this.state.pan.y, + * }]), + * onPanResponderRelease: () => { + * Animated.spring( + * this.state.pan, // Auto-multiplexed + * {toValue: {x: 0, y: 0}} // Back to zero + * ).start(); + * }, + * }); + * } + * render() { + * return ( + * + * {this.props.children} + * + * ); + * } + * } + *``` + */ class AnimatedValueXY extends AnimatedWithChildren { x: AnimatedValue; y: AnimatedValue; @@ -677,6 +759,13 @@ class AnimatedValueXY extends AnimatedWithChildren { delete this._listeners[id]; } + /** + * Converts `{x, y}` into `{left, top}` for use in style, e.g. + * + *```javascript + * style={this.state.anim.getLayout()} + *``` + */ getLayout(): {[key: string]: AnimatedValue} { return { left: this.x, @@ -684,6 +773,15 @@ class AnimatedValueXY extends AnimatedWithChildren { }; } + /** + * Converts `{x, y}` into a useable translation transform, e.g. + * + *```javascript + * style={{ + * transform: this.state.anim.getTranslateTransform() + * }} + *``` + */ getTranslateTransform(): Array<{[key: string]: AnimatedValue}> { return [ {translateX: this.x}, @@ -1235,21 +1333,6 @@ var stagger = function( type Mapping = {[key: string]: Mapping} | AnimatedValue; -/** - * Takes an array of mappings and extracts values from each arg accordingly, - * then calls setValue on the mapped outputs. e.g. - * - * onScroll={this.AnimatedEvent( - * [{nativeEvent: {contentOffset: {x: this._scrollX}}}] - * {listener} // optional listener invoked asynchronously - * ) - * ... - * onPanResponderMove: this.AnimatedEvent([ - * null, // raw event arg - * {dx: this._panX}, // gestureState arg - * ]), - * - */ type EventConfig = {listener?: ?Function}; var event = function( argMapping: Array, @@ -1287,23 +1370,179 @@ var event = function( }; }; +/** + * Animations are an important part of modern UX, and the `Animated` + * library is designed to make them fluid, powerful, and easy to build and + * maintain. + * + * The simplest workflow is to create an `Animated.Value`, hook it up to one or + * more style attributes of an animated component, and then drive updates either + * via animations, such as `Animated.timing`, or by hooking into gestures like + * panning or scolling via `Animated.event`. `Animated.Value` can also bind to + * props other than style, and can be interpolated as well. Here is a basic + * example of a container view that will fade in when it's mounted: + * + *```javascript + * class FadeInView extends React.Component { + * constructor(props) { + * super(props); + * this.state = { + * fadeAnim: new Animated.Value(0), // init opacity 0 + * }; + * } + * componentDidMount() { + * Animated.timing( // Uses easing functions + * this.state.fadeAnim, // The value to drive + * {toValue: 1}, // Configuration + * ).start(); // Don't forget start! + * } + * render() { + * return ( + * // Binds + * {this.props.children} + * + * ); + * } + * } + *``` + * + * Note that only animatable components can be animated. `View`, `Text`, and + * `Image` are already provided, and you can create custom ones with + * `createAnimatedComponent`. These special components do the magic of binding + * the animated values to the properties, and do targetted native updates to + * avoid the cost of the react render and reconciliation process on every frame. + * They also handle cleanup on unmount so they are safe by default. + * + * Animations are heavily configurable. Custom and pre-defined easing + * functions, delays, durations, decay factors, spring constants, and more can + * all be tweaked depending on the type of animation. + * + * A single `Animated.Value` can drive any number of properties, and each + * property can be run through an interpolation first. An interpolation maps + * input ranges to output ranges, typically using a linear interpolation but + * also supports easing functions. By default, it will extrapolate the curve + * beyond the ranges given, but you can also have it clamp the output value. + * + * For example, you may want to think about your `Animated.Value` as going from + * 0 to 1, but animate the position from 150px to 0px and the opacity from 0 to + * 1. This can easily be done by modifying `style` in the example above like so: + * + *```javascript + * style={{ + * opacity: this.state.fadeAnim, // Binds directly + * transform: [{ + * translateY: this.state.fadeAnim.interpolate({ + * inputRange: [0, 1], + * outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0 + * }), + * }], + * }}> + *``` + * + * Animations can also be combined in complex ways using composition functions + * such as `sequence` and `parallel`, and can also be chained together simply + * by setting the `toValue` of one animation to be another `Animated.Value`. + * + * `Animated.ValueXY` is handy for 2D animations, like panning, and there are + * other helpful additions like `setOffset` and `getLayout` to aid with typical + * interaction patterns, like drag-and-drop. + * + * You can see more example usage in `AnimationExample.js`, the Gratuitous + * Animation App, and [Animations documentation guide](http://facebook.github.io/react-native/docs/animations.html). + * + * Note that `Animated` is designed to be fully serializable so that animations + * can be run in a high performace way, independent of the normal JavaScript + * event loop. This does influence the API, so keep that in mind when it seems a + * little trickier to do something compared to a fully synchronous system. + * Checkout `Animated.Value.addListener` as a way to work around some of these + * limitations, but use it sparingly since it might have performance + * implications in the future. + */ module.exports = { - delay, - sequence, - parallel, - stagger, + /** + * Standard value class for driving animations. Typically initialized with + * `new Animated.Value(0);` + */ + Value: AnimatedValue, + /** + * 2D value class for driving 2D animations, such as pan gestures. + */ + ValueXY: AnimatedValueXY, + /** + * An animatable View component. + */ + View: createAnimatedComponent(View), + /** + * An animatable Text component. + */ + Text: createAnimatedComponent(Text), + /** + * An animatable Image component. + */ + Image: createAnimatedComponent(Image), + + /** + * Animates a value from an initial velocity to zero based on a decay + * coefficient. + */ decay, + /** + * Animates a value along a timed easing curve. The `Easing` module has tons + * of pre-defined curves, or you can use your own function. + */ timing, + /** + * Spring animation based on Rebound and Origami. Tracks velocity state to + * create fluid motions as the `toValue` updates, and can be chained together. + */ spring, + /** + * Starts an animation after the given delay. + */ + delay, + /** + * Starts an array of animations in order, waiting for each to complete + * before starting the next. If the current running animation is stopped, no + * following animations will be started. + */ + sequence, + /** + * Starts an array of animations all at the same time. By default, if one + * of the animations is stopped, they will all be stopped. You can override + * this with the `stopTogether` flag. + */ + parallel, + /** + * Array of animations may run in parallel (overlap), but are started in + * sequence with successive delays. Nice for doing trailing effects. + */ + stagger, + + /** + * Takes an array of mappings and extracts values from each arg accordingly, + * then calls `setValue` on the mapped outputs. e.g. + * + *```javascript + * onScroll={this.AnimatedEvent( + * [{nativeEvent: {contentOffset: {x: this._scrollX}}}] + * {listener}, // Optional async listener + * ) + * ... + * onPanResponderMove: this.AnimatedEvent([ + * null, // raw event arg ignored + * {dx: this._panX}, // gestureState arg + * ]), + *``` + */ event, - Value: AnimatedValue, - ValueXY: AnimatedValueXY, - __PropsOnlyForTests: AnimatedProps, - View: createAnimatedComponent(View), - Text: createAnimatedComponent(Text), - Image: createAnimatedComponent(Image), + /** + * Make any React component Animatable. Used to create `Animated.View`, etc. + */ createAnimatedComponent, + + __PropsOnlyForTests: AnimatedProps, }; From 44fec06891de5d111e004399c467bdec3dca2c51 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 4 Sep 2015 03:21:58 -0700 Subject: [PATCH 0009/2013] Fix example apps to use new packager paths --- Examples/2048/2048/AppDelegate.m | 2 +- Examples/Movies/Movies/AppDelegate.m | 2 +- Examples/SampleApp/iOS/SampleApp/AppDelegate.m | 2 +- Examples/TicTacToe/TicTacToe/AppDelegate.m | 2 +- Examples/UIExplorer/UIExplorer/AppDelegate.m | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Examples/2048/2048/AppDelegate.m b/Examples/2048/2048/AppDelegate.m index 2413b3552..b4b1769ff 100644 --- a/Examples/2048/2048/AppDelegate.m +++ b/Examples/2048/2048/AppDelegate.m @@ -36,7 +36,7 @@ * on the same Wi-Fi network. */ - jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/2048/Game2048.bundle?platform=ios"]; + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/2048/Game2048.bundle?platform=ios&dev=true"]; /** * OPTION 2 diff --git a/Examples/Movies/Movies/AppDelegate.m b/Examples/Movies/Movies/AppDelegate.m index 719cdca66..4d322023f 100644 --- a/Examples/Movies/Movies/AppDelegate.m +++ b/Examples/Movies/Movies/AppDelegate.m @@ -37,7 +37,7 @@ * on the same Wi-Fi network. */ - jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/Movies/MoviesApp.includeRequire.runModule.bundle"]; + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/Movies/MoviesApp.ios.bundle?platform=ios&dev=true"]; /** * OPTION 2 diff --git a/Examples/SampleApp/iOS/SampleApp/AppDelegate.m b/Examples/SampleApp/iOS/SampleApp/AppDelegate.m index b1c018ca5..d49b891a2 100644 --- a/Examples/SampleApp/iOS/SampleApp/AppDelegate.m +++ b/Examples/SampleApp/iOS/SampleApp/AppDelegate.m @@ -31,7 +31,7 @@ * on the same Wi-Fi network. */ - jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/SampleApp/index.ios.bundle"]; + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/SampleApp/index.ios.bundle?platform=ios&dev=true"]; /** * OPTION 2 diff --git a/Examples/TicTacToe/TicTacToe/AppDelegate.m b/Examples/TicTacToe/TicTacToe/AppDelegate.m index aa746ba69..f0199b6dd 100644 --- a/Examples/TicTacToe/TicTacToe/AppDelegate.m +++ b/Examples/TicTacToe/TicTacToe/AppDelegate.m @@ -36,7 +36,7 @@ * on the same Wi-Fi network. */ - jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/TicTacToe/TicTacToeApp.bundle?platform=ios"]; + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/TicTacToe/TicTacToeApp.bundle?platform=ios&dev=true"]; /** * OPTION 2 diff --git a/Examples/UIExplorer/UIExplorer/AppDelegate.m b/Examples/UIExplorer/UIExplorer/AppDelegate.m index 24ada2b56..1d0d0079c 100644 --- a/Examples/UIExplorer/UIExplorer/AppDelegate.m +++ b/Examples/UIExplorer/UIExplorer/AppDelegate.m @@ -59,7 +59,7 @@ * on the same Wi-Fi network. */ - sourceURL = [NSURL URLWithString:@"http://localhost:8081/Examples/UIExplorer/UIExplorerApp.ios.bundle?dev=true"]; + sourceURL = [NSURL URLWithString:@"http://localhost:8081/Examples/UIExplorer/UIExplorerApp.ios.bundle?platform=ios&dev=true"]; /** * OPTION 2 From 9b1f6c9e3062e7425dfc39b03f7b7f3d269c0978 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 4 Sep 2015 03:21:57 -0700 Subject: [PATCH 0010/2013] Make RCTTestRunner wait for JS context to deallocate --- Libraries/RCTTest/RCTTestRunner.m | 102 +++++++++++++++++------------- React/Base/RCTRootView.m | 1 - 2 files changed, 57 insertions(+), 46 deletions(-) diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index b0c932bc1..41316360b 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -16,13 +16,8 @@ #import "RCTTestModule.h" #import "RCTUtils.h" -#define TIMEOUT_SECONDS 60 - -@interface RCTBridge (RCTTestRunner) - -@property (nonatomic, weak) RCTBridge *batchedBridge; - -@end +static const NSTimeInterval kTestTimeoutSeconds = 60; +static const NSTimeInterval kTestTeardownTimeoutSeconds = 30; @implementation RCTTestRunner { @@ -49,7 +44,7 @@ _scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"]; RCTAssert(_scriptURL != nil, @"Could not locate main.jsBundle"); #else - _scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?dev=true&platform=ios", app]]; + _scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?platform=ios&dev=true", app]]; #endif } return self; @@ -83,52 +78,69 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock { - __block NSString *error = nil; - RCTSetLogFunction(^(RCTLogLevel level, NSString *fileName, NSNumber *lineNumber, NSString *message) { - if (level >= RCTLogLevelError) { - error = message; + __weak id weakJSContext; + + @autoreleasepool { + __block NSString *error = nil; + RCTSetLogFunction(^(RCTLogLevel level, NSString *fileName, NSNumber *lineNumber, NSString *message) { + if (level >= RCTLogLevelError) { + error = message; + } + }); + + RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_scriptURL + moduleProvider:_moduleProvider + launchOptions:nil]; + + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:initialProps]; + rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices + + NSString *testModuleName = RCTBridgeModuleNameForClass([RCTTestModule class]); + RCTTestModule *testModule = rootView.bridge.modules[testModuleName]; + RCTAssert(_testController != nil, @"_testController should not be nil"); + testModule.controller = _testController; + testModule.testSelector = test; + testModule.view = rootView; + + UIViewController *vc = [UIApplication sharedApplication].delegate.window.rootViewController; + vc.view = [UIView new]; + [vc.view addSubview:rootView]; // Add as subview so it doesn't get resized + + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:kTestTimeoutSeconds]; + while (date.timeIntervalSinceNow > 0 && testModule.status == RCTTestStatusPending && error == nil) { + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; } - }); - RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_scriptURL - moduleProvider:_moduleProvider - launchOptions:nil]; + // Take a weak reference to the JS context, so we track its deallocation later + // (we can only do this now, since it's been lazily initialized) + weakJSContext = [[[bridge valueForKey:@"batchedBridge"] valueForKey:@"javaScriptExecutor"] valueForKey:@"context"]; + [rootView removeFromSuperview]; - RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:initialProps]; - rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices + RCTSetLogFunction(RCTDefaultLogFunction); - NSString *testModuleName = RCTBridgeModuleNameForClass([RCTTestModule class]); - RCTTestModule *testModule = rootView.bridge.batchedBridge.modules[testModuleName]; - RCTAssert(_testController != nil, @"_testController should not be nil"); - testModule.controller = _testController; - testModule.testSelector = test; - testModule.view = rootView; + 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); - UIViewController *vc = [UIApplication sharedApplication].delegate.window.rootViewController; - vc.view = [UIView new]; - [vc.view addSubview:rootView]; // Add as subview so it doesn't get resized + if (expectErrorBlock) { + RCTAssert(expectErrorBlock(error), @"Expected an error but nothing matched."); + } else { + RCTAssert(error == nil, @"RedBox error: %@", error); + RCTAssert(testModule.status != RCTTestStatusPending, @"Test didn't finish within %0.f seconds", kTestTimeoutSeconds); + RCTAssert(testModule.status == RCTTestStatusPassed, @"Test failed"); + } + [bridge invalidate]; + } - NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; - while (date.timeIntervalSinceNow > 0 && testModule.status == RCTTestStatusPending && error == nil) { + // Wait for the executor to have shut down completely before returning + NSDate *teardownTimeout = [NSDate dateWithTimeIntervalSinceNow:kTestTeardownTimeoutSeconds]; + while (teardownTimeout.timeIntervalSinceNow > 0 && weakJSContext) { [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; } - [rootView removeFromSuperview]; - - RCTSetLogFunction(RCTDefaultLogFunction); - - 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); - - if (expectErrorBlock) { - RCTAssert(expectErrorBlock(error), @"Expected an error but nothing matched."); - } else { - RCTAssert(error == nil, @"RedBox error: %@", error); - RCTAssert(testModule.status != RCTTestStatusPending, @"Test didn't finish within %d seconds", TIMEOUT_SECONDS); - RCTAssert(testModule.status == RCTTestStatusPassed, @"Test failed"); - } + RCTAssert(!weakJSContext, @"JS context was not deallocated after being invalidated"); } @end diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 400842288..15ed227af 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -13,7 +13,6 @@ #import "RCTAssert.h" #import "RCTBridge.h" -#import "RCTContextExecutor.h" #import "RCTEventDispatcher.h" #import "RCTKeyCommands.h" #import "RCTLog.h" From 3f4c7e40c6d9353fb59a765fb87ab7612a18048b Mon Sep 17 00:00:00 2001 From: James Ide Date: Fri, 4 Sep 2015 03:19:30 -0700 Subject: [PATCH 0011/2013] [Bridge] Consistently post "DidFailToLoad" notification when there's an error Summary: Previously the bridge sometimes never fired RCTJavaScriptDidLoadNotification or RCTJavaScriptDidFailToLoadNotification if there was an error (for example, if the source code loaded but we couldn't inject the JSON config). This diff moves the error handling into a method called `stopLoadingWithError` that the bridge can call whenever there is an error. Also if the script failed to load, the BatchedBridge still called `executeSourceCode`. With this diff the `_loading` flag is set to NO when the script fails to load, and `executeSourceCode` returns immediately when `_loading` is false. This way the bridge does not try to execute JS when there is a loading error. Closes https://github.com/facebook/react-native/pull/2520 Github Author: James Ide --- React/Base/RCTBatchedBridge.m | 77 +++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 3151d8005..482e213fc 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -117,8 +117,15 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); dispatch_group_t initModulesAndLoadSource = dispatch_group_create(); dispatch_group_enter(initModulesAndLoadSource); + __weak RCTBatchedBridge *weakSelf = self; __block NSString *sourceCode; - [self loadSource:^(__unused NSError *error, NSString *source) { + [self loadSource:^(NSError *error, NSString *source) { + if (error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf stopLoadingWithError:error]; + }); + } + sourceCode = source; dispatch_group_leave(initModulesAndLoadSource); }]; @@ -131,7 +138,6 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); RCTProfileHookModules(self); } - __weak RCTBatchedBridge *weakSelf = self; __block NSString *config; dispatch_group_enter(initModulesAndLoadSource); dispatch_async(bridgeQueue, ^{ @@ -150,16 +156,24 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); // We're not waiting for this complete to leave the dispatch group, since // injectJSONConfiguration and executeSourceCode will schedule operations on the // same queue anyway. - [weakSelf injectJSONConfiguration:config onComplete:^(__unused NSError *error) { + [weakSelf injectJSONConfiguration:config onComplete:^(NSError *error) { RCTPerformanceLoggerEnd(RCTPLNativeModuleInit); + if (error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf stopLoadingWithError:error]; + }); + } }]; dispatch_group_leave(initModulesAndLoadSource); }); }); - dispatch_group_notify(initModulesAndLoadSource, bridgeQueue, ^{ - if (sourceCode) { - [weakSelf executeSourceCode:sourceCode]; + dispatch_group_notify(initModulesAndLoadSource, dispatch_get_main_queue(), ^{ + RCTBatchedBridge *strongSelf = weakSelf; + if (sourceCode && strongSelf.loading) { + dispatch_async(bridgeQueue, ^{ + [weakSelf executeSourceCode:sourceCode]; + }); } }); } @@ -172,23 +186,6 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); RCTSourceLoadBlock onSourceLoad = ^(NSError *error, NSString *source) { RCTProfileEndAsyncEvent(0, @"init,download", cookie, @"JavaScript download", nil); RCTPerformanceLoggerEnd(RCTPLScriptDownload); - - if (error) { - NSArray *stack = error.userInfo[@"stack"]; - if (stack) { - [self.redBox showErrorMessage:error.localizedDescription - withStack:stack]; - } else { - [self.redBox showErrorMessage:error.localizedDescription - withDetails:error.localizedFailureReason]; - } - - NSDictionary *userInfo = @{@"bridge": self, @"error": error}; - [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification - object:_parentBridge - userInfo:userInfo]; - } - _onSourceLoad(error, source); }; @@ -283,7 +280,6 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); object:self]; } - - (void)setupExecutor { [_javaScriptExecutor setUp]; @@ -313,12 +309,7 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); [_javaScriptExecutor injectJSONText:configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig" - callback:^(NSError *error) { - if (error) { - [self.redBox showError:error]; - } - onComplete(error); - }]; + callback:onComplete]; } - (void)executeSourceCode:(NSString *)sourceCode @@ -333,7 +324,9 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); [self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:^(NSError *loadError) { if (loadError) { - [self.redBox showError:loadError]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self stopLoadingWithError:loadError]; + }); return; } @@ -352,6 +345,28 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); }]; } +- (void)stopLoadingWithError:(NSError *)error +{ + RCTAssertMainThread(); + + if (!self.isValid || !self.loading) { + return; + } + + _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]; +} RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleURL moduleProvider:(__unused RCTBridgeModuleProviderBlock)block From 7a6f116ed49b9671a2245aff878c1126f4798e7b Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 4 Sep 2015 03:30:18 -0700 Subject: [PATCH 0012/2013] Fix React Native test configuration --- ...onTests.m => UIExplorerIntegrationTests.m} | 55 +++++-------------- 1 file changed, 15 insertions(+), 40 deletions(-) rename Examples/UIExplorer/UIExplorerIntegrationTests/{IntegrationTests.m => UIExplorerIntegrationTests.m} (64%) diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m similarity index 64% rename from Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m rename to Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m index 267ed1409..9bc71ed80 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m @@ -14,11 +14,17 @@ #import "RCTAssert.h" -@interface IntegrationTests : XCTestCase +#define RCT_TEST(name) \ +- (void)test##name \ +{ \ + [_runner runTest:_cmd module:@#name]; \ +} + +@interface UIExplorerIntegrationTests : XCTestCase @end -@implementation IntegrationTests +@implementation UIExplorerIntegrationTests { RCTTestRunner *_runner; } @@ -36,11 +42,6 @@ #pragma mark Logic Tests -- (void)testTheTester -{ - [_runner runTest:_cmd module:@"IntegrationTestHarnessTest"]; -} - - (void)testTheTester_waitOneFrame { [_runner runTest:_cmd @@ -57,38 +58,12 @@ expectErrorRegex:@"because shouldThrow"]; } -- (void)testTimers -{ - [_runner runTest:_cmd module:@"TimersTest"]; -} - -- (void)testAsyncStorage -{ - [_runner runTest:_cmd module:@"AsyncStorageTest"]; -} - -- (void)DISABLED_testLayoutEvents // #7149037 -{ - [_runner runTest:_cmd module:@"LayoutEventsTest"]; -} - -- (void)testAppEvents -{ - [_runner runTest:_cmd module:@"AppEventsTest"]; -} - -- (void)testPromises -{ - [_runner runTest:_cmd module:@"PromiseTest"]; -} - -#pragma mark Snapshot Tests - -- (void)testSimpleSnapshot -{ - // If tests have changes, set recordMode = YES below and re-run - _runner.recordMode = NO; - [_runner runTest:_cmd module:@"SimpleSnapshotTest"]; -} +RCT_TEST(TimersTest) +RCT_TEST(IntegrationTestHarnessTest) +RCT_TEST(AsyncStorageTest) +// RCT_TEST(LayoutEventsTest) -- Disabled: #8153468 +RCT_TEST(AppEventsTest) +RCT_TEST(PromiseTest) +// RCT_TEST(SimpleSnapshotTest) -- Disabled: #8153475 @end From 58661978a750bfca9931c9f4244fa63d8c160cd0 Mon Sep 17 00:00:00 2001 From: Tj Date: Fri, 4 Sep 2015 03:44:55 -0700 Subject: [PATCH 0013/2013] [RCTDevLoadingView] Add ability to disable during development. Summary: I'd like this ability as this has a tendency to get in the way of some of the more complex UI pieces I have. Disabling RCT_DEV entirely is too much for me. Closes https://github.com/facebook/react-native/pull/2451 Github Author: Tj --- React/Modules/RCTDevLoadingView.h | 2 ++ React/Modules/RCTDevLoadingView.m | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/React/Modules/RCTDevLoadingView.h b/React/Modules/RCTDevLoadingView.h index 85a43a369..b09315fb6 100644 --- a/React/Modules/RCTDevLoadingView.h +++ b/React/Modules/RCTDevLoadingView.h @@ -11,4 +11,6 @@ @interface RCTDevLoadingView : NSObject ++ (void)setEnabled:(BOOL)enabled; + @end diff --git a/React/Modules/RCTDevLoadingView.m b/React/Modules/RCTDevLoadingView.m index b716b2483..a0266b7ce 100644 --- a/React/Modules/RCTDevLoadingView.m +++ b/React/Modules/RCTDevLoadingView.m @@ -16,6 +16,8 @@ #if RCT_DEV +static BOOL isEnabled = YES; + @implementation RCTDevLoadingView { UIWindow *_window; @@ -27,6 +29,11 @@ RCT_EXPORT_MODULE() ++ (void)setEnabled:(BOOL)enabled +{ + isEnabled = enabled; +} + - (instancetype)init { if ((self = [super init])) { @@ -57,6 +64,10 @@ RCT_EXPORT_MODULE() - (void)showWithURL:(NSURL *)URL { + if (!isEnabled) { + return; + } + dispatch_async(dispatch_get_main_queue(), ^{ _showDate = [NSDate date]; @@ -90,6 +101,10 @@ RCT_EXPORT_MODULE() - (void)hide { + if (!isEnabled) { + return; + } + dispatch_async(dispatch_get_main_queue(), ^{ const NSTimeInterval MIN_PRESENTED_TIME = 0.6; @@ -117,6 +132,7 @@ RCT_EXPORT_MODULE() @implementation RCTDevLoadingView + (NSString *)moduleName { return nil; } ++ (void)setEnabled:(BOOL)enabled { } @end From 3c4adeb2e730bb3e252d1611d9da2c777d63998f Mon Sep 17 00:00:00 2001 From: Aaron Chiu Date: Fri, 4 Sep 2015 10:50:30 -0100 Subject: [PATCH 0014/2013] [ReactNative][SyncDiff] Hook in the Dialog.onDismiss to JS --- Libraries/Modal/Modal.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js index 2063f0fdd..8e3a88665 100644 --- a/Libraries/Modal/Modal.js +++ b/Libraries/Modal/Modal.js @@ -33,6 +33,7 @@ class Modal extends React.Component { {this.props.children} @@ -45,6 +46,7 @@ class Modal extends React.Component { Modal.propTypes = { animated: PropTypes.bool, transparent: PropTypes.bool, + onDismiss: PropTypes.func, }; var styles = StyleSheet.create({ From e4110456abb29a5d53814e8626e8dcb6161089d2 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Fri, 4 Sep 2015 04:35:44 -0700 Subject: [PATCH 0015/2013] Changed RCTImageLoader to always return a UIImage Summary: GIF images are currently loaded as a CAKeyframeAnimation, however returning this animation directly from RCTImageLoader was dangerous, as any code that expected a UIImage would crash. This diff changes RCTGIFImageLoader to return a UIImage of the first frame, with the keyframe animation attached as an associated object. This way, code that is not expecting an animation will still work correctly. --- Examples/UIExplorer/ImageExample.js | 18 ++++- Libraries/Image/RCTGIFImageDecoder.m | 107 +++++++++++++++++---------- Libraries/Image/RCTImageDownloader.m | 2 +- Libraries/Image/RCTImageLoader.h | 9 ++- Libraries/Image/RCTImageLoader.m | 18 ++++- Libraries/Image/RCTImageView.m | 6 +- 6 files changed, 110 insertions(+), 50 deletions(-) diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js index cea7b5511..5e47dcb54 100644 --- a/Examples/UIExplorer/ImageExample.js +++ b/Examples/UIExplorer/ImageExample.js @@ -325,6 +325,18 @@ exports.examples = [ ); }, }, + { + title: 'Animated GIF', + render: function() { + return ( + + ); + }, + platform: 'ios', + }, { title: 'Cap Insets', description: @@ -384,5 +396,9 @@ var styles = StyleSheet.create({ }, horizontal: { flexDirection: 'row', - } + }, + gif: { + flex: 1, + height: 200, + }, }); diff --git a/Libraries/Image/RCTGIFImageDecoder.m b/Libraries/Image/RCTGIFImageDecoder.m index 3d85f94e7..332769e7d 100644 --- a/Libraries/Image/RCTGIFImageDecoder.m +++ b/Libraries/Image/RCTGIFImageDecoder.m @@ -27,60 +27,85 @@ RCT_EXPORT_MODULE() return !strcmp(header, "GIF87a") || !strcmp(header, "GIF89a"); } -- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode completionHandler:(RCTImageLoaderCompletionBlock)completionHandler +- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(UIViewContentMode)resizeMode + completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, NULL); NSUInteger loopCount = [properties[(id)kCGImagePropertyGIFDictionary][(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue]; + UIImage *image = nil; size_t imageCount = CGImageSourceGetCount(imageSource); - NSTimeInterval duration = 0; - NSMutableArray *delays = [NSMutableArray arrayWithCapacity:imageCount]; - NSMutableArray *images = [NSMutableArray arrayWithCapacity:imageCount]; - for (size_t i = 0; i < imageCount; i++) { - CGImageRef image = CGImageSourceCreateImageAtIndex(imageSource, i, NULL); - NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, i, NULL); - NSDictionary *frameGIFProperties = frameProperties[(id)kCGImagePropertyGIFDictionary]; + if (imageCount > 1) { - const NSTimeInterval kDelayTimeIntervalDefault = 0.1; - NSNumber *delayTime = frameGIFProperties[(id)kCGImagePropertyGIFUnclampedDelayTime] ?: frameGIFProperties[(id)kCGImagePropertyGIFDelayTime]; - if (delayTime == nil) { - if (i == 0) { - delayTime = @(kDelayTimeIntervalDefault); - } else { - delayTime = delays[i - 1]; + NSTimeInterval duration = 0; + NSMutableArray *delays = [NSMutableArray arrayWithCapacity:imageCount]; + NSMutableArray *images = [NSMutableArray arrayWithCapacity:imageCount]; + for (size_t i = 0; i < imageCount; i++) { + + CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, NULL); + if (!image) { + image = [UIImage imageWithCGImage:imageRef]; } + + NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, i, NULL); + NSDictionary *frameGIFProperties = frameProperties[(id)kCGImagePropertyGIFDictionary]; + + const NSTimeInterval kDelayTimeIntervalDefault = 0.1; + NSNumber *delayTime = frameGIFProperties[(id)kCGImagePropertyGIFUnclampedDelayTime] ?: frameGIFProperties[(id)kCGImagePropertyGIFDelayTime]; + if (delayTime == nil) { + if (i == 0) { + delayTime = @(kDelayTimeIntervalDefault); + } else { + delayTime = delays[i - 1]; + } + } + + const NSTimeInterval kDelayTimeIntervalMinimum = 0.02; + if (delayTime.floatValue < (float)kDelayTimeIntervalMinimum - FLT_EPSILON) { + delayTime = @(kDelayTimeIntervalDefault); + } + + duration += delayTime.doubleValue; + delays[i] = delayTime; + images[i] = (__bridge_transfer id)imageRef; + } + CFRelease(imageSource); + + NSMutableArray *keyTimes = [NSMutableArray arrayWithCapacity:delays.count]; + NSTimeInterval runningDuration = 0; + for (NSNumber *delayNumber in delays) { + [keyTimes addObject:@(runningDuration / duration)]; + runningDuration += delayNumber.doubleValue; } - const NSTimeInterval kDelayTimeIntervalMinimum = 0.02; - if (delayTime.floatValue < (float)kDelayTimeIntervalMinimum - FLT_EPSILON) { - delayTime = @(kDelayTimeIntervalDefault); + [keyTimes addObject:@1.0]; + + // Create animation + CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"]; + animation.calculationMode = kCAAnimationDiscrete; + animation.repeatCount = loopCount == 0 ? HUGE_VALF : loopCount; + animation.keyTimes = keyTimes; + animation.values = images; + animation.duration = duration; + image.reactKeyframeAnimation = animation; + + } else { + + // Don't bother creating an animation + CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); + if (imageRef) { + image = [UIImage imageWithCGImage:imageRef]; + CFRelease(imageRef); } - - duration += delayTime.doubleValue; - delays[i] = delayTime; - images[i] = (__bridge_transfer id)image; - } - CFRelease(imageSource); - - NSMutableArray *keyTimes = [NSMutableArray arrayWithCapacity:delays.count]; - NSTimeInterval runningDuration = 0; - for (NSNumber *delayNumber in delays) { - [keyTimes addObject:@(runningDuration / duration)]; - runningDuration += delayNumber.doubleValue; + CFRelease(imageSource); } - [keyTimes addObject:@1.0]; - - CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"]; - animation.calculationMode = kCAAnimationDiscrete; - animation.repeatCount = loopCount == 0 ? HUGE_VALF : loopCount; - animation.keyTimes = keyTimes; - animation.values = images; - animation.duration = duration; - completionHandler(nil, animation); - - return nil; + completionHandler(nil, image); + return ^{}; } @end diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m index f110bd628..c2aa89f73 100644 --- a/Libraries/Image/RCTImageDownloader.m +++ b/Libraries/Image/RCTImageDownloader.m @@ -50,7 +50,7 @@ RCT_EXPORT_MODULE() */ - (RCTImageLoaderCancellationBlock)downloadDataForURL:(NSURL *)url progressHandler:(RCTImageLoaderProgressBlock)progressBlock - completionHandler:(RCTImageLoaderCompletionBlock)completionBlock + completionHandler:(void (^)(NSError *error, NSData *data))completionBlock { if (![_bridge respondsToSelector:NSSelectorFromString(@"networking")]) { RCTLogError(@"You need to import the RCTNetworking library in order to download remote images."); diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index bba887602..a55df421b 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -15,10 +15,15 @@ @class ALAssetsLibrary; typedef void (^RCTImageLoaderProgressBlock)(int64_t progress, int64_t total); -typedef void (^RCTImageLoaderCompletionBlock)(NSError *error, id image /* UIImage or CAAnimation */); -typedef void (^RCTImageLoaderCompletionBlock)(NSError *error, id image /* NSData, UIImage, CAAnimation */); +typedef void (^RCTImageLoaderCompletionBlock)(NSError *error, UIImage *image); typedef void (^RCTImageLoaderCancellationBlock)(void); +@interface UIImage (React) + +@property (nonatomic, copy) CAKeyframeAnimation *reactKeyframeAnimation; + +@end + @interface RCTImageLoader : NSObject /** diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 422a189d5..50040c95f 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -28,6 +28,20 @@ static void RCTDispatchCallbackOnMainQueue(void (^callback)(NSError *, id), NSEr } } +@implementation UIImage (React) + +- (CAKeyframeAnimation *)reactKeyframeAnimation +{ + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setReactKeyframeAnimation:(CAKeyframeAnimation *)reactKeyframeAnimation +{ + objc_setAssociatedObject(self, @selector(reactKeyframeAnimation), reactKeyframeAnimation, OBJC_ASSOCIATION_COPY_NONATOMIC); +} + +@end + @implementation RCTImageLoader @synthesize bridge = _bridge; @@ -99,7 +113,7 @@ RCT_EXPORT_MODULE() progressBlock(progress, total); }); } - } completionHandler:^(NSError *error, id image) { + } completionHandler:^(NSError *error, UIImage *image) { RCTDispatchCallbackOnMainQueue(completionBlock, error, image); }] ?: ^{}; } @@ -142,7 +156,7 @@ RCT_EXPORT_MODULE() { id imageDecoder = [self imageDecoderForRequest:data]; if (imageDecoder) { - return [imageDecoder decodeImageData:data size:size scale:scale resizeMode:resizeMode completionHandler:^(NSError *error, id image) { + return [imageDecoder decodeImageData:data size:size scale:scale resizeMode:resizeMode completionHandler:^(NSError *error, UIImage *image) { RCTDispatchCallbackOnMainQueue(completionBlock, error, image); }]; } else { diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index 01e5b2fbe..9b5ba8f80 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -142,10 +142,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) scale:RCTScreenScale() resizeMode:self.contentMode progressBlock:progressHandler - completionBlock:^(NSError *error, id image) { + completionBlock:^(NSError *error, UIImage *image) { - if ([image isKindOfClass:[CAAnimation class]]) { - [self.layer addAnimation:image forKey:@"contents"]; + if (image.reactKeyframeAnimation) { + [self.layer addAnimation:image.reactKeyframeAnimation forKey:@"contents"]; } else { [self.layer removeAnimationForKey:@"contents"]; self.image = image; From bb5522582d47fa22a7cf5d34c3069340ba495863 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Fri, 4 Sep 2015 05:26:09 -0700 Subject: [PATCH 0016/2013] Fixed layout animation crash --- React/Modules/RCTUIManager.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 9acaff806..f6e35dad4 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -488,6 +488,11 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); // Perform layout (possibly animated) return ^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { RCTResponseSenderBlock callback = self->_layoutAnimation.callback; + + // It's unsafe to call this callback more than once, so we nil it out here + // to make sure that doesn't happen. + _layoutAnimation.callback = nil; + __block NSUInteger completionsCalled = 0; for (NSUInteger ii = 0; ii < frames.count; ii++) { NSNumber *reactTag = frameReactTags[ii]; From bbeaac5d3bf48ba713c92ca9073a23ec9b05c87a Mon Sep 17 00:00:00 2001 From: Alexey Lang Date: Fri, 4 Sep 2015 05:50:01 -0700 Subject: [PATCH 0017/2013] Automatically adjust content inset after view controller did layout subviews --- .../testSliderExample_1@2x.png | Bin 22422 -> 22421 bytes .../testSwitchExample_1@2x.png | Bin 88496 -> 88327 bytes .../testTextExample_1@2x.png | Bin 272392 -> 273029 bytes .../testViewExample_1@2x.png | Bin 99477 -> 99387 bytes React/Views/RCTAutoInsetsProtocol.h | 9 +++++ React/Views/RCTScrollView.m | 11 +++--- React/Views/RCTWebView.m | 10 ++++-- React/Views/RCTWrapperViewController.m | 32 ++++++++++++++++-- 8 files changed, 53 insertions(+), 9 deletions(-) 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 b08e761b68c485a437b138f54c1b3a98612a0e45..239761ed298285c8d006f7ab972471c10c379964 100644 GIT binary patch literal 22421 zcmeHvXIN9&+V&1bf{F|xqJp5Jq9|ZN0tg0ctXL^Z35tqHCxlKC+c=0MHi`nGqQg)F zgEWC8IwD{wA_gKY5eP^p3893LygNb9@%0=X@q52>ogerI*n2-~t$nZOzMp%oy)MHJ zSs)Zqg9#(0%kg^KH9#FS%@9wr%bb$PUxL z{8OsZx&4JB#p5l7grZ=^Kq}vIdrPHl|6kqMcvN*ktXYB20SG{8&$_D(0n7m@UU6*}e*th0i<8)KDdZ(*08%C0nYbzy{#=;{^s{uzZG=}slCHe`fw-{v-YG_p%~oyu!& zX&E&o7Y{xcTT7s)o8sSJL}gFL1kXkiIAZOmtL3V23au1s{H47#%*M&j4g%$ zQeL<=(nxXc;hNrkNTE;G$u^XcG8+ha1lIY(&?;>U@6;5>R*CSusHG z*O?Uc_+VP91FZ(dhhe#qrz(%Smh${%04U|xS_^onxoK|^x2j})sru14J~u&?A?SdG zkAU4=l4=`m6U72<$~bwVjLn3%t!FVhO9YXdCt_&|9~!Y=YR*p(4kd;6AZt z6cBRKvS*l{O$Z%ctzkb*e^6>xc+!c@BZ`tBgozibWFrr<7C47d8YhZQo3ER#Vg+k4 z2&OnyZyrDvV(t*@rM+m~n^-jk-EErV^inEZts8|I7-wh>8WW3$pLt7=%k&+4mn?np zLj_X!XP*=5Wp|$^7Qy}zT_Y_$cH_qg>eVc zr)~fWrfh^wN@YRb_ydsuv1bL*Pp5#{s zR75`IRkJhvi&Kl}k2LVJ=P zN+!>+BE~qIem2(qgvO?QxX`gOEov=(ma1NN5U}OXp_)^LeYvh7A=PFh>b~3C;_kVV z2o5Nq!}*Ub!yPbk!RTK@UD8|bFQ7)|)110tCo|+6No1W&pGC&R(!vj~Vy{J}S}CgG z3ObP!P8*#o7ajhM%%>4O1kIWgA4}~;lhkn~7Lo!O<1Mh>3D%aOhl-JGTKYJE5y`>N zg{(!4&b^#YB9q4pi8%_C5Pjajgug2CEe93SS;%lV?FdKmk5GZ@FcLc=qFxZuzU&5a zqPu=ELn||~s4kOVh*%uzTaQJ6LhH7Apn;u33=K8E{`6MqJ87q41a8NyAq0N_KgrUQERs)P zPqy8Gaeh4+bI4IZthG=B!MWqf%i`*fR^DJ#&)MAjW&^+tD(J38DeY=mpBT?~sp~fI z0-xdqB+65aVT`vqM=8dA&3%$-N1UFht-dp0a+q+(-tYYr@5?SvAvRe17IZyhUHi!3 z1;vmDX8_@7DZFb`_icAZ5ho>z2vP42UZ4Y569ZG)8gpj*E0tYQoyh2!q~=Hhb@Rax zAvZx^!CxkW7MWX6%CM2j#1uvz&(2uDajj=4Bx>M{Ut3NDW{g%Qw_CIi>r_Hw#@P{u zfRjWF>Cfdf-?9gmCB^q7jIMH;+vQd87}6fvSkQq|l6jlr7) zgDt~Y3_Cm*3G>vlCX~>s6|rp?Z0HBSgPKYrU?p; zgkev}6OAG|^hy!xC8+8PIc)8?L55rt=5!hnB6w{r+}`FSCoDleop=_gP4H8_(2n-v z#hP`VB3LCjmE2xKHK!Ivv|nQ;B?RX7Kw?HdruW+~2!!4K&@!`Mu2d7wL^*huM7D7@ zO93*Dt35-l2!tZu@xtJ8)}|m{?0RfJDgm59L!{CAGp`FSkvv*nxJT!a&}HL5$BZi8 znscaQ>0?~=z;}nGJw{D65P)q-H^?vBwZU$IR4pu4G*UV{z^x_hbP-s3z>EvLyxJ#a z6A?z;e72p3W|;Now;oO4zr&Rl^`nVv+T7D13$AKnMsd}l73)jqk;y}Uw(`|H)>@c! zC6!og?lbr(h3^33f*BLi7D`yoZS3(l{}%H6yrv*qHZ{9Shj=TUA+?fG8zAfWLT_ok zz|dFIc#y(@p7!8`e0+FyzGuK+YBNk%=Am6k@_w*M64>MkyEw9;j{SEAo65ciOoUPM}p zAoz3}&`5^-af$u4+4_*wo7~Uiu-QhkGAwN^<4K zStTU}i&-k=HMa@6=g#iW| zH*U=C-w)iod-txqIpo~gvu9TVvJb$&?t+hkf1L$e_<@^XGaHp9&#we3D=I1`Tf8f2 zUl}L$MsjJUx%e^ADv6%}9hA89AG;|Y!DM3_C1X4*X%TE=$^-r1uQ(LD;s0>pY4G8X ziG=y38o_Q^^$x)*jUI*WS=_44s&b2c4|%r)cB<%;MjvUHN!~ph({NFC)-}-IkmzpN zl_yL%Wm;x?Cei0n>(Y)N?aeCHh@dxya4W+nMi^weOH%xd9n3K8`yb0UNKj1E^c($f z6}HS1ENyEK$~x;84;}Fye($UZ{mV6@;Fa>^=d>}$v=v9fiyoY6Ah*6nb`-Hpy~ zobV>yf9#UM@KTYy$CHy?M=fMtoVCb%1oSHTEGLg=UBAC_5?6Un8Hlqu9qJM{^FJo4 zd5@%fhNiNd96;}VD%L&M#k4=l)`R`>(?h9^p5%!|C!-sSpEinl70@)+nJ^GGIrC}O zdE@g${Rf}R!8H6ET#^D?!JGjTq=)97*X$ROS@_{swT3PIziHvmnf6~+ zwix_J>#5}2fsR;DkMVqdB&Bq*f1GyJB%lt>dWF4jp@qk@&*p?SJ-n*X5YV(bDX#UO zJ%ic}d(rq_nK2e|*} zc5~O`+lx;6(@$Q_38`o;30EV$59&z|NB7kIbLf?^Frtz_`1+w)mw_qN&K(?Z(0A;rI z)2+7qp~LS_4fnn;kyW)jWQPHl64HvM%suP>hKyo4_+M0$moRve=szY55PI{(ST$B- zu$qVU6y{6#+WVZYHtVpsaj8r~tH;yzl3x!2!-1m;s+U?pu>eznvV{OgKtZJdg(9x~ zgZAfNm(+~B-KQUMO`;wUx)vMOz*HlyOB1aH+XJ=BG5UpPQ6e!`5-V=Udkz^s(REM( z-?(v;G=OPOygp<;Og#j5ld zOeJD0g}#sO-jtse0P-t5$F+XGYhBuEs|>s2X*OAphF&TkG!Tw~iL=%!{e-0{@7>W7 zKCdTSLCtU;$W1$~nT3llNrtCw5BRnHhD_B84I{m5e1`6MP2bnPQ)sLdvta)_)_`RK zToeaS6*_ENw{=0-E9d)xzh8e|qrPz7MKM^10Q?eQvsqmWwvVH$?}1(uPd>yb zS<1^GDKxf?Xwn>8n?;S**KgBDv^jLKco)Mc3%XoRiY0a>puH2ocRck(30P3%sWmk` zP#L2T6NYCDSgxJEUKa39-c{O_Wlj<1OC^s7n*gOj@AOkrgHf+xSvkI4Z#hurj^nMw zXCEJ9ax%3pd;*|#SkF?x+D3@$KOk6T%qc^1eU`67=%B6)?)K2o z6_+R($AfZA8{$wpa`2|s`SAI=I{LXEBUcFLUXZU7sMD$q8YWYOUJIe`5m&Qt%@K~iVjGYe=(XEK6$jyT6q zj4T}mF|rfTJV$ONIxCtEng>F56ZH@xP%^SO&SllKejcuJsib&zBJ~&a=MhuE3 z6ww+kDQ>Z8;XJ92u3y)&+tI?Mvs0~S=Tb?=1up}xhLicp3_GKniD!-Z(F=NctTJRv z&SQ+FS1MFe2iwfX4%8U6KUW9_$D)%7|cxDwm65(qW~dJv*} zp2Cnu_q<4i)Hs?pu4YY?PP32>)|Dh??_EH>uZy7*l+s9Ux8LA%+N~GefNyUl z1OXlC43GQ|zmo9x1a5(ZHW^2~kYqVW?W7A5D+e?^hR`DtvGNZtAG)||q^z+2Sr6aj zJSQ$Lu;XsY6;%ftJD>6uB?DN|cq^!_?)klcQm;|<$ssqAk0u%Jk2Q3^0T+Bw1l}m8 zh!~qf`9rNKsTn%@QYyIHMgYot2Ob#au4WeX52R&REt-duCkF{=mx#vv6BC5hA}uML z!yPcq%rtOX1!E{PfU7}~r^I?ZOD3LCkd%Egb4p)ue-ep$y zd2qO$f#>`;c_ocVOJpD9V$68!3OtBZ#u2i|hZk2^H>+ek!5s*5YbB#&z#RWgnP6_p zQ{>r;C^t1A6-$}p17vc(kl!;t437yOD9#H2^>*_diiM|~lUTdFUdTFf8_78)Qu#it z*PNB*fd_1b#f+$M+y4AE{03hxm(@96?4YH*j>tGp%w*~S%7}t(e&P%c--gO=$JkKS~AI1W{GLkZF!PmmcQ;N3e!S<0l?zz5JTcRBm6dG3ik^vf9 zhZz~-Dy@vW-MR%AFXQ-VRnlJ6onCOCat&j3q9$Idqt4KHB33K5;LVDMYb27(d`wo@ zuYxl+iORdUZ!K$;W6LsK;;dd8tIn{1c=DnjsMQg(%sE`iQUYS+;wcjkO_9npQHTp) zG50_qvMFv$UrAZ{NSszoLCP4ou3bZLTM4AjGH;Tb9B(&}<{^6Ml$h9i3E?q#c!E~x6B zIM!k`25!FU{caI|LPMoDQr9*&?Rq`9k(m>s;AkOQ=>~RtPdYQm{_!LVp{lq_*d>oS zlx{RG>`Bu+6QXQIGILEv{-Zlumy|=p>fdKOtL8QN>>Fz#gS%I2k)<}pLeI1jz<1_k zIUfbo`?uZb0QN6m5jFQn;P}%W?erjz&~bfyRpO3LKYdwid632bU~hGjOw_< z_6Ob09d@*r{WD*ewpM~iljxv$^$60$e`dh)qzgT;%N`_6N3>YAQR8>6Y6SfeV&;gK z=~!h8X2UthjCn10Aa{uIsi7z$`K3#%KEqkKHi?LOW&%ePhQoISjjK*(G;Bx z0~k^v?pz93T4a|N#4PiU)VfeWc6hV~cCG4gp8D#LC(&BLV_SS8eKLj*#96VU9Yeuo z)eKPkF+;@KMzZf*tavF2dZmw`$ha`Ob8vwZc4!?VrI_01#+>N-_bY@xCPoNp?!egQ zw&D=aF~0nprgr1Jls$Lq+Vr2_Gj}6#@?v4w`B*V^Gn1q;n_ZZG2uNz|b~w1Ai-1=2 z^!mI@56X+^s!fW9IaImO)dPEm=c2~$6_AlFjj+6=WQT!huDxyV-6Rcv z!tK5yB}+p#O%HlJ=e*^@3kAtODi^pNm9FXvMc|e%HPNTQem@@C@#wm#UV`&eDx*@V zIHbR*f3U%osEfQ5LyRWI5)%fr*w%~3QZxAJ3H-&x#7}SRaSP~OE`!e8C^f58ovo55KzAq+gC9UC0*=Mim^>wUgXoT zX?@hHR6KOoDczbVEx)jW9gY|ElCC8kh_xaW+vzPU7gdtA=b^&ys&`RcTGY2zxU_Vi zL$Zn?O3M0#I%ym*(XE1l;rE)i`*pZEWQ8FFpZ1K-ncf3?C z8ZWC?zeKzF+%D~=RMDG5B5%4M@5hIyEeJQU$ zY+v|tf+#^UD~nPRFIYlXzL|SoK6f|8(&zaud-Q1{e}wOpc#WhZ@bM&v#H;hDl?mJ& z)bl;0gb?KR5(enYO!~lqffm>o@Z+v>d}^Z7q$8z_ENI?~pRuy6d#O!{qsnDk&GU!H z2&o9VLx$%|gL?eavs+GWKW#V!X~MXM{7tT|@@q=ooa}(p_IUYs=+?PZh@K z9#p;G*Q*e!t|yb4c+qlp$FP1_L77X_UX{Q|Q$0ep3#;Q}Dx;C8fzdK%tr1H4W5_ZO zEv2iQY89B|TSDsy&^o6~V<_s1$BL5+!U|uWMDNYZc=iaE03yk+A^9N$CJJ!XkQlL8 z>`}&P6&pRKRX(z9*3Yz`ZI!Yr?M3Dh_@CCHX~L0)#a(JT{1aCa)`W^pI`c7JSVPyD*^}~H z>cx!X+_uzeOG5EY^{yTR{^V=-Qh{Cr8F7<`>3Xwm7jIIxNG02fnzolXq%xI5C3*K& zj=9?@Q{zgX-m--{^K466cRwAE>mw09VCG16m2R5=nzff+ovA&pTqm0%)jh7{{WodJ~QyG zmz8>R8vu8(r*iy2+?4s04=wy$)c?y-hbbMafPCBoOyLN|XE>*hocLVF)T5^Hp47&v zBlq8q_t1_XJ@)fC=fs@&@xwx&qm}?>^8ekE2mHX%r_XQmSr>jl6Ti~z=@K(}rfllS zi8W~IQPX&T#(TP?OBnu{x>J+#pNN_+>Ci&(5~3Q@+JZK8?*k^?Jp_v zgN13j#7yJ;gQOjgx}<-WJV^q6T|>TN_Sd(+n6e)%Ow%Q18t*Ub!_>{t z&swPIl0IExzR;_w7mJ@QG1C_6=aBoksOggaSzfPf3@#1p8k8BXY7TJ(H*3~`Cj=Lm1HkMh05AhQeIyP3e>@Nw@ba>M zdz}LAyL>n7KNIzxA$(_qQ;(tm*MRT6?>9R8#^ul8e&hBxE`KLVJX8OXs83lj#lEH< zHRBL?F!m?H{+YN_7~&V{{)16pk>vkd)B^LR|H8;0=-WcZhzzQcZ%Y) qX`ftmPThWs*Q!&G`gxlBL%N>)-gtiDEfN6!+q2VRN5(eCi~j@u2bRbH literal 22422 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 Db54xZ 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 7297dde2dea9c3f3a5e57467726eb1a869db8a90..efdc8fe054d017b00544b4ac2ac66c6c38ff40dc 100644 GIT binary patch literal 88327 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 *)v(xWBuY^{Uf&kmRRJK^G?NEEOpfI( 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 casR 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 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 32449556d98f1055de668cb0b5e758e017668352..4055e789b6d9b7be89d7a95b09202785ebe3972e 100644 GIT binary patch literal 273029 zcmce;cUV(hw=Wt*v7so6fCwlG3MjoOEr=9p(u>liDOI|3R1oRXr1uU|1f+x#1f};D z=}0G(C?NzAlAIZQzjN;X_PzJHdq3w~|B!^NtToG+V~+VNgs4AJICtji83+V&PVup< zCIoUU00N=OJVgb5V^dQ)27X+0mD6|Ca

wFmtwqymGKMcXo4mA?>D!h8*YORg`_G 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

  • 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�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 literal 272392 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

    {x+ng= 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�!l^ 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�ZeOWG&HP8A`%yM~MDz3MMdAOK%stSDd^2gu z(*{FJ$XpO(O`&7Lw+`&G?j9N_9?gU%>dyyIB?=ZC4CjFkqBO9*Iy!B30V7x{X6*Ra zjRmPNt?N=9fH>`x)`fm^P(MZYCooPrgb{*T=_M#MBK)u$)971TQ=+<2l!Z}OtdRB* z`nEz81|@C1$lB8?;l(ZGRMevH@D!O@O2;Y6K8vGQUO}#sUMDu}Qe;W~r#n0m_n6*C#fgA=?Z46iZDbs zL}CA)J~SsNJpcghxbQ2)uDPlOp{&ZjyPV}ev--P_6n3|G-uy%|hByeHm(TAuaE?!* z`6ne6n9~eubVE#Tq6?f%y5;V@CtfqTEa2TX2fD7xDx!&Bp^IvT>iiNgiQplx3N6K4 zT(J(o^a7-BpO{&2V;nA`k=U7hseU>!Kkd>I981TwyQ_O5 zLg{evOF~K@ePOOuHxLu}cXj|OA zgdXVScg#XgC1=iY(0wUXAzIsxc?^d1?n#bAS49>5EUW z-wv!gMgudz;y@eganSEwc-Hl6S1LF}rfbZLX4G&4+koG-TpWXPs7DofkSAI};!{&L z!M_y=%8kqtcY_2pDdxd~We5s%--9{y|C;P2KN?bzWx)c2 zYGdO4=(`CMt6@lNt+Cu0*Tcet7*i&2vTz;q9erx;n}bM6E+CVh>MB3=?c#t^=o=X> zs(>6z)P9!h=an7N)>PcrHMnqQL`+H_Z81}iSGHD7iHl~eINAZcc$)HSM2&KoX+#afFIRO77?7nc8<&{i zRKp0ja~#R%1tljb`sUM*&%&~hi&5kn7p7w=8cgGXX#-FIsIy2If6xuC8Fs~fYj1ae z8H$Ddm&OE>Q!hZXqSKbcn{6bjh+`}yb!S6G18_AOrS%K0rrtRuN@Rx@qH=^J@gxw) zhXfGA{C3e;m{`-q6R2MoDon+EXplu%dv8R0YH>*=*MUQ-aVBS^L;DWD2M~ zgG-?A%;OW#JUn!pNb3oN{_2ESd@JZl2j|xZiz}* zTI+Z)BC+!Z84t`2$DqoIwho{TE!vRlH^Dv-2>pSD-}QoaVwqF3#4wMvpb2ZqZ%S9KT``r`J=dL~6# z+MMSE1+%T=CWSN+=Ha5oY|;b1Jhc=OR!L$Q@XjV1AxI_#`Rp*mse`7H-DZ1^6Stg5u8gS?v2&31rq_v&u0Mi@3m+I6rTwQrp+86c)hCY0Kgq)enYp z!6ud5@C6kYvv<*{O6qMpb-2DI4ZIRexeE3TbXu?nkv4mD<5;VT^RK4INq;P|L=Jnbpf# zc4Uvr5S0zOFbCz5XXJi7i$diZ6=78Ns~;(Y+Y}tLnxT9ud37|u8M*Yx4we|TL6-z3 z9dT~E@R)Y}2l4^#L5~=i&yw!T;?~7?syxnt(<2v@cRo}xvWPggu0rWJ_Z}TM%j9zI z4AWAh6-oZoYhC7G??522dG32dYi&nz%^3GX%n8F0ALN2 z1bv-j+rnK}Ss3AuN6M*C5{Hmu`(RIGO$E80bco$>S&8!lC@=2e$NSq$1Y>R!10Bz! zo2uq(Sbj*?&BW7@!m||ppMIMP5Ctk0YOBd+ z0>(KYIiF-E%)1G7x%O8)&Gj7r?FUV+RD502L4*i6oCoB$;ckrlCI!=%3MZ=eiVA8W zTX~Q+F}C8ajQe&?cXy|@?k;@QQPGU0$d9D^t|*QWujqIEixW;ep;i+a-Z@{}Z*IHa zstG65uXUf!mTq?1%J2nM<1zPIxTGKb6~1MY5)%0C^d*y48NWP8V%a6&^W8pdrD;* z<=0lIbebzoETGC5jMABtMBWBpP%VR3$@mtpLB1p)D9M#26F-RNGv*Ea{BRl&tqNKW z6GOIDLp^xcBmk!@4OuE}ljh1>=RwrN3rU(y!Lc??RIX>9hz!&0jOhj*E>#sQckA!R zn%=Q;@T0Ug@y9fWZ4_=^)xWzUB0;0-Hq_hOjEvVMiR=SAS2_bq{&o>KTIe@OBOfyF1ydCv(=Y2fTPeq`<4vN$cS$)0`U;OT3A87+?=HpK^ns)*ne5mfG!pPO>8J}p7LMm*i zeRXW-bDCHh0nOKi-=auOz5T@0C=goLEZw+#yj~4(vvpz?k zh)IKhTv`cC|55vH5xSd-9z3a$6PXe6eksE#x zj!U(wYQb0K`nF}x>UDw8LfxgK@VbFwCBYBnOFc4P|2ENsG;`WS&#Vji|Mlf+tf9D|CA)8;FLznD7APScBxr(ONoAUYWF=tq zs`Awdu({eR zfzHnBHXs~GgNr;=2PWI*@nEos2{C{GhVp(E^jW7SMcbr;&DWz{3=qd;&e41E_<}Lm z$qlEBKZgO~SsBy$9I(BqNMIvL?#lt?sDk84)nv4jfw!69$qM~Z^w5o8Cl?)V+#6h3 zO0sECL$U&f2!Zo0`r-zAUwa88JO0N)n%|r`KfQ)bC41tTvsClWp93idvbs|6eT3=* ztwCr>Md!(FfKVm(NZ+9Mj#k2cR1SjEsUPs$1p5Loznok!VMLyVdU|SUcVO9 z$B(9N=(ZhkV{YW?@5yR1lHSg>Gt*o>v2&yx(K0)(@;eOH(npbJ9%_!bC^d%#i24Q% z7=E+yaU7%Eho@#^EA5XwZgd|@&%N9an0gl&7EPq0s!kn}F(Gqu@05kq{HWE?#(%>t z;a@NWQ&=G~e2)1Nf)vTeK%@Z4x9=O?8XOQtGpho@^3N`;8OCFmUO{q_2b9c(S9R{t zt8{($NSoxBS7n2Q$fti4iUEO)hsdfBhG->lLoUO}(q(|OVF-I^rWzdxgRBD3A7PDa zvYyZ%jGkN{uX`a4NSFJDw04qXBPf0N;Xw>a%UDq5eOa~OSnMVF!P=K7V?FZ`_+~!A zcx(2T&%r!5=iG*xc-I`&FfVXk#_g8k+~&6G1&NBl2W^H2um<(w)f1+jMD|bGm?clIT+|-|gsAyN%c}eVLuMv@RLvE{jL=Gg7hcW2|s_E-$EfET! zJzVZVl%0<#_W)bl&_6OE1H_cAc22mBPE!@j1+7&ml~o2WI#Pe%U1_C zE~U!JzutHJVg+Tcz;ZGR-0A;TSd62J6GK$7umm;$?G$vk;_pVY3)lxawcwUa*g-iM z4eErMHHhihN|s_a`>Ha1=58liFHb3knx8^f57smaA6ATvMMHM!0%I8ezy}p?d`?%R zm0vFtGp}Hi^}(blt%+Mv;M+gd2Ou@nw`l*3*=-RHvd^Nj{3(p_rL8aSl#DWjwgGXh3*9IPOpwL(8@lL8D}KAvYNUz6XWvrO~!HudGX%RxyoBbs>M z516f0VAl&P^P*ai5%n^flYrpj<{Z>PKe9lDOdVf+)o$jVXdS}DlPJ>+j8D5+8dXb6 za0WGDUXSywtLbx>zqnN^T(3V9Fb)ile@6%z_Te*`zbp(~X19yQEe4_*PU{eF#Es&c7Eq`g|WA9o^seyEB@XxeSj(vmFev0$HZxqkIgS!p-uxY`rCW>R=nVkM` z&+$)E(Y_v^DmSELnTs>f5_HRSoUq7A-v0`suuSS7T5}3$Sg^T_oA+ciQ`3w(C@LRDDu;I z5PxWsceX4UG|DfyJb)t!6bsPHysKlqY(U-3@0hL#J?ad14+R-=A6OG=8-T*JN} zzvvdV^@Jr@8^!o7-1uKU-M{ti{=M%v!o-!o^EEwn<<0b!zxH4B5&zEDZ|ui^?=*h< z+D7SNrT_G(`mJZx+rIyeJHc<=3f}g85V#$@>(`%IZN2U1Z~N$f^!m*6$}<1pjou;i z&Q}9ny|O{l33-qpO)xe`{6$*_prX=YQ*p zy!+PucQ>mQLFZ~^>g`G|9|1e_fhH1CN=&19{Ktoi%RbSS@gVx-;*7)ca-rdb9lFeeG@6{@ssy_XUfdxpzP6{W8Zk`*&Zk zXkotlQSX=eG~Re0&8P9#yD!-H%lsE~_CA`w2kmdd@_W#ZK;8Rj{yv)jJEMOO+TZLB z@0?|$(DEL%V}#GI^2R?&M&JDS-QzNPQs4ckcZc8aLHjRs-{06A{>MRk#SDsS$im=p S-FhkbXSCJqo6OC7j{P4~9iof? literal 99477 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 diff --git a/React/Views/RCTAutoInsetsProtocol.h b/React/Views/RCTAutoInsetsProtocol.h index 8a14fc0bd..8e7c72c80 100644 --- a/React/Views/RCTAutoInsetsProtocol.h +++ b/React/Views/RCTAutoInsetsProtocol.h @@ -17,4 +17,13 @@ @property (nonatomic, assign, readwrite) UIEdgeInsets contentInset; @property (nonatomic, assign, readwrite) BOOL automaticallyAdjustContentInsets; +/** + * Automatically adjusted content inset depends on view controller's top and bottom + * layout guides so if you've changed one of them (e.g. after rotation or manually) you should call this method + * to recalculate and refresh content inset. + * To handle case with changing navigation bar height call this method from viewDidLayoutSubviews: + * of your view controller. + */ +- (void)refreshContentInset; + @end diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index 8c649da56..b502203f0 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -460,10 +460,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) _scrollView.frame = self.bounds; _scrollView.contentOffset = originalOffset; - [RCTView autoAdjustInsetsForView:self - withScrollView:_scrollView - updateOffset:YES]; - [self updateClippedSubviews]; } @@ -523,6 +519,13 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [_scrollView zoomToRect:rect animated:animated]; } +- (void)refreshContentInset +{ + [RCTView autoAdjustInsetsForView:self + withScrollView:_scrollView + updateOffset:YES]; +} + #pragma mark - ScrollView delegate #define RCT_SCROLL_EVENT_HANDLER(delegateMethod, eventName) \ diff --git a/React/Views/RCTWebView.m b/React/Views/RCTWebView.m index 4838179a5..46d47c895 100644 --- a/React/Views/RCTWebView.m +++ b/React/Views/RCTWebView.m @@ -95,9 +95,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) { [super layoutSubviews]; _webView.frame = self.bounds; - [RCTView autoAdjustInsetsForView:self - withScrollView:_webView.scrollView - updateOffset:YES]; } - (void)setContentInset:(UIEdgeInsets)contentInset @@ -133,6 +130,13 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) return event; } +- (void)refreshContentInset +{ + [RCTView autoAdjustInsetsForView:self + withScrollView:_webView.scrollView + updateOffset:YES]; +} + #pragma mark - UIWebViewDelegate methods - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request diff --git a/React/Views/RCTWrapperViewController.m b/React/Views/RCTWrapperViewController.m index 92707e683..7959326d5 100644 --- a/React/Views/RCTWrapperViewController.m +++ b/React/Views/RCTWrapperViewController.m @@ -16,13 +16,15 @@ #import "RCTUtils.h" #import "RCTViewControllerProtocol.h" #import "UIView+React.h" +#import "RCTAutoInsetsProtocol.h" @implementation RCTWrapperViewController { UIView *_wrapperView; UIView *_contentView; - CGFloat _previousTopLayout; - CGFloat _previousBottomLayout; + RCTEventDispatcher *_eventDispatcher; + CGFloat _previousTopLayoutLength; + CGFloat _previousBottomLayoutLength; } @synthesize currentTopLayoutGuide = _currentTopLayoutGuide; @@ -58,6 +60,32 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) _currentBottomLayoutGuide = self.bottomLayoutGuide; } +static BOOL RCTFindScrollViewAndRefreshContentInsetInView(UIView *view) +{ + if ([view conformsToProtocol:@protocol(RCTAutoInsetsProtocol)]) { + [(id ) view refreshContentInset]; + return YES; + } + for (UIView *subview in view.subviews) { + if (RCTFindScrollViewAndRefreshContentInsetInView(subview)) { + return YES; + } + } + return NO; +} + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + + if (_previousTopLayoutLength != _currentTopLayoutGuide.length || + _previousBottomLayoutLength != _currentBottomLayoutGuide.length) { + RCTFindScrollViewAndRefreshContentInsetInView(_contentView); + _previousTopLayoutLength = _currentTopLayoutGuide.length; + _previousBottomLayoutLength = _currentBottomLayoutGuide.length; + } +} + static UIView *RCTFindNavBarShadowViewInView(UIView *view) { if ([view isKindOfClass:[UIImageView class]] && view.bounds.size.height <= 1) { From b9f12056e95248398a0e26f2595c7d1a3105a62f Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Fri, 4 Sep 2015 08:51:32 -0100 Subject: [PATCH 0018/2013] [ReactNative] Update uglify-js Summary: Uglify had already been updated, but was accidentally changed back to the previous version. Update it again. @allow-crlf-text --- npm-shrinkwrap.json | 60 ++++++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9e5c55c36..875b7b045 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2112,10 +2112,15 @@ "from": "stacktrace-parser@0.1.3", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.3.tgz" }, + "timed-out": { + "version": "2.0.0", + "from": "timed-out@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz" + }, "uglify-js": { - "version": "2.4.16", - "from": "uglify-js@2.4.16", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.16.tgz", + "version": "2.4.24", + "from": "uglify-js@2.4.24", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", "dependencies": { "async": { "version": "0.2.10", @@ -2134,22 +2139,37 @@ } } }, - "optimist": { - "version": "0.3.7", - "from": "optimist@>=0.3.5 <0.4.0", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "dependencies": { - "wordwrap": { - "version": "0.0.3", - "from": "wordwrap@>=0.0.2 <0.1.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" - } - } - }, "uglify-to-browserify": { "version": "1.0.2", "from": "uglify-to-browserify@>=1.0.0 <1.1.0", "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz" + }, + "yargs": { + "version": "3.5.4", + "from": "yargs@>=3.5.4 <3.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", + "dependencies": { + "camelcase": { + "version": "1.2.1", + "from": "camelcase@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz" + }, + "decamelize": { + "version": "1.0.0", + "from": "decamelize@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.0.0.tgz" + }, + "window-size": { + "version": "0.1.0", + "from": "window-size@0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz" + }, + "wordwrap": { + "version": "0.0.2", + "from": "wordwrap@0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz" + } + } } } }, @@ -3106,11 +3126,6 @@ "version": "1.2.1", "from": "statuses@>=1.2.1 <2.0.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz" - }, - "timed-out": { - "version": "2.0.0", - "from": "timed-out@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz" } } }, @@ -4928,11 +4943,6 @@ } } } - }, - "timed-out": { - "version": "2.0.0", - "from": "timed-out@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz" } } }, diff --git a/package.json b/package.json index cebee4dfa..beb99bf45 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "semver": "^4.3.6", "source-map": "0.1.31", "stacktrace-parser": "0.1.3", - "uglify-js": "2.4.16", + "uglify-js": "2.4.24", "underscore": "1.7.0", "wordwrap": "^1.0.0", "worker-farm": "^1.3.1", From 1d04a58821c48316bd1e58276c7cef3302d447d3 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 4 Sep 2015 08:17:15 -0700 Subject: [PATCH 0019/2013] Correct references to unexistent SnapshotTestManager to TestModule --- .../UIExplorer/UIExplorerIntegrationTests/js/AppEventsTest.js | 2 +- .../UIExplorerIntegrationTests/js/LayoutEventsTest.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/AppEventsTest.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/AppEventsTest.js index bf7931b5a..95370771b 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/AppEventsTest.js +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/AppEventsTest.js @@ -19,7 +19,7 @@ var { Text, View, } = React; -var TestModule = NativeModules.TestModule || NativeModules.SnapshotTestManager; +var TestModule = NativeModules.TestModule; var deepDiffer = require('deepDiffer'); diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/LayoutEventsTest.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/LayoutEventsTest.js index 0333baf6c..9149eab4e 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/LayoutEventsTest.js +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/LayoutEventsTest.js @@ -20,7 +20,7 @@ var { Text, View, } = React; -var TestModule = NativeModules.TestModule || NativeModules.SnapshotTestManager; +var TestModule = NativeModules.TestModule; var deepDiffer = require('deepDiffer'); From c5935049106b87fd6455b3fac2d7ef49d134abc3 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Fri, 4 Sep 2015 10:44:18 -0700 Subject: [PATCH 0020/2013] [ReactNative] Add RCTJSCProfiler to internal apps --- React/Executors/RCTContextExecutor.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 2dcf3d7b0..85854bf64 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -23,7 +23,7 @@ #import "RCTUtils.h" #ifndef RCT_JSC_PROFILER -#if RCT_DEV && RCT_DEBUG +#if RCT_DEV #define RCT_JSC_PROFILER 1 #else #define RCT_JSC_PROFILER 0 @@ -34,7 +34,7 @@ #include #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:@"Frameworks"] UTF8String] +#define RCT_JSC_PROFILER_DYLIB [[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"RCTJSCProfiler.ios%zd", [[[UIDevice currentDevice] systemVersion] integerValue]] ofType:@"dylib" inDirectory:@"RCTJSCProfiler"] UTF8String] #endif #endif From 90409770c9487d8e84242b2e409383054f05891f Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Fri, 4 Sep 2015 12:06:44 -0700 Subject: [PATCH 0021/2013] [RectNative][Packager] Cache minification result Summary: The packager just cached the result of the bundle, but would minify it on every request. Change it to cache the minification result. --- packager/react-packager/src/Bundler/Bundle.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packager/react-packager/src/Bundler/Bundle.js b/packager/react-packager/src/Bundler/Bundle.js index 88431cbf6..6f20b49d0 100644 --- a/packager/react-packager/src/Bundler/Bundle.js +++ b/packager/react-packager/src/Bundler/Bundle.js @@ -115,13 +115,18 @@ class Bundle { getMinifiedSourceAndMap() { this._assertFinalized(); + if (this._minifiedSourceAndMap) { + return this._minifiedSourceAndMap; + } + const source = this._getSource(); try { - return UglifyJS.minify(source, { + this._minifiedSourceAndMap = UglifyJS.minify(source, { fromString: true, outSourceMap: 'bundle.js', inSourceMap: this.getSourceMap(), }); + return this._minifiedSourceAndMap; } catch(e) { // Sometimes, when somebody is using a new syntax feature that we // don't yet have transform for, the untransformed line is sent to From 8831bb10a1427842b9995cab6441e1af3ab3d066 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Fri, 4 Sep 2015 11:29:04 -0700 Subject: [PATCH 0022/2013] [npm] Upgrade all the modules to their latest version --- npm-shrinkwrap.json | 2490 +++++++++++++++++++++++++++++++++++++------ package.json | 54 +- 2 files changed, 2193 insertions(+), 351 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 875b7b045..2277ceccf 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -8,9 +8,9 @@ "resolved": "https://registry.npmjs.org/absolute-path/-/absolute-path-0.0.0.tgz" }, "babel": { - "version": "5.8.21", - "from": "babel@5.8.21", - "resolved": "https://registry.npmjs.org/babel/-/babel-5.8.21.tgz", + "version": "5.8.23", + "from": "babel@5.8.23", + "resolved": "https://registry.npmjs.org/babel/-/babel-5.8.23.tgz", "dependencies": { "chokidar": { "version": "1.0.5", @@ -354,7 +354,7 @@ }, "inherits": { "version": "2.0.1", - "from": "inherits@>=2.0.1 <2.1.0", + "from": "inherits@>=2.0.0 <3.0.0", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "minimatch": { @@ -434,18 +434,6 @@ "from": "path-is-absolute@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" }, - "source-map": { - "version": "0.4.4", - "from": "source-map@>=0.4.0 <0.5.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "dependencies": { - "amdefine": { - "version": "1.0.0", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" - } - } - }, "slash": { "version": "1.0.0", "from": "slash@>=1.0.0 <2.0.0", @@ -454,9 +442,9 @@ } }, "babel-core": { - "version": "5.8.21", - "from": "babel-core@5.8.21", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-5.8.21.tgz", + "version": "5.8.23", + "from": "babel-core@5.8.23", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-5.8.23.tgz", "dependencies": { "babel-plugin-constant-folding": { "version": "1.0.1", @@ -542,7 +530,7 @@ }, "babylon": { "version": "5.8.23", - "from": "babylon@>=5.8.21 <6.0.0", + "from": "babylon@>=5.8.23 <6.0.0", "resolved": "https://registry.npmjs.org/babylon/-/babylon-5.8.23.tgz" }, "bluebird": { @@ -560,18 +548,6 @@ "from": "core-js@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.1.3.tgz" }, - "debug": { - "version": "2.2.0", - "from": "debug@>=2.1.1 <3.0.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "dependencies": { - "ms": { - "version": "0.7.1", - "from": "ms@0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" - } - } - }, "detect-indent": { "version": "3.0.1", "from": "detect-indent@>=3.0.0 <4.0.0", @@ -888,6 +864,11 @@ "version": "0.1.2", "from": "tryor@>=0.1.2 <0.2.0", "resolved": "https://registry.npmjs.org/tryor/-/tryor-0.1.2.tgz" + }, + "yargs": { + "version": "1.3.3", + "from": "yargs@>=1.3.2 <1.4.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-1.3.3.tgz" } } }, @@ -995,18 +976,6 @@ "from": "slash@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz" }, - "source-map": { - "version": "0.4.4", - "from": "source-map@>=0.4.0 <0.5.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "dependencies": { - "amdefine": { - "version": "1.0.0", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" - } - } - }, "source-map-support": { "version": "0.2.10", "from": "source-map-support@>=0.2.10 <0.3.0", @@ -1043,10 +1012,153 @@ } } }, + "babel-eslint": { + "version": "4.1.1", + "from": "babel-eslint@4.1.1", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-4.1.1.tgz", + "dependencies": { + "lodash.assign": { + "version": "3.2.0", + "from": "lodash.assign@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", + "dependencies": { + "lodash._baseassign": { + "version": "3.2.0", + "from": "lodash._baseassign@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "dependencies": { + "lodash._basecopy": { + "version": "3.0.1", + "from": "lodash._basecopy@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz" + } + } + }, + "lodash._createassigner": { + "version": "3.1.1", + "from": "lodash._createassigner@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "dependencies": { + "lodash._bindcallback": { + "version": "3.0.1", + "from": "lodash._bindcallback@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz" + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "from": "lodash._isiterateecall@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz" + }, + "lodash.restparam": { + "version": "3.6.1", + "from": "lodash.restparam@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz" + } + } + }, + "lodash.keys": { + "version": "3.1.2", + "from": "lodash.keys@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "dependencies": { + "lodash._getnative": { + "version": "3.9.1", + "from": "lodash._getnative@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz" + }, + "lodash.isarguments": { + "version": "3.0.4", + "from": "lodash.isarguments@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.0.4.tgz" + }, + "lodash.isarray": { + "version": "3.0.4", + "from": "lodash.isarray@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" + } + } + } + } + }, + "lodash.pick": { + "version": "3.1.0", + "from": "lodash.pick@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-3.1.0.tgz", + "dependencies": { + "lodash._baseflatten": { + "version": "3.1.4", + "from": "lodash._baseflatten@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._baseflatten/-/lodash._baseflatten-3.1.4.tgz", + "dependencies": { + "lodash.isarguments": { + "version": "3.0.4", + "from": "lodash.isarguments@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.0.4.tgz" + }, + "lodash.isarray": { + "version": "3.0.4", + "from": "lodash.isarray@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" + } + } + }, + "lodash._bindcallback": { + "version": "3.0.1", + "from": "lodash._bindcallback@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz" + }, + "lodash._pickbyarray": { + "version": "3.0.2", + "from": "lodash._pickbyarray@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._pickbyarray/-/lodash._pickbyarray-3.0.2.tgz" + }, + "lodash._pickbycallback": { + "version": "3.0.0", + "from": "lodash._pickbycallback@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._pickbycallback/-/lodash._pickbycallback-3.0.0.tgz", + "dependencies": { + "lodash._basefor": { + "version": "3.0.2", + "from": "lodash._basefor@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.2.tgz" + }, + "lodash.keysin": { + "version": "3.0.8", + "from": "lodash.keysin@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.keysin/-/lodash.keysin-3.0.8.tgz", + "dependencies": { + "lodash.isarguments": { + "version": "3.0.4", + "from": "lodash.isarguments@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.0.4.tgz" + }, + "lodash.isarray": { + "version": "3.0.4", + "from": "lodash.isarray@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" + } + } + } + } + }, + "lodash.restparam": { + "version": "3.6.1", + "from": "lodash.restparam@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz" + } + } + }, + "acorn-to-esprima": { + "version": "1.0.2", + "from": "acorn-to-esprima@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/acorn-to-esprima/-/acorn-to-esprima-1.0.2.tgz" + } + } + }, "bser": { - "version": "1.0.0", - "from": "bser@1.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-1.0.0.tgz", + "version": "1.0.2", + "from": "bser@1.0.2", + "resolved": "https://registry.npmjs.org/bser/-/bser-1.0.2.tgz", "dependencies": { "node-int64": { "version": "0.4.0", @@ -1056,13 +1168,13 @@ } }, "chalk": { - "version": "1.0.0", - "from": "chalk@1.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.0.0.tgz", + "version": "1.1.1", + "from": "chalk@1.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.1.tgz", "dependencies": { "ansi-styles": { "version": "2.1.0", - "from": "ansi-styles@>=2.0.1 <3.0.0", + "from": "ansi-styles@>=2.1.0 <3.0.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.1.0.tgz" }, "escape-string-regexp": { @@ -1071,127 +1183,866 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.3.tgz" }, "has-ansi": { - "version": "1.0.3", - "from": "has-ansi@>=1.0.3 <2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-1.0.3.tgz", + "version": "2.0.0", + "from": "has-ansi@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "dependencies": { "ansi-regex": { - "version": "1.1.1", - "from": "ansi-regex@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz" - }, - "get-stdin": { - "version": "4.0.1", - "from": "get-stdin@>=4.0.1 <5.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz" + "version": "2.0.0", + "from": "ansi-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" } } }, "strip-ansi": { - "version": "2.0.1", - "from": "strip-ansi@>=2.0.1 <3.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-2.0.1.tgz", + "version": "3.0.0", + "from": "strip-ansi@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.0.tgz", "dependencies": { "ansi-regex": { - "version": "1.1.1", - "from": "ansi-regex@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz" + "version": "2.0.0", + "from": "ansi-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" } } }, "supports-color": { - "version": "1.3.1", - "from": "supports-color@>=1.3.0 <2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.3.1.tgz" + "version": "2.0.0", + "from": "supports-color@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" } } }, "connect": { - "version": "2.8.3", - "from": "connect@2.8.3", - "resolved": "https://registry.npmjs.org/connect/-/connect-2.8.3.tgz", + "version": "3.4.0", + "from": "connect@3.4.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.4.0.tgz", "dependencies": { - "qs": { - "version": "0.6.5", - "from": "qs@0.6.5", - "resolved": "https://registry.npmjs.org/qs/-/qs-0.6.5.tgz" - }, - "formidable": { - "version": "1.0.14", - "from": "formidable@1.0.14", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.14.tgz" - }, - "cookie-signature": { - "version": "1.0.1", - "from": "cookie-signature@1.0.1", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.1.tgz" - }, - "buffer-crc32": { - "version": "0.2.1", - "from": "buffer-crc32@0.2.1", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.1.tgz" - }, - "cookie": { - "version": "0.1.0", - "from": "cookie@0.1.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz" - }, - "send": { - "version": "0.1.2", - "from": "send@0.1.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.1.2.tgz", + "finalhandler": { + "version": "0.4.0", + "from": "finalhandler@0.4.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz", "dependencies": { - "mime": { - "version": "1.2.11", - "from": "mime@>=1.2.9 <1.3.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz" + "escape-html": { + "version": "1.0.2", + "from": "escape-html@1.0.2", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz" }, - "range-parser": { - "version": "0.0.4", - "from": "range-parser@0.0.4", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-0.0.4.tgz" + "on-finished": { + "version": "2.3.0", + "from": "on-finished@>=2.3.0 <2.4.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "dependencies": { + "ee-first": { + "version": "1.1.1", + "from": "ee-first@1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + } + } + }, + "unpipe": { + "version": "1.0.0", + "from": "unpipe@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" } } }, - "bytes": { - "version": "0.2.0", - "from": "bytes@0.2.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-0.2.0.tgz" + "parseurl": { + "version": "1.3.0", + "from": "parseurl@>=1.3.0 <1.4.0", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz" }, - "fresh": { - "version": "0.1.0", - "from": "fresh@0.1.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.1.0.tgz" - }, - "pause": { - "version": "0.0.1", - "from": "pause@0.0.1", - "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz" - }, - "uid2": { - "version": "0.0.2", - "from": "uid2@0.0.2", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.2.tgz" - }, - "methods": { - "version": "0.0.1", - "from": "methods@0.0.1", - "resolved": "https://registry.npmjs.org/methods/-/methods-0.0.1.tgz" + "utils-merge": { + "version": "1.0.0", + "from": "utils-merge@1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz" } } }, "debug": { - "version": "2.1.0", - "from": "debug@2.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.0.tgz", + "version": "2.2.0", + "from": "debug@2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "dependencies": { "ms": { - "version": "0.6.2", - "from": "ms@0.6.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz" + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" } } }, + "eslint": { + "version": "1.3.1", + "from": "eslint@1.3.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-1.3.1.tgz", + "dependencies": { + "concat-stream": { + "version": "1.5.0", + "from": "concat-stream@>=1.4.6 <2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.0.tgz", + "dependencies": { + "inherits": { + "version": "2.0.1", + "from": "inherits@>=2.0.1 <2.1.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "typedarray": { + "version": "0.0.6", + "from": "typedarray@>=0.0.5 <0.1.0", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" + }, + "readable-stream": { + "version": "2.0.2", + "from": "readable-stream@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.2.tgz", + "dependencies": { + "core-util-is": { + "version": "1.0.1", + "from": "core-util-is@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz" + }, + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "process-nextick-args": { + "version": "1.0.2", + "from": "process-nextick-args@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.2.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "util-deprecate": { + "version": "1.0.1", + "from": "util-deprecate@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.1.tgz" + } + } + } + } + }, + "doctrine": { + "version": "0.6.4", + "from": "doctrine@>=0.6.2 <0.7.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.6.4.tgz", + "dependencies": { + "esutils": { + "version": "1.1.6", + "from": "esutils@>=1.1.6 <2.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz" + }, + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + } + } + }, + "escape-string-regexp": { + "version": "1.0.3", + "from": "escape-string-regexp@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.3.tgz" + }, + "escope": { + "version": "3.2.0", + "from": "escope@>=3.2.0 <4.0.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.2.0.tgz", + "dependencies": { + "es6-map": { + "version": "0.1.1", + "from": "es6-map@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.1.tgz", + "dependencies": { + "d": { + "version": "0.1.1", + "from": "d@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz" + }, + "es5-ext": { + "version": "0.10.7", + "from": "es5-ext@>=0.10.4 <0.11.0", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.7.tgz", + "dependencies": { + "es6-symbol": { + "version": "2.0.1", + "from": "es6-symbol@>=2.0.1 <2.1.0", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-2.0.1.tgz" + } + } + }, + "es6-iterator": { + "version": "0.1.3", + "from": "es6-iterator@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-0.1.3.tgz", + "dependencies": { + "es6-symbol": { + "version": "2.0.1", + "from": "es6-symbol@>=2.0.1 <2.1.0", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-2.0.1.tgz" + } + } + }, + "es6-set": { + "version": "0.1.1", + "from": "es6-set@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.1.tgz" + }, + "es6-symbol": { + "version": "0.1.1", + "from": "es6-symbol@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-0.1.1.tgz" + }, + "event-emitter": { + "version": "0.3.3", + "from": "event-emitter@>=0.3.1 <0.4.0", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.3.tgz" + } + } + }, + "es6-weak-map": { + "version": "0.1.4", + "from": "es6-weak-map@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-0.1.4.tgz", + "dependencies": { + "d": { + "version": "0.1.1", + "from": "d@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz" + }, + "es5-ext": { + "version": "0.10.7", + "from": "es5-ext@>=0.10.6 <0.11.0", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.7.tgz" + }, + "es6-iterator": { + "version": "0.1.3", + "from": "es6-iterator@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-0.1.3.tgz" + }, + "es6-symbol": { + "version": "2.0.1", + "from": "es6-symbol@>=2.0.1 <2.1.0", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-2.0.1.tgz" + } + } + }, + "esrecurse": { + "version": "3.1.1", + "from": "esrecurse@>=3.1.1 <4.0.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-3.1.1.tgz" + }, + "estraverse": { + "version": "3.1.0", + "from": "estraverse@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-3.1.0.tgz" + } + } + }, + "espree": { + "version": "2.2.4", + "from": "espree@>=2.2.4 <3.0.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-2.2.4.tgz" + }, + "estraverse": { + "version": "4.1.0", + "from": "estraverse@>=4.1.0 <5.0.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.1.0.tgz" + }, + "estraverse-fb": { + "version": "1.3.1", + "from": "estraverse-fb@>=1.3.1 <2.0.0", + "resolved": "https://registry.npmjs.org/estraverse-fb/-/estraverse-fb-1.3.1.tgz" + }, + "globals": { + "version": "8.7.0", + "from": "globals@>=8.5.0 <9.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-8.7.0.tgz" + }, + "handlebars": { + "version": "3.0.3", + "from": "handlebars@>=3.0.3 <4.0.0", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-3.0.3.tgz", + "dependencies": { + "source-map": { + "version": "0.1.43", + "from": "source-map@>=0.1.40 <0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "dependencies": { + "amdefine": { + "version": "1.0.0", + "from": "amdefine@>=0.0.4", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" + } + } + }, + "uglify-js": { + "version": "2.3.6", + "from": "uglify-js@>=2.3.0 <2.4.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.3.6.tgz", + "dependencies": { + "async": { + "version": "0.2.10", + "from": "async@>=0.2.6 <0.3.0", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz" + }, + "optimist": { + "version": "0.3.7", + "from": "optimist@>=0.3.5 <0.4.0", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "from": "wordwrap@>=0.0.2 <0.1.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" + } + } + } + } + } + } + }, + "inquirer": { + "version": "0.9.0", + "from": "inquirer@>=0.9.0 <0.10.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.9.0.tgz", + "dependencies": { + "ansi-regex": { + "version": "2.0.0", + "from": "ansi-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" + }, + "cli-width": { + "version": "1.0.1", + "from": "cli-width@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-1.0.1.tgz" + }, + "figures": { + "version": "1.3.5", + "from": "figures@>=1.3.5 <2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.3.5.tgz" + }, + "lodash": { + "version": "3.10.1", + "from": "lodash@>=3.3.1 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz" + }, + "readline2": { + "version": "0.1.1", + "from": "readline2@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-0.1.1.tgz", + "dependencies": { + "mute-stream": { + "version": "0.0.4", + "from": "mute-stream@0.0.4", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.4.tgz" + }, + "strip-ansi": { + "version": "2.0.1", + "from": "strip-ansi@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-2.0.1.tgz", + "dependencies": { + "ansi-regex": { + "version": "1.1.1", + "from": "ansi-regex@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz" + } + } + } + } + }, + "run-async": { + "version": "0.1.0", + "from": "run-async@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "dependencies": { + "once": { + "version": "1.3.2", + "from": "once@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.2.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.1", + "from": "wrappy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" + } + } + } + } + }, + "rx-lite": { + "version": "2.5.2", + "from": "rx-lite@>=2.5.2 <3.0.0", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-2.5.2.tgz" + }, + "strip-ansi": { + "version": "3.0.0", + "from": "strip-ansi@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.0.tgz" + }, + "through": { + "version": "2.3.8", + "from": "through@>=2.3.6 <3.0.0", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + } + } + }, + "is-my-json-valid": { + "version": "2.12.2", + "from": "is-my-json-valid@>=2.10.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.12.2.tgz", + "dependencies": { + "generate-function": { + "version": "2.0.0", + "from": "generate-function@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" + }, + "generate-object-property": { + "version": "1.2.0", + "from": "generate-object-property@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "dependencies": { + "is-property": { + "version": "1.0.2", + "from": "is-property@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" + } + } + }, + "jsonpointer": { + "version": "2.0.0", + "from": "jsonpointer@2.0.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" + }, + "xtend": { + "version": "4.0.0", + "from": "xtend@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.0.tgz" + } + } + }, + "is-resolvable": { + "version": "1.0.0", + "from": "is-resolvable@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", + "dependencies": { + "tryit": { + "version": "1.0.1", + "from": "tryit@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.1.tgz" + } + } + }, + "js-yaml": { + "version": "3.4.0", + "from": "js-yaml@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.4.0.tgz", + "dependencies": { + "argparse": { + "version": "1.0.2", + "from": "argparse@>=1.0.2 <1.1.0", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.2.tgz", + "dependencies": { + "lodash": { + "version": "3.10.1", + "from": "lodash@>=3.2.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz" + }, + "sprintf-js": { + "version": "1.0.3", + "from": "sprintf-js@>=1.0.2 <1.1.0", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" + } + } + }, + "esprima": { + "version": "2.2.0", + "from": "esprima@>=2.2.0 <2.3.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.2.0.tgz" + } + } + }, + "lodash.clonedeep": { + "version": "3.0.2", + "from": "lodash.clonedeep@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz", + "dependencies": { + "lodash._baseclone": { + "version": "3.3.0", + "from": "lodash._baseclone@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz", + "dependencies": { + "lodash._arraycopy": { + "version": "3.0.0", + "from": "lodash._arraycopy@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz" + }, + "lodash._arrayeach": { + "version": "3.0.0", + "from": "lodash._arrayeach@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz" + }, + "lodash._baseassign": { + "version": "3.2.0", + "from": "lodash._baseassign@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "dependencies": { + "lodash._basecopy": { + "version": "3.0.1", + "from": "lodash._basecopy@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz" + } + } + }, + "lodash._basefor": { + "version": "3.0.2", + "from": "lodash._basefor@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.2.tgz" + }, + "lodash.isarray": { + "version": "3.0.4", + "from": "lodash.isarray@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" + }, + "lodash.keys": { + "version": "3.1.2", + "from": "lodash.keys@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "dependencies": { + "lodash._getnative": { + "version": "3.9.1", + "from": "lodash._getnative@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz" + }, + "lodash.isarguments": { + "version": "3.0.4", + "from": "lodash.isarguments@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.0.4.tgz" + } + } + } + } + }, + "lodash._bindcallback": { + "version": "3.0.1", + "from": "lodash._bindcallback@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz" + } + } + }, + "lodash.merge": { + "version": "3.3.2", + "from": "lodash.merge@>=3.3.2 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-3.3.2.tgz", + "dependencies": { + "lodash._arraycopy": { + "version": "3.0.0", + "from": "lodash._arraycopy@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz" + }, + "lodash._arrayeach": { + "version": "3.0.0", + "from": "lodash._arrayeach@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz" + }, + "lodash._createassigner": { + "version": "3.1.1", + "from": "lodash._createassigner@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "dependencies": { + "lodash._bindcallback": { + "version": "3.0.1", + "from": "lodash._bindcallback@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz" + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "from": "lodash._isiterateecall@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz" + }, + "lodash.restparam": { + "version": "3.6.1", + "from": "lodash.restparam@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz" + } + } + }, + "lodash._getnative": { + "version": "3.9.1", + "from": "lodash._getnative@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz" + }, + "lodash.isarguments": { + "version": "3.0.4", + "from": "lodash.isarguments@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.0.4.tgz" + }, + "lodash.isarray": { + "version": "3.0.4", + "from": "lodash.isarray@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" + }, + "lodash.isplainobject": { + "version": "3.2.0", + "from": "lodash.isplainobject@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-3.2.0.tgz", + "dependencies": { + "lodash._basefor": { + "version": "3.0.2", + "from": "lodash._basefor@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.2.tgz" + } + } + }, + "lodash.istypedarray": { + "version": "3.0.2", + "from": "lodash.istypedarray@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.istypedarray/-/lodash.istypedarray-3.0.2.tgz" + }, + "lodash.keys": { + "version": "3.1.2", + "from": "lodash.keys@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz" + }, + "lodash.keysin": { + "version": "3.0.8", + "from": "lodash.keysin@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.keysin/-/lodash.keysin-3.0.8.tgz" + }, + "lodash.toplainobject": { + "version": "3.0.0", + "from": "lodash.toplainobject@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.toplainobject/-/lodash.toplainobject-3.0.0.tgz", + "dependencies": { + "lodash._basecopy": { + "version": "3.0.1", + "from": "lodash._basecopy@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz" + } + } + } + } + }, + "lodash.omit": { + "version": "3.1.0", + "from": "lodash.omit@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-3.1.0.tgz", + "dependencies": { + "lodash._arraymap": { + "version": "3.0.0", + "from": "lodash._arraymap@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._arraymap/-/lodash._arraymap-3.0.0.tgz" + }, + "lodash._basedifference": { + "version": "3.0.3", + "from": "lodash._basedifference@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basedifference/-/lodash._basedifference-3.0.3.tgz", + "dependencies": { + "lodash._baseindexof": { + "version": "3.1.0", + "from": "lodash._baseindexof@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz" + }, + "lodash._cacheindexof": { + "version": "3.0.2", + "from": "lodash._cacheindexof@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz" + }, + "lodash._createcache": { + "version": "3.1.2", + "from": "lodash._createcache@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._createcache/-/lodash._createcache-3.1.2.tgz", + "dependencies": { + "lodash._getnative": { + "version": "3.9.1", + "from": "lodash._getnative@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz" + } + } + } + } + }, + "lodash._baseflatten": { + "version": "3.1.4", + "from": "lodash._baseflatten@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._baseflatten/-/lodash._baseflatten-3.1.4.tgz", + "dependencies": { + "lodash.isarguments": { + "version": "3.0.4", + "from": "lodash.isarguments@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.0.4.tgz" + }, + "lodash.isarray": { + "version": "3.0.4", + "from": "lodash.isarray@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" + } + } + }, + "lodash._bindcallback": { + "version": "3.0.1", + "from": "lodash._bindcallback@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz" + }, + "lodash._pickbyarray": { + "version": "3.0.2", + "from": "lodash._pickbyarray@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._pickbyarray/-/lodash._pickbyarray-3.0.2.tgz" + }, + "lodash._pickbycallback": { + "version": "3.0.0", + "from": "lodash._pickbycallback@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._pickbycallback/-/lodash._pickbycallback-3.0.0.tgz", + "dependencies": { + "lodash._basefor": { + "version": "3.0.2", + "from": "lodash._basefor@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.2.tgz" + } + } + }, + "lodash.keysin": { + "version": "3.0.8", + "from": "lodash.keysin@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.keysin/-/lodash.keysin-3.0.8.tgz", + "dependencies": { + "lodash.isarguments": { + "version": "3.0.4", + "from": "lodash.isarguments@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.0.4.tgz" + }, + "lodash.isarray": { + "version": "3.0.4", + "from": "lodash.isarray@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" + } + } + }, + "lodash.restparam": { + "version": "3.6.1", + "from": "lodash.restparam@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz" + } + } + }, + "minimatch": { + "version": "2.0.10", + "from": "minimatch@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "dependencies": { + "brace-expansion": { + "version": "1.1.0", + "from": "brace-expansion@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.0.tgz", + "dependencies": { + "balanced-match": { + "version": "0.2.0", + "from": "balanced-match@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.2.0.tgz" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "dependencies": { + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + } + }, + "object-assign": { + "version": "2.1.1", + "from": "object-assign@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz" + }, + "optionator": { + "version": "0.5.0", + "from": "optionator@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.5.0.tgz", + "dependencies": { + "prelude-ls": { + "version": "1.1.2", + "from": "prelude-ls@>=1.1.1 <1.2.0", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" + }, + "deep-is": { + "version": "0.1.3", + "from": "deep-is@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz" + }, + "wordwrap": { + "version": "0.0.3", + "from": "wordwrap@>=0.0.2 <0.1.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" + }, + "type-check": { + "version": "0.3.1", + "from": "type-check@>=0.3.1 <0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.1.tgz" + }, + "levn": { + "version": "0.2.5", + "from": "levn@>=0.2.5 <0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.2.5.tgz" + }, + "fast-levenshtein": { + "version": "1.0.7", + "from": "fast-levenshtein@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.0.7.tgz" + } + } + }, + "path-is-absolute": { + "version": "1.0.0", + "from": "path-is-absolute@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" + }, + "path-is-inside": { + "version": "1.0.1", + "from": "path-is-inside@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.1.tgz" + }, + "strip-json-comments": { + "version": "1.0.4", + "from": "strip-json-comments@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz" + }, + "text-table": { + "version": "0.2.0", + "from": "text-table@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + }, + "user-home": { + "version": "1.1.1", + "from": "user-home@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz" + }, + "xml-escape": { + "version": "1.0.0", + "from": "xml-escape@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/xml-escape/-/xml-escape-1.0.0.tgz" + } + } + }, + "eslint-plugin-react": { + "version": "3.3.1", + "from": "eslint-plugin-react@3.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-3.3.1.tgz" + }, "graceful-fs": { "version": "4.1.2", "from": "graceful-fs@4.1.2", @@ -1207,10 +2058,910 @@ "from": "immutable@>=3.7.4 <4.0.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.5.tgz" }, + "jest-cli": { + "version": "0.5.1", + "from": "jest-cli@0.5.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-0.5.1.tgz", + "dependencies": { + "coffee-script": { + "version": "1.10.0", + "from": "jashkenas/coffeescript", + "resolved": "git://github.com/jashkenas/coffeescript.git#8711da03a27bac6ec056dc9535e0ac29065d8ea6" + }, + "cover": { + "version": "0.2.9", + "from": "cover@>=0.2.9 <0.3.0", + "resolved": "https://registry.npmjs.org/cover/-/cover-0.2.9.tgz", + "dependencies": { + "cli-table": { + "version": "0.0.2", + "from": "cli-table@>=0.0.0 <0.1.0", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.0.2.tgz", + "dependencies": { + "colors": { + "version": "0.3.0", + "from": "colors@0.3.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.3.0.tgz" + } + } + }, + "underscore": { + "version": "1.2.4", + "from": "underscore@>=1.2.0 <1.3.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.2.4.tgz" + }, + "underscore.string": { + "version": "2.0.0", + "from": "underscore.string@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.0.0.tgz" + }, + "which": { + "version": "1.0.9", + "from": "which@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz" + } + } + }, + "diff": { + "version": "2.1.0", + "from": "diff@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-2.1.0.tgz" + }, + "istanbul": { + "version": "0.3.19", + "from": "istanbul@>=0.3.15 <0.4.0", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.3.19.tgz", + "dependencies": { + "esprima": { + "version": "2.5.0", + "from": "esprima@>=2.5.0 <2.6.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.5.0.tgz" + }, + "escodegen": { + "version": "1.6.1", + "from": "escodegen@>=1.6.0 <1.7.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.6.1.tgz", + "dependencies": { + "estraverse": { + "version": "1.9.3", + "from": "estraverse@>=1.9.1 <2.0.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz" + }, + "esutils": { + "version": "1.1.6", + "from": "esutils@>=1.1.6 <2.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz" + }, + "esprima": { + "version": "1.2.5", + "from": "esprima@>=1.2.2 <2.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.5.tgz" + }, + "optionator": { + "version": "0.5.0", + "from": "optionator@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.5.0.tgz", + "dependencies": { + "prelude-ls": { + "version": "1.1.2", + "from": "prelude-ls@>=1.1.1 <1.2.0", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" + }, + "deep-is": { + "version": "0.1.3", + "from": "deep-is@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz" + }, + "type-check": { + "version": "0.3.1", + "from": "type-check@>=0.3.1 <0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.1.tgz" + }, + "levn": { + "version": "0.2.5", + "from": "levn@>=0.2.5 <0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.2.5.tgz" + }, + "fast-levenshtein": { + "version": "1.0.7", + "from": "fast-levenshtein@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.0.7.tgz" + } + } + }, + "source-map": { + "version": "0.1.43", + "from": "source-map@>=0.1.40 <0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "dependencies": { + "amdefine": { + "version": "1.0.0", + "from": "amdefine@>=0.0.4", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" + } + } + } + } + }, + "handlebars": { + "version": "3.0.0", + "from": "handlebars@3.0.0", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-3.0.0.tgz", + "dependencies": { + "source-map": { + "version": "0.1.43", + "from": "source-map@>=0.1.40 <0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "dependencies": { + "amdefine": { + "version": "1.0.0", + "from": "amdefine@>=0.0.4", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" + } + } + }, + "uglify-js": { + "version": "2.3.6", + "from": "uglify-js@>=2.3.0 <2.4.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.3.6.tgz", + "dependencies": { + "async": { + "version": "0.2.10", + "from": "async@>=0.2.6 <0.3.0", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz" + }, + "optimist": { + "version": "0.3.7", + "from": "optimist@>=0.3.5 <0.4.0", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz" + } + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "dependencies": { + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + } + }, + "nopt": { + "version": "3.0.3", + "from": "nopt@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.3.tgz" + }, + "fileset": { + "version": "0.2.1", + "from": "fileset@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-0.2.1.tgz", + "dependencies": { + "minimatch": { + "version": "2.0.10", + "from": "minimatch@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "dependencies": { + "brace-expansion": { + "version": "1.1.0", + "from": "brace-expansion@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.0.tgz", + "dependencies": { + "balanced-match": { + "version": "0.2.0", + "from": "balanced-match@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.2.0.tgz" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + } + } + } + }, + "glob": { + "version": "5.0.14", + "from": "glob@>=5.0.0 <6.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.14.tgz", + "dependencies": { + "inflight": { + "version": "1.0.4", + "from": "inflight@>=1.0.4 <2.0.0", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.4.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.1", + "from": "wrappy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" + } + } + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "path-is-absolute": { + "version": "1.0.0", + "from": "path-is-absolute@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" + } + } + } + } + }, + "which": { + "version": "1.0.9", + "from": "which@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz" + }, + "async": { + "version": "1.4.2", + "from": "async@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.4.2.tgz" + }, + "supports-color": { + "version": "1.3.1", + "from": "supports-color@>=1.3.0 <1.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.3.1.tgz" + }, + "abbrev": { + "version": "1.0.7", + "from": "abbrev@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz" + }, + "wordwrap": { + "version": "0.0.3", + "from": "wordwrap@>=0.0.0 <0.1.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" + }, + "js-yaml": { + "version": "3.4.0", + "from": "js-yaml@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.4.0.tgz", + "dependencies": { + "argparse": { + "version": "1.0.2", + "from": "argparse@>=1.0.2 <1.1.0", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.2.tgz", + "dependencies": { + "lodash": { + "version": "3.10.1", + "from": "lodash@>=3.2.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz" + }, + "sprintf-js": { + "version": "1.0.3", + "from": "sprintf-js@>=1.0.2 <1.1.0", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" + } + } + }, + "esprima": { + "version": "2.2.0", + "from": "esprima@>=2.2.0 <2.3.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.2.0.tgz" + } + } + }, + "once": { + "version": "1.3.2", + "from": "once@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.2.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.1", + "from": "wrappy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" + } + } + } + } + }, + "jasmine-only": { + "version": "0.1.1", + "from": "jasmine-only@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/jasmine-only/-/jasmine-only-0.1.1.tgz", + "dependencies": { + "coffee-script": { + "version": "1.6.3", + "from": "coffee-script@>=1.6.3 <1.7.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.3.tgz" + } + } + }, + "jasmine-pit": { + "version": "2.0.2", + "from": "jasmine-pit@>=2.0.2 <3.0.0", + "resolved": "https://registry.npmjs.org/jasmine-pit/-/jasmine-pit-2.0.2.tgz" + }, + "jsdom": { + "version": "6.3.0", + "from": "jsdom@6.3.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-6.3.0.tgz", + "dependencies": { + "acorn": { + "version": "1.2.2", + "from": "acorn@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz" + }, + "acorn-globals": { + "version": "1.0.5", + "from": "acorn-globals@>=1.0.4 <2.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.5.tgz", + "dependencies": { + "acorn": { + "version": "2.4.0", + "from": "acorn@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.4.0.tgz" + } + } + }, + "browser-request": { + "version": "0.3.3", + "from": "browser-request@>=0.3.1 <0.4.0", + "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz" + }, + "cssom": { + "version": "0.3.0", + "from": "cssom@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.0.tgz" + }, + "cssstyle": { + "version": "0.2.29", + "from": "cssstyle@>=0.2.29 <0.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.29.tgz" + }, + "escodegen": { + "version": "1.6.1", + "from": "escodegen@>=1.6.1 <2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.6.1.tgz", + "dependencies": { + "estraverse": { + "version": "1.9.3", + "from": "estraverse@>=1.9.1 <2.0.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz" + }, + "esutils": { + "version": "1.1.6", + "from": "esutils@>=1.1.6 <2.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz" + }, + "esprima": { + "version": "1.2.5", + "from": "esprima@>=1.2.2 <2.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.5.tgz" + }, + "optionator": { + "version": "0.5.0", + "from": "optionator@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.5.0.tgz", + "dependencies": { + "prelude-ls": { + "version": "1.1.2", + "from": "prelude-ls@>=1.1.1 <1.2.0", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" + }, + "deep-is": { + "version": "0.1.3", + "from": "deep-is@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz" + }, + "wordwrap": { + "version": "0.0.3", + "from": "wordwrap@>=0.0.2 <0.1.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" + }, + "type-check": { + "version": "0.3.1", + "from": "type-check@>=0.3.1 <0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.1.tgz" + }, + "levn": { + "version": "0.2.5", + "from": "levn@>=0.2.5 <0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.2.5.tgz" + }, + "fast-levenshtein": { + "version": "1.0.7", + "from": "fast-levenshtein@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.0.7.tgz" + } + } + }, + "source-map": { + "version": "0.1.43", + "from": "source-map@>=0.1.40 <0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "dependencies": { + "amdefine": { + "version": "1.0.0", + "from": "amdefine@>=0.0.4", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" + } + } + } + } + }, + "htmlparser2": { + "version": "3.8.3", + "from": "htmlparser2@>=3.7.3 <4.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "dependencies": { + "domhandler": { + "version": "2.3.0", + "from": "domhandler@>=2.3.0 <2.4.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz" + }, + "domutils": { + "version": "1.5.1", + "from": "domutils@>=1.5.0 <1.6.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "dependencies": { + "dom-serializer": { + "version": "0.1.0", + "from": "dom-serializer@>=0.0.0 <1.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "from": "domelementtype@>=1.1.1 <1.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz" + }, + "entities": { + "version": "1.1.1", + "from": "entities@>=1.1.1 <1.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz" + } + } + } + } + }, + "domelementtype": { + "version": "1.3.0", + "from": "domelementtype@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz" + }, + "readable-stream": { + "version": "1.1.13", + "from": "readable-stream@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.13.tgz", + "dependencies": { + "core-util-is": { + "version": "1.0.1", + "from": "core-util-is@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz" + }, + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@>=2.0.1 <2.1.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + }, + "entities": { + "version": "1.0.0", + "from": "entities@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz" + } + } + }, + "nwmatcher": { + "version": "1.3.6", + "from": "nwmatcher@>=1.3.6 <2.0.0", + "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.3.6.tgz" + }, + "parse5": { + "version": "1.5.0", + "from": "parse5@>=1.4.2 <2.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.0.tgz" + }, + "request": { + "version": "2.61.0", + "from": "request@>=2.55.0 <3.0.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.61.0.tgz", + "dependencies": { + "bl": { + "version": "1.0.0", + "from": "bl@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.0.tgz", + "dependencies": { + "readable-stream": { + "version": "2.0.2", + "from": "readable-stream@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.2.tgz", + "dependencies": { + "core-util-is": { + "version": "1.0.1", + "from": "core-util-is@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@>=2.0.1 <2.1.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "process-nextick-args": { + "version": "1.0.2", + "from": "process-nextick-args@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.2.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "util-deprecate": { + "version": "1.0.1", + "from": "util-deprecate@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.1.tgz" + } + } + } + } + }, + "caseless": { + "version": "0.11.0", + "from": "caseless@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" + }, + "extend": { + "version": "3.0.0", + "from": "extend@>=3.0.0 <3.1.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" + }, + "forever-agent": { + "version": "0.6.1", + "from": "forever-agent@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + }, + "form-data": { + "version": "1.0.0-rc3", + "from": "form-data@>=1.0.0-rc1 <1.1.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc3.tgz", + "dependencies": { + "async": { + "version": "1.4.2", + "from": "async@>=1.4.0 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.4.2.tgz" + } + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "from": "json-stringify-safe@>=5.0.0 <5.1.0", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + }, + "mime-types": { + "version": "2.1.6", + "from": "mime-types@>=2.1.2 <2.2.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.6.tgz", + "dependencies": { + "mime-db": { + "version": "1.18.0", + "from": "mime-db@>=1.18.0 <1.19.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.18.0.tgz" + } + } + }, + "node-uuid": { + "version": "1.4.3", + "from": "node-uuid@>=1.4.0 <1.5.0", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz" + }, + "qs": { + "version": "4.0.0", + "from": "qs@>=4.0.0 <4.1.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz" + }, + "tunnel-agent": { + "version": "0.4.1", + "from": "tunnel-agent@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.1.tgz" + }, + "http-signature": { + "version": "0.11.0", + "from": "http-signature@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.11.0.tgz", + "dependencies": { + "assert-plus": { + "version": "0.1.5", + "from": "assert-plus@>=0.1.5 <0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz" + }, + "asn1": { + "version": "0.1.11", + "from": "asn1@0.1.11", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz" + }, + "ctype": { + "version": "0.5.3", + "from": "ctype@0.5.3", + "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz" + } + } + }, + "oauth-sign": { + "version": "0.8.0", + "from": "oauth-sign@>=0.8.0 <0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.0.tgz" + }, + "hawk": { + "version": "3.1.0", + "from": "hawk@>=3.1.0 <3.2.0", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.0.tgz", + "dependencies": { + "hoek": { + "version": "2.14.0", + "from": "hoek@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.14.0.tgz" + }, + "boom": { + "version": "2.8.0", + "from": "boom@>=2.8.0 <3.0.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.8.0.tgz" + }, + "cryptiles": { + "version": "2.0.4", + "from": "cryptiles@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.4.tgz" + }, + "sntp": { + "version": "1.0.9", + "from": "sntp@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" + } + } + }, + "aws-sign2": { + "version": "0.5.0", + "from": "aws-sign2@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz" + }, + "stringstream": { + "version": "0.0.4", + "from": "stringstream@>=0.0.4 <0.1.0", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.4.tgz" + }, + "combined-stream": { + "version": "1.0.5", + "from": "combined-stream@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "dependencies": { + "delayed-stream": { + "version": "1.0.0", + "from": "delayed-stream@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + } + } + }, + "isstream": { + "version": "0.1.2", + "from": "isstream@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + }, + "har-validator": { + "version": "1.8.0", + "from": "har-validator@>=1.6.1 <2.0.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-1.8.0.tgz", + "dependencies": { + "bluebird": { + "version": "2.9.34", + "from": "bluebird@>=2.9.30 <3.0.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.34.tgz" + }, + "commander": { + "version": "2.8.1", + "from": "commander@>=2.8.1 <3.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "dependencies": { + "graceful-readlink": { + "version": "1.0.1", + "from": "graceful-readlink@>=1.0.0", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" + } + } + }, + "is-my-json-valid": { + "version": "2.12.2", + "from": "is-my-json-valid@>=2.12.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.12.2.tgz", + "dependencies": { + "generate-function": { + "version": "2.0.0", + "from": "generate-function@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" + }, + "generate-object-property": { + "version": "1.2.0", + "from": "generate-object-property@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "dependencies": { + "is-property": { + "version": "1.0.2", + "from": "is-property@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" + } + } + }, + "jsonpointer": { + "version": "2.0.0", + "from": "jsonpointer@2.0.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" + } + } + } + } + } + } + }, + "symbol-tree": { + "version": "3.1.2", + "from": "symbol-tree@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.1.2.tgz" + }, + "tough-cookie": { + "version": "1.2.0", + "from": "tough-cookie@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-1.2.0.tgz" + }, + "whatwg-url-compat": { + "version": "0.6.5", + "from": "whatwg-url-compat@>=0.6.5 <0.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url-compat/-/whatwg-url-compat-0.6.5.tgz", + "dependencies": { + "tr46": { + "version": "0.0.2", + "from": "tr46@>=0.0.1 <0.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.2.tgz" + } + } + }, + "xml-name-validator": { + "version": "2.0.1", + "from": "xml-name-validator@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz" + }, + "xmlhttprequest": { + "version": "1.7.0", + "from": "xmlhttprequest@>=1.6.0 <2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.7.0.tgz" + }, + "xtend": { + "version": "4.0.0", + "from": "xtend@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.0.tgz" + } + } + }, + "lodash.template": { + "version": "3.6.2", + "from": "lodash.template@>=3.6.1 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "dependencies": { + "lodash._basecopy": { + "version": "3.0.1", + "from": "lodash._basecopy@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz" + }, + "lodash._basetostring": { + "version": "3.0.1", + "from": "lodash._basetostring@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz" + }, + "lodash._basevalues": { + "version": "3.0.0", + "from": "lodash._basevalues@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz" + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "from": "lodash._isiterateecall@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz" + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "from": "lodash._reinterpolate@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz" + }, + "lodash.escape": { + "version": "3.0.0", + "from": "lodash.escape@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.0.0.tgz" + }, + "lodash.keys": { + "version": "3.1.2", + "from": "lodash.keys@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "dependencies": { + "lodash._getnative": { + "version": "3.9.1", + "from": "lodash._getnative@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz" + }, + "lodash.isarguments": { + "version": "3.0.4", + "from": "lodash.isarguments@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.0.4.tgz" + }, + "lodash.isarray": { + "version": "3.0.4", + "from": "lodash.isarray@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" + } + } + }, + "lodash.restparam": { + "version": "3.6.1", + "from": "lodash.restparam@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz" + }, + "lodash.templatesettings": { + "version": "3.1.0", + "from": "lodash.templatesettings@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.0.tgz" + } + } + }, + "node-haste": { + "version": "1.2.8", + "from": "node-haste@>=1.2.8 <2.0.0", + "resolved": "https://registry.npmjs.org/node-haste/-/node-haste-1.2.8.tgz", + "dependencies": { + "esprima-fb": { + "version": "4001.1001.0-dev-harmony-fb", + "from": "esprima-fb@4001.1001.0-dev-harmony-fb", + "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-4001.1001.0-dev-harmony-fb.tgz" + } + } + }, + "node-worker-pool": { + "version": "3.0.0", + "from": "node-worker-pool@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/node-worker-pool/-/node-worker-pool-3.0.0.tgz" + }, + "object-assign": { + "version": "4.0.1", + "from": "object-assign@>=4.0.1 <5.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.0.1.tgz" + }, + "resolve": { + "version": "1.1.6", + "from": "resolve@>=1.1.6 <2.0.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.6.tgz" + }, + "through": { + "version": "2.3.8", + "from": "through@>=2.3.7 <3.0.0", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + } + } + }, "joi": { - "version": "5.1.0", - "from": "joi@5.1.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-5.1.0.tgz", + "version": "6.6.1", + "from": "joi@6.6.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-6.6.1.tgz", "dependencies": { "hoek": { "version": "2.14.0", @@ -1235,9 +2986,9 @@ } }, "jstransform": { - "version": "11.0.1", - "from": "jstransform@11.0.1", - "resolved": "https://registry.npmjs.org/jstransform/-/jstransform-11.0.1.tgz", + "version": "11.0.3", + "from": "jstransform@11.0.3", + "resolved": "https://registry.npmjs.org/jstransform/-/jstransform-11.0.3.tgz", "dependencies": { "base62": { "version": "1.1.0", @@ -1372,35 +3123,23 @@ "version": "2.1.1", "from": "object-assign@>=2.0.0 <3.0.0", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz" - }, - "source-map": { - "version": "0.4.4", - "from": "source-map@>=0.4.2 <0.5.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "dependencies": { - "amdefine": { - "version": "1.0.0", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" - } - } } } }, "module-deps": { - "version": "3.5.6", - "from": "module-deps@3.5.6", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-3.5.6.tgz", + "version": "3.9.1", + "from": "module-deps@3.9.1", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-3.9.1.tgz", "dependencies": { "JSONStream": { - "version": "0.7.4", - "from": "JSONStream@>=0.7.1 <0.8.0", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.7.4.tgz", + "version": "1.0.4", + "from": "JSONStream@>=1.0.3 <2.0.0", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.0.4.tgz", "dependencies": { "jsonparse": { - "version": "0.0.5", - "from": "jsonparse@0.0.5", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz" + "version": "1.0.0", + "from": "jsonparse@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.0.0.tgz" }, "through": { "version": "2.3.8", @@ -1411,15 +3150,8 @@ }, "browser-resolve": { "version": "1.9.1", - "from": "browser-resolve@>=1.3.1 <2.0.0", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.9.1.tgz", - "dependencies": { - "resolve": { - "version": "1.1.6", - "from": "resolve@1.1.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.6.tgz" - } - } + "from": "browser-resolve@>=1.7.0 <2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.9.1.tgz" }, "concat-stream": { "version": "1.4.10", @@ -1433,37 +3165,91 @@ } } }, + "defined": { + "version": "1.0.0", + "from": "defined@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz" + }, "detective": { - "version": "3.1.0", - "from": "detective@>=3.1.0 <4.0.0", - "resolved": "https://registry.npmjs.org/detective/-/detective-3.1.0.tgz", + "version": "4.2.0", + "from": "detective@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-4.2.0.tgz", "dependencies": { + "acorn": { + "version": "1.2.2", + "from": "acorn@>=1.0.3 <2.0.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz" + }, "escodegen": { - "version": "1.1.0", - "from": "escodegen@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.1.0.tgz", + "version": "1.6.1", + "from": "escodegen@>=1.4.1 <2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.6.1.tgz", "dependencies": { - "esprima": { - "version": "1.0.4", - "from": "esprima@>=1.0.4 <1.1.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz" - }, "estraverse": { - "version": "1.5.1", - "from": "estraverse@>=1.5.0 <1.6.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz" + "version": "1.9.3", + "from": "estraverse@>=1.9.1 <2.0.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz" }, "esutils": { - "version": "1.0.0", - "from": "esutils@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz" + "version": "1.1.6", + "from": "esutils@>=1.1.6 <2.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz" + }, + "esprima": { + "version": "1.2.5", + "from": "esprima@>=1.2.2 <2.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.5.tgz" + }, + "optionator": { + "version": "0.5.0", + "from": "optionator@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.5.0.tgz", + "dependencies": { + "prelude-ls": { + "version": "1.1.2", + "from": "prelude-ls@>=1.1.1 <1.2.0", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" + }, + "deep-is": { + "version": "0.1.3", + "from": "deep-is@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz" + }, + "wordwrap": { + "version": "0.0.3", + "from": "wordwrap@>=0.0.2 <0.1.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" + }, + "type-check": { + "version": "0.3.1", + "from": "type-check@>=0.3.1 <0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.1.tgz" + }, + "levn": { + "version": "0.2.5", + "from": "levn@>=0.2.5 <0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.2.5.tgz" + }, + "fast-levenshtein": { + "version": "1.0.7", + "from": "fast-levenshtein@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.0.7.tgz" + } + } + }, + "source-map": { + "version": "0.1.43", + "from": "source-map@>=0.1.40 <0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "dependencies": { + "amdefine": { + "version": "1.0.0", + "from": "amdefine@>=0.0.4", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" + } + } } } - }, - "esprima-fb": { - "version": "3001.1.0-dev-harmony-fb", - "from": "esprima-fb@3001.1.0-dev-harmony-fb", - "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz" } } }, @@ -1477,11 +3263,6 @@ "from": "inherits@>=2.0.1 <3.0.0", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, - "minimist": { - "version": "0.2.0", - "from": "minimist@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.2.0.tgz" - }, "parents": { "version": "1.0.1", "from": "parents@>=1.0.0 <2.0.0", @@ -1496,7 +3277,7 @@ }, "readable-stream": { "version": "1.1.13", - "from": "readable-stream@>=1.0.27-1 <2.0.0", + "from": "readable-stream@>=1.1.13 <2.0.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.13.tgz", "dependencies": { "core-util-is": { @@ -1517,14 +3298,9 @@ } }, "resolve": { - "version": "0.7.4", - "from": "resolve@>=0.7.2 <0.8.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.7.4.tgz" - }, - "shallow-copy": { - "version": "0.0.1", - "from": "shallow-copy@0.0.1", - "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz" + "version": "1.1.6", + "from": "resolve@>=1.1.3 <2.0.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.6.tgz" }, "stream-combiner2": { "version": "1.0.2", @@ -1568,57 +3344,26 @@ } }, "subarg": { - "version": "0.0.1", - "from": "subarg@0.0.1", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-0.0.1.tgz", + "version": "1.0.0", + "from": "subarg@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", "dependencies": { "minimist": { - "version": "0.0.10", - "from": "minimist@>=0.0.7 <0.1.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" + "version": "1.2.0", + "from": "minimist@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" } } }, "through2": { - "version": "0.4.2", - "from": "through2@>=0.4.1 <0.5.0", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", - "dependencies": { - "readable-stream": { - "version": "1.0.33", - "from": "readable-stream@>=1.0.17 <1.1.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz", - "dependencies": { - "core-util-is": { - "version": "1.0.1", - "from": "core-util-is@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz" - }, - "isarray": { - "version": "0.0.1", - "from": "isarray@0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" - }, - "string_decoder": { - "version": "0.10.31", - "from": "string_decoder@>=0.10.0 <0.11.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - } - } - }, - "xtend": { - "version": "2.1.2", - "from": "xtend@>=2.1.1 <2.2.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "dependencies": { - "object-keys": { - "version": "0.4.0", - "from": "object-keys@>=0.4.0 <0.5.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz" - } - } - } - } + "version": "1.1.1", + "from": "through2@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz" + }, + "xtend": { + "version": "4.0.0", + "from": "xtend@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.0.tgz" } } }, @@ -1686,18 +3431,6 @@ "from": "esprima-fb@>=15001.1001.0-dev-harmony-fb <15001.1002.0", "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz" }, - "source-map": { - "version": "0.4.4", - "from": "source-map@>=0.4.4 <0.5.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "dependencies": { - "amdefine": { - "version": "1.0.0", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" - } - } - }, "ast-types": { "version": "0.8.11", "from": "ast-types@0.8.11", @@ -1800,9 +3533,9 @@ } }, "rebound": { - "version": "0.0.12", - "from": "rebound@>=0.0.12 <0.0.13", - "resolved": "https://registry.npmjs.org/rebound/-/rebound-0.0.12.tgz" + "version": "0.0.13", + "from": "rebound@>=0.0.13 <0.0.14", + "resolved": "https://registry.npmjs.org/rebound/-/rebound-0.0.13.tgz" }, "regenerator": { "version": "0.8.36", @@ -1962,6 +3695,11 @@ "version": "0.1.2", "from": "tryor@>=0.1.2 <0.2.0", "resolved": "https://registry.npmjs.org/tryor/-/tryor-0.1.2.tgz" + }, + "yargs": { + "version": "1.3.3", + "from": "yargs@>=1.3.2 <1.4.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-1.3.3.tgz" } } }, @@ -1980,18 +3718,6 @@ "from": "recast@0.10.25", "resolved": "https://registry.npmjs.org/recast/-/recast-0.10.25.tgz", "dependencies": { - "source-map": { - "version": "0.4.4", - "from": "source-map@>=0.4.1 <0.5.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "dependencies": { - "amdefine": { - "version": "1.0.0", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" - } - } - }, "ast-types": { "version": "0.8.5", "from": "ast-types@0.8.5", @@ -2026,21 +3752,7 @@ "fb-watchman": { "version": "1.6.0", "from": "fb-watchman@>=1.5.0 <2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-1.6.0.tgz", - "dependencies": { - "bser": { - "version": "1.0.2", - "from": "bser@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-1.0.2.tgz", - "dependencies": { - "node-int64": { - "version": "0.4.0", - "from": "node-int64@>=0.4.0 <0.5.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" - } - } - } - } + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-1.6.0.tgz" }, "minimatch": { "version": "0.2.14", @@ -2091,14 +3803,14 @@ } }, "semver": { - "version": "4.3.6", - "from": "semver@>=4.3.6 <5.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz" + "version": "5.0.1", + "from": "semver@>=5.0.1 <6.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.1.tgz" }, "source-map": { - "version": "0.1.31", - "from": "source-map@0.1.31", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.31.tgz", + "version": "0.4.4", + "from": "source-map@0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "dependencies": { "amdefine": { "version": "1.0.0", @@ -2112,11 +3824,6 @@ "from": "stacktrace-parser@0.1.3", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.3.tgz" }, - "timed-out": { - "version": "2.0.0", - "from": "timed-out@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz" - }, "uglify-js": { "version": "2.4.24", "from": "uglify-js@2.4.24", @@ -2174,9 +3881,9 @@ } }, "underscore": { - "version": "1.7.0", - "from": "underscore@1.7.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz" + "version": "1.8.3", + "from": "underscore@1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz" }, "wordwrap": { "version": "1.0.0", @@ -2259,9 +3966,134 @@ } }, "yargs": { - "version": "1.3.2", - "from": "yargs@1.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-1.3.2.tgz" + "version": "3.24.0", + "from": "yargs@3.24.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.24.0.tgz", + "dependencies": { + "camelcase": { + "version": "1.2.1", + "from": "camelcase@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz" + }, + "cliui": { + "version": "2.1.0", + "from": "cliui@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "dependencies": { + "center-align": { + "version": "0.1.1", + "from": "center-align@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.1.tgz", + "dependencies": { + "align-text": { + "version": "0.1.3", + "from": "align-text@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.3.tgz", + "dependencies": { + "kind-of": { + "version": "2.0.1", + "from": "kind-of@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "dependencies": { + "is-buffer": { + "version": "1.0.2", + "from": "is-buffer@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.0.2.tgz" + } + } + }, + "longest": { + "version": "1.0.1", + "from": "longest@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz" + }, + "repeat-string": { + "version": "1.5.2", + "from": "repeat-string@>=1.5.2 <2.0.0", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.5.2.tgz" + } + } + } + } + }, + "right-align": { + "version": "0.1.3", + "from": "right-align@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "dependencies": { + "align-text": { + "version": "0.1.3", + "from": "align-text@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.3.tgz", + "dependencies": { + "kind-of": { + "version": "2.0.1", + "from": "kind-of@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "dependencies": { + "is-buffer": { + "version": "1.0.2", + "from": "is-buffer@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.0.2.tgz" + } + } + }, + "longest": { + "version": "1.0.1", + "from": "longest@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz" + }, + "repeat-string": { + "version": "1.5.2", + "from": "repeat-string@>=1.5.2 <2.0.0", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.5.2.tgz" + } + } + } + } + }, + "wordwrap": { + "version": "0.0.2", + "from": "wordwrap@0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz" + } + } + }, + "decamelize": { + "version": "1.0.0", + "from": "decamelize@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.0.0.tgz" + }, + "os-locale": { + "version": "1.3.1", + "from": "os-locale@>=1.3.1 <2.0.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.3.1.tgz", + "dependencies": { + "lcid": { + "version": "1.0.0", + "from": "lcid@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "dependencies": { + "invert-kv": { + "version": "1.0.0", + "from": "invert-kv@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz" + } + } + } + } + }, + "window-size": { + "version": "0.1.2", + "from": "window-size@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.2.tgz" + }, + "y18n": { + "version": "3.1.0", + "from": "y18n@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.1.0.tgz" + } + } }, "yeoman-environment": { "version": "1.2.7", @@ -2297,7 +4129,7 @@ }, "async": { "version": "1.4.2", - "from": "async@>=1.2.1 <2.0.0", + "from": "async@>=1.4.2 <2.0.0", "resolved": "https://registry.npmjs.org/async/-/async-1.4.2.tgz" }, "glob": { @@ -2440,7 +4272,7 @@ }, "lodash": { "version": "3.10.1", - "from": "lodash@>=3.5.0 <4.0.0", + "from": "lodash@>=3.1.0 <4.0.0", "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz" }, "log-symbols": { @@ -2460,7 +4292,7 @@ "dependencies": { "readable-stream": { "version": "1.0.33", - "from": "readable-stream@>=1.0.17 <1.1.0", + "from": "readable-stream@>=1.0.33-1 <1.1.0-0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz", "dependencies": { "core-util-is": { @@ -2675,7 +4507,7 @@ }, "readable-stream": { "version": "2.0.2", - "from": "readable-stream@>=2.0.0 <3.0.0", + "from": "readable-stream@>=2.0.0 <2.1.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.2.tgz", "dependencies": { "core-util-is": { @@ -3126,6 +4958,11 @@ "version": "1.2.1", "from": "statuses@>=1.2.1 <2.0.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz" + }, + "timed-out": { + "version": "2.0.0", + "from": "timed-out@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz" } } }, @@ -3272,7 +5109,7 @@ }, "vinyl": { "version": "0.5.3", - "from": "vinyl@>=0.5.1 <0.6.0", + "from": "vinyl@>=0.5.0 <0.6.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", "dependencies": { "clone": { @@ -3838,7 +5675,7 @@ }, "vinyl": { "version": "0.5.3", - "from": "vinyl@>=0.5.1 <0.6.0", + "from": "vinyl@>=0.5.0 <0.6.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", "dependencies": { "clone": { @@ -4943,6 +6780,11 @@ } } } + }, + "timed-out": { + "version": "2.0.0", + "from": "timed-out@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz" } } }, @@ -4955,7 +6797,7 @@ }, "meow": { "version": "3.3.0", - "from": "meow@*", + "from": "meow@>=3.1.0 <4.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.3.0.tgz", "dependencies": { "camelcase-keys": { @@ -5544,7 +7386,7 @@ }, "meow": { "version": "3.3.0", - "from": "meow@*", + "from": "meow@>=3.1.0 <4.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.3.0.tgz", "dependencies": { "camelcase-keys": { diff --git a/package.json b/package.json index beb99bf45..3723c5141 100644 --- a/package.json +++ b/package.json @@ -47,42 +47,42 @@ }, "dependencies": { "absolute-path": "0.0.0", - "babel": "5.8.21", - "babel-core": "5.8.21", - "bser": "1.0.0", - "chalk": "1.0.0", - "connect": "2.8.3", - "debug": "2.1.0", + "babel": "5.8.23", + "babel-core": "5.8.23", + "bser": "1.0.2", + "chalk": "1.1.1", + "connect": "3.4.0", + "debug": "2.2.0", "graceful-fs": "4.1.2", "image-size": "0.3.5", - "immutable": "^3.7.4", - "joi": "5.1.0", - "jstransform": "11.0.1", - "module-deps": "3.5.6", + "immutable": "3.7.4", + "joi": "6.6.1", + "jstransform": "11.0.3", + "module-deps": "3.9.1", "optimist": "0.6.1", - "progress": "^1.1.8", - "promise": "^7.0.3", - "react-timer-mixin": "^0.13.1", + "progress": "1.1.8", + "promise": "7.0.3", + "react-timer-mixin": "0.13.1", "react-tools": "git://github.com/facebook/react#b4e74e38e43ac53af8acd62c78c9213be0194245", - "rebound": "^0.0.12", + "rebound": "0.0.13", "regenerator": "0.8.36", - "sane": "^1.1.2", - "semver": "^4.3.6", - "source-map": "0.1.31", + "sane": "1.1.2", + "semver": "5.0.1", + "source-map": "0.4.4", "stacktrace-parser": "0.1.3", "uglify-js": "2.4.24", - "underscore": "1.7.0", - "wordwrap": "^1.0.0", - "worker-farm": "^1.3.1", + "underscore": "1.8.3", + "wordwrap": "1.0.0", + "worker-farm": "1.3.1", "ws": "0.8.0", - "yargs": "1.3.2", - "yeoman-environment": "^1.2.7", - "yeoman-generator": "^0.20.2" + "yargs": "3.24.0", + "yeoman-environment": "1.2.7", + "yeoman-generator": "0.20.2" }, "devDependencies": { - "jest-cli": "0.5.0", - "babel-eslint": "3.1.5", - "eslint": "0.21.2", - "eslint-plugin-react": "2.3.0" + "jest-cli": "0.5.1", + "babel-eslint": "4.1.1", + "eslint": "1.3.1", + "eslint-plugin-react": "3.3.1" } } From 936e1d4a11c5ef93b136668f2b17d1fe9f721d57 Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Fri, 4 Sep 2015 15:21:58 -0700 Subject: [PATCH 0023/2013] [react-packager] Support platform extensions in image requires Summary: We don't currently support platform extensions in asset modules. This adds supports for it: ``` require('./a.png'); ``` Will require 'a.ios.png' if it exists and 'a.png' if it doesn't. --- .../AssetServer/__tests__/AssetServer-test.js | 69 ++++++++++ .../react-packager/src/AssetServer/index.js | 39 ++++-- .../__tests__/DependencyGraph-test.js | 85 ++++++++++++ .../DependencyGraph/index.js | 26 ++-- .../src/Server/__tests__/Server-test.js | 10 +- packager/react-packager/src/Server/index.js | 5 +- .../__tests__/extractAssetResolution-test.js | 52 -------- .../__tests__/getAssetDataFromName-test.js | 123 ++++++++++++++++++ .../__tests__/getPotentialPlatformExt-test.js | 24 ++++ .../src/lib/getAssetDataFromName.js | 38 +++++- .../src/lib/getPlatformExtension.js | 28 ++++ 11 files changed, 413 insertions(+), 86 deletions(-) delete mode 100644 packager/react-packager/src/lib/__tests__/extractAssetResolution-test.js create mode 100644 packager/react-packager/src/lib/__tests__/getAssetDataFromName-test.js create mode 100644 packager/react-packager/src/lib/__tests__/getPotentialPlatformExt-test.js create mode 100644 packager/react-packager/src/lib/getPlatformExtension.js diff --git a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js index 54ef70120..d36889149 100644 --- a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js +++ b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js @@ -1,6 +1,7 @@ 'use strict'; jest + .dontMock('../../lib/getPlatformExtension') .dontMock('../../lib/getAssetDataFromName') .dontMock('../'); @@ -47,6 +48,43 @@ describe('AssetServer', () => { ); }); + pit('should work for the simple case with platform ext', () => { + const server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png'], + }); + + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b.ios.png': 'b ios image', + 'b.android.png': 'b android image', + 'c.png': 'c general image', + 'c.android.png': 'c android image', + } + } + }); + + return Promise.all([ + server.get('imgs/b.png', 'ios').then( + data => expect(data).toBe('b ios image') + ), + server.get('imgs/b.png', 'android').then( + data => expect(data).toBe('b android image') + ), + server.get('imgs/c.png', 'android').then( + data => expect(data).toBe('c android image') + ), + server.get('imgs/c.png', 'ios').then( + data => expect(data).toBe('c general image') + ), + server.get('imgs/c.png').then( + data => expect(data).toBe('c general image') + ), + ]); + }); + + pit('should work for the simple case with jpg', () => { const server = new AssetServer({ projectRoots: ['/root'], @@ -95,6 +133,37 @@ describe('AssetServer', () => { ); }); + pit('should pick the bigger one with platform ext', () => { + const server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png'], + }); + + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b@1x.png': 'b1 image', + 'b@2x.png': 'b2 image', + 'b@4x.png': 'b4 image', + 'b@4.5x.png': 'b4.5 image', + 'b@1x.ios.png': 'b1 ios image', + 'b@2x.ios.png': 'b2 ios image', + 'b@4x.ios.png': 'b4 ios image', + 'b@4.5x.ios.png': 'b4.5 ios image', + } + } + }); + + return Promise.all([ + server.get('imgs/b@3x.png').then(data => + expect(data).toBe('b4 image') + ), + server.get('imgs/b@3x.png', 'ios').then(data => + expect(data).toBe('b4 ios image') + ), + ]); + }); + pit('should support multiple project roots', () => { const server = new AssetServer({ projectRoots: ['/root', '/root2'], diff --git a/packager/react-packager/src/AssetServer/index.js b/packager/react-packager/src/AssetServer/index.js index f442f6b89..5e4c11273 100644 --- a/packager/react-packager/src/AssetServer/index.js +++ b/packager/react-packager/src/AssetServer/index.js @@ -20,7 +20,6 @@ const stat = Promise.denodeify(fs.stat); const readDir = Promise.denodeify(fs.readdir); const readFile = Promise.denodeify(fs.readFile); - const validateOpts = declareOpts({ projectRoots: { type: 'array', @@ -39,9 +38,9 @@ class AssetServer { this._assetExts = opts.assetExts; } - get(assetPath) { + get(assetPath, platform = null) { const assetData = getAssetDataFromName(assetPath); - return this._getAssetRecord(assetPath).then(record => { + return this._getAssetRecord(assetPath, platform).then(record => { for (let i = 0; i < record.scales.length; i++) { if (record.scales[i] >= assetData.resolution) { return readFile(record.files[i]); @@ -85,9 +84,10 @@ class AssetServer { * 1. We first parse the directory of the asset * 2. We check to find a matching directory in one of the project roots * 3. We then build a map of all assets and their scales in this directory - * 4. Then pick the closest resolution (rounding up) to the requested one + * 4. Then try to pick platform-specific asset records + * 5. Then pick the closest resolution (rounding up) to the requested one */ - _getAssetRecord(assetPath) { + _getAssetRecord(assetPath, platform = null) { const filename = path.basename(assetPath); return ( @@ -104,11 +104,20 @@ class AssetServer { const files = res[1]; const assetData = getAssetDataFromName(filename); - const map = this._buildAssetMap(dir, files); - const record = map[assetData.assetName]; + const map = this._buildAssetMap(dir, files, platform); + + let record; + if (platform != null){ + record = map[getAssetKey(assetData.assetName, platform)] || + map[assetData.assetName]; + } else { + record = map[assetData.assetName]; + } if (!record) { - throw new Error('Asset not found'); + throw new Error( + `Asset not found: ${assetPath} for platform: ${platform}` + ); } return record; @@ -141,9 +150,10 @@ class AssetServer { const map = Object.create(null); assets.forEach(function(asset, i) { const file = files[i]; - let record = map[asset.assetName]; + const assetKey = getAssetKey(asset.assetName, asset.platform); + let record = map[assetKey]; if (!record) { - record = map[asset.assetName] = { + record = map[assetKey] = { scales: [], files: [], }; @@ -151,6 +161,7 @@ class AssetServer { let insertIndex; const length = record.scales.length; + for (insertIndex = 0; insertIndex < length; insertIndex++) { if (asset.resolution < record.scales[insertIndex]) { break; @@ -164,4 +175,12 @@ class AssetServer { } } +function getAssetKey(assetName, platform) { + if (platform != null) { + return `${assetName} : ${platform}`; + } else { + return assetName; + } +} + module.exports = AssetServer; diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js index bf4f65964..6dc2bc7ee 100644 --- a/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js @@ -16,6 +16,7 @@ jest .dontMock('../../crawlers') .dontMock('../../crawlers/node') .dontMock('../../replacePatterns') + .dontMock('../../../lib/getPlatformExtension') .dontMock('../../../lib/getAssetDataFromName') .dontMock('../../fastfs') .dontMock('../../AssetModule_DEPRECATED') @@ -423,6 +424,90 @@ describe('DependencyGraph', function() { }); }); + pit('should respect platform extension in assets', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./imgs/a.png");', + 'require("./imgs/b.png");', + 'require("./imgs/c.png");', + ].join('\n'), + 'imgs': { + 'a@1.5x.ios.png': '', + 'b@.7x.ios.png': '', + 'c.ios.png': '', + 'c@2x.ios.png': '', + }, + 'package.json': JSON.stringify({ + name: 'rootPackage' + }), + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + cache: cache, + }); + + dgraph.setup({ platform: 'ios' }); + + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: [ + './imgs/a.png', + './imgs/b.png', + './imgs/c.png', + ], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'rootPackage/imgs/a.png', + path: '/root/imgs/a@1.5x.ios.png', + resolution: 1.5, + dependencies: [], + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + { + id: 'rootPackage/imgs/b.png', + path: '/root/imgs/b@.7x.ios.png', + resolution: 0.7, + dependencies: [], + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + { + id: 'rootPackage/imgs/c.png', + path: '/root/imgs/c.ios.png', + resolution: 1, + dependencies: [], + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + ]); + }); + }); + pit('Deprecated and relative assets can live together', function() { var root = '/root'; fs.__setMockFilesystem({ diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js index f2b77f4d5..0820c14a9 100644 --- a/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js @@ -1,4 +1,4 @@ -/** + /** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * @@ -17,6 +17,7 @@ const crawl = require('../crawlers'); const debug = require('debug')('DependencyGraph'); const declareOpts = require('../../lib/declareOpts'); const getAssetDataFromName = require('../../lib/getAssetDataFromName'); +const getPontentialPlatformExt = require('../../lib/getPlatformExtension'); const isAbsolutePath = require('absolute-path'); const path = require('path'); const util = require('util'); @@ -274,7 +275,7 @@ class DependencyGraph { // `platformExt` could be set in the `setup` method. if (!this._platformExt) { - const platformExt = getPlatformExt(entryPath); + const platformExt = getPontentialPlatformExt(entryPath); if (platformExt && this._opts.platforms.indexOf(platformExt) > -1) { this._platformExt = platformExt; } else { @@ -390,12 +391,18 @@ class DependencyGraph { return Promise.resolve().then(() => { if (this._isAssetFile(potentialModulePath)) { const {name, type} = getAssetDataFromName(potentialModulePath); - const pattern = new RegExp('^' + name + '(@[\\d\\.]+x)?\\.' + type); + + let pattern = '^' + name + '(@[\\d\\.]+x)?'; + if (this._platformExt != null) { + pattern += '(\\.' + this._platformExt + ')?'; + } + pattern += '\\.' + type; + // We arbitrarly grab the first one, because scale selection // will happen somewhere const [assetFile] = this._fastfs.matches( path.dirname(potentialModulePath), - pattern + new RegExp(pattern) ); if (assetFile) { @@ -496,7 +503,7 @@ class DependencyGraph { const modules = this._hasteMap[name]; if (this._platformExt != null) { for (let i = 0; i < modules.length; i++) { - if (getPlatformExt(modules[i].path) === this._platformExt) { + if (getPontentialPlatformExt(modules[i].path) === this._platformExt) { return modules[i]; } } @@ -662,15 +669,6 @@ function normalizePath(modulePath) { return modulePath.replace(/\/$/, ''); } -// Extract platform extension: index.ios.js -> ios -function getPlatformExt(file) { - const parts = path.basename(file).split('.'); - if (parts.length < 3) { - return null; - } - return parts[parts.length - 2]; -} - util.inherits(NotFoundError, Error); module.exports = DependencyGraph; diff --git a/packager/react-packager/src/Server/__tests__/Server-test.js b/packager/react-packager/src/Server/__tests__/Server-test.js index 5152054ee..64af474ae 100644 --- a/packager/react-packager/src/Server/__tests__/Server-test.js +++ b/packager/react-packager/src/Server/__tests__/Server-test.js @@ -244,8 +244,16 @@ describe('processRequest', () => { expect(res.end).toBeCalledWith('i am image'); }); - it('should return 404', () => { + it('should parse the platform option', () => { + const req = {url: '/assets/imgs/a.png?platform=ios'}; + const res = {end: jest.genMockFn()}; + AssetServer.prototype.get.mockImpl(() => Promise.resolve('i am image')); + + server.processRequest(req, res); + jest.runAllTimers(); + expect(AssetServer.prototype.get).toBeCalledWith('imgs/a.png', 'ios'); + expect(res.end).toBeCalledWith('i am image'); }); }); diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 7d65d0a2e..e889a357e 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -278,7 +278,8 @@ class Server { _processAssetsRequest(req, res) { const urlObj = url.parse(req.url, true); const assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/); - this._assetServer.get(assetPath[1]) + const assetEvent = Activity.startEvent(`processing asset request ${assetPath[1]}`); + this._assetServer.get(assetPath[1], urlObj.query.platform) .then( data => res.end(data), error => { @@ -286,7 +287,7 @@ class Server { res.writeHead('404'); res.end('Asset not found'); } - ).done(); + ).done(() => Activity.endEvent(assetEvent)); } _processProfile(req, res) { diff --git a/packager/react-packager/src/lib/__tests__/extractAssetResolution-test.js b/packager/react-packager/src/lib/__tests__/extractAssetResolution-test.js deleted file mode 100644 index d0309ca6a..000000000 --- a/packager/react-packager/src/lib/__tests__/extractAssetResolution-test.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -jest.autoMockOff(); -var getAssetDataFromName = require('../getAssetDataFromName'); - -describe('getAssetDataFromName', function() { - it('should extract resolution simple case', function() { - var data = getAssetDataFromName('test@2x.png'); - expect(data).toEqual({ - assetName: 'test.png', - resolution: 2, - type: 'png', - name: 'test', - }); - }); - - it('should default resolution to 1', function() { - var data = getAssetDataFromName('test.png'); - expect(data).toEqual({ - assetName: 'test.png', - resolution: 1, - type: 'png', - name: 'test', - }); - }); - - it('should support float', function() { - var data = getAssetDataFromName('test@1.1x.png'); - expect(data).toEqual({ - assetName: 'test.png', - resolution: 1.1, - type: 'png', - name: 'test', - }); - - data = getAssetDataFromName('test@.1x.png'); - expect(data).toEqual({ - assetName: 'test.png', - resolution: 0.1, - type: 'png', - name: 'test', - }); - - data = getAssetDataFromName('test@0.2x.png'); - expect(data).toEqual({ - assetName: 'test.png', - resolution: 0.2, - type: 'png', - name: 'test', - }); - }); -}); diff --git a/packager/react-packager/src/lib/__tests__/getAssetDataFromName-test.js b/packager/react-packager/src/lib/__tests__/getAssetDataFromName-test.js new file mode 100644 index 000000000..d67110015 --- /dev/null +++ b/packager/react-packager/src/lib/__tests__/getAssetDataFromName-test.js @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.dontMock('../getPlatformExtension') + .dontMock('../getAssetDataFromName'); + +describe('getAssetDataFromName', () => { + let getAssetDataFromName; + + beforeEach(() => { + getAssetDataFromName = require('../getAssetDataFromName'); + }); + + it('should get data from name', () => { + expect(getAssetDataFromName('a/b/c.png')).toEqual({ + resolution: 1, + assetName: 'a/b/c.png', + type: 'png', + name: 'c', + platform: null, + }); + + expect(getAssetDataFromName('a/b/c@1x.png')).toEqual({ + resolution: 1, + assetName: 'a/b/c.png', + type: 'png', + name: 'c', + platform: null, + }); + + expect(getAssetDataFromName('a/b/c@2.5x.png')).toEqual({ + resolution: 2.5, + assetName: 'a/b/c.png', + type: 'png', + name: 'c', + platform: null, + }); + + expect(getAssetDataFromName('a/b/c.ios.png')).toEqual({ + resolution: 1, + assetName: 'a/b/c.png', + type: 'png', + name: 'c', + platform: 'ios', + }); + + expect(getAssetDataFromName('a/b/c@1x.ios.png')).toEqual({ + resolution: 1, + assetName: 'a/b/c.png', + type: 'png', + name: 'c', + platform: 'ios', + }); + + expect(getAssetDataFromName('a/b/c@2.5x.ios.png')).toEqual({ + resolution: 2.5, + assetName: 'a/b/c.png', + type: 'png', + name: 'c', + platform: 'ios', + }); + }); + + describe('resolution extraction', () => { + it('should extract resolution simple case', () => { + var data = getAssetDataFromName('test@2x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 2, + type: 'png', + name: 'test', + platform: null, + }); + }); + + it('should default resolution to 1', () => { + var data = getAssetDataFromName('test.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 1, + type: 'png', + name: 'test', + platform: null, + }); + }); + + it('should support float', () => { + var data = getAssetDataFromName('test@1.1x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 1.1, + type: 'png', + name: 'test', + platform: null, + }); + + data = getAssetDataFromName('test@.1x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 0.1, + type: 'png', + name: 'test', + platform: null, + }); + + data = getAssetDataFromName('test@0.2x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 0.2, + type: 'png', + name: 'test', + platform: null, + }); + }); + }); +}); diff --git a/packager/react-packager/src/lib/__tests__/getPotentialPlatformExt-test.js b/packager/react-packager/src/lib/__tests__/getPotentialPlatformExt-test.js new file mode 100644 index 000000000..5f734880e --- /dev/null +++ b/packager/react-packager/src/lib/__tests__/getPotentialPlatformExt-test.js @@ -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. + */ +'use strict'; + +jest.dontMock('../getPlatformExtension'); + +describe('getPlatformExtension', function() { + it('should get platform ext', function() { + var getPlatformExtension = require('../getPlatformExtension'); + expect(getPlatformExtension('a.ios.js')).toBe('ios'); + expect(getPlatformExtension('a.android.js')).toBe('android'); + expect(getPlatformExtension('/b/c/a.ios.js')).toBe('ios'); + expect(getPlatformExtension('/b/c.android/a.ios.js')).toBe('ios'); + expect(getPlatformExtension('/b/c/a@1.5x.ios.png')).toBe('ios'); + expect(getPlatformExtension('/b/c/a@1.5x.lol.png')).toBe(null); + expect(getPlatformExtension('/b/c/a.lol.png')).toBe(null); + }); +}); diff --git a/packager/react-packager/src/lib/getAssetDataFromName.js b/packager/react-packager/src/lib/getAssetDataFromName.js index c4848fd17..33fa13cea 100644 --- a/packager/react-packager/src/lib/getAssetDataFromName.js +++ b/packager/react-packager/src/lib/getAssetDataFromName.js @@ -1,14 +1,29 @@ + /** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ 'use strict'; -var path = require('path'); +const path = require('path'); +const getPlatformExtension = require('./getPlatformExtension'); function getAssetDataFromName(filename) { - var ext = path.extname(filename); + const ext = path.extname(filename); + const platformExt = getPlatformExtension(filename); - var re = new RegExp('@([\\d\\.]+)x\\' + ext + '$'); + let pattern = '@([\\d\\.]+)x'; + if (platformExt != null) { + pattern += '(\\.' + platformExt + ')?'; + } + pattern += '\\' + ext + '$'; + const re = new RegExp(pattern); - var match = filename.match(re); - var resolution; + const match = filename.match(re); + let resolution; if (!(match && match[1])) { resolution = 1; @@ -19,12 +34,21 @@ function getAssetDataFromName(filename) { } } - var assetName = match ? filename.replace(re, ext) : filename; + let assetName; + if (match) { + assetName = filename.replace(re, ext); + } else if (platformExt != null) { + assetName = filename.replace(new RegExp(`\\.${platformExt}\\${ext}`), ext); + } else { + assetName = filename; + } + return { resolution: resolution, assetName: assetName, type: ext.slice(1), - name: path.basename(assetName, ext) + name: path.basename(assetName, ext), + platform: platformExt, }; } diff --git a/packager/react-packager/src/lib/getPlatformExtension.js b/packager/react-packager/src/lib/getPlatformExtension.js new file mode 100644 index 000000000..3f34da421 --- /dev/null +++ b/packager/react-packager/src/lib/getPlatformExtension.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const path = require('path'); + +const SUPPORTED_PLATFORM_EXTS = ['android', 'ios']; + +const re = new RegExp( + '[^\\.]+\\.(' + SUPPORTED_PLATFORM_EXTS.join('|') + ')\\.\\w+$' +); + +// Extract platform extension: index.ios.js -> ios +function getPlatformExtension(file) { + const match = file.match(re); + if (match && match[1]) { + return match[1]; + } + return null; +} + +module.exports = getPlatformExtension; From 874f39bf7f5f0ac19ff78447aef589d23cd429d0 Mon Sep 17 00:00:00 2001 From: Gabe Levi Date: Fri, 4 Sep 2015 17:00:21 -0700 Subject: [PATCH 0024/2013] [Flow] Clean up react-native for Flow v0.15.0 --- Examples/Movies/getStyleFromScore.js | 4 +- Libraries/Devtools/setupDevtools.js | 4 +- Libraries/Network/NetInfo.js | 98 ++++++++++++------------- Libraries/StyleSheet/StyleSheetTypes.js | 15 ++++ Libraries/StyleSheet/flattenStyle.js | 3 +- 5 files changed, 70 insertions(+), 54 deletions(-) create mode 100644 Libraries/StyleSheet/StyleSheetTypes.js diff --git a/Examples/Movies/getStyleFromScore.js b/Examples/Movies/getStyleFromScore.js index 98feaa50b..1d5b599b6 100644 --- a/Examples/Movies/getStyleFromScore.js +++ b/Examples/Movies/getStyleFromScore.js @@ -22,7 +22,9 @@ var { var MAX_VALUE = 200; -function getStyleFromScore(score: number): {color: string} { +import type { StyleObj } from 'StyleSheetTypes'; + +function getStyleFromScore(score: number): StyleObj { if (score < 0) { return styles.noScore; } diff --git a/Libraries/Devtools/setupDevtools.js b/Libraries/Devtools/setupDevtools.js index 0bd003dca..93ba87555 100644 --- a/Libraries/Devtools/setupDevtools.js +++ b/Libraries/Devtools/setupDevtools.js @@ -72,7 +72,9 @@ function setupDevtools() { } function handleMessage(evt) { - var data; + // It's hard to handle JSON in a safe manner without inspecting it at + // runtime, hence the any + var data: any; try { data = JSON.parse(evt.data); } catch (e) { diff --git a/Libraries/Network/NetInfo.js b/Libraries/Network/NetInfo.js index f51a95529..5c45be617 100644 --- a/Libraries/Network/NetInfo.js +++ b/Libraries/Network/NetInfo.js @@ -138,6 +138,23 @@ type ConnectivityStateAndroid = $Enum<{ var _subscriptions = new Map(); +if (Platform.OS === 'ios') { + var _isConnected = function( + reachability: ReachabilityStateIOS + ): bool { + return reachability !== 'none' && + reachability !== 'unknown'; + }; +} else if (Platform.OS === 'android') { + var _isConnected = function( + connectionType: ConnectivityStateAndroid + ): bool { + return connectionType !== 'NONE' && connectionType !== 'UNKNOWN'; + }; +} + +var _isConnectedSubscriptions = new Map(); + var NetInfo = { addEventListener: function ( eventName: ChangeEventName, @@ -175,60 +192,41 @@ var NetInfo = { }); }, - isConnected: {}, + isConnected: { + addEventListener: function ( + eventName: ChangeEventName, + handler: Function + ): void { + var listener = (connection) => { + handler(_isConnected(connection)); + }; + _isConnectedSubscriptions.set(handler, listener); + NetInfo.addEventListener( + eventName, + listener + ); + }, - isConnectionMetered: {}, -}; + removeEventListener: function( + eventName: ChangeEventName, + handler: Function + ): void { + var listener = _isConnectedSubscriptions.get(handler); + NetInfo.removeEventListener( + eventName, + listener + ); + _isConnectedSubscriptions.delete(handler); + }, -if (Platform.OS === 'ios') { - var _isConnected = function( - reachability: ReachabilityStateIOS - ): bool { - return reachability !== 'none' && - reachability !== 'unknown'; - }; -} else if (Platform.OS === 'android') { - var _isConnected = function( - connectionType: ConnectivityStateAndroid - ): bool { - return connectionType !== 'NONE' && connectionType !== 'UNKNOWN'; - }; -} - -var _isConnectedSubscriptions = new Map(); - -NetInfo.isConnected = { - addEventListener: function ( - eventName: ChangeEventName, - handler: Function - ): void { - var listener = (connection) => { - handler(_isConnected(connection)); - }; - _isConnectedSubscriptions.set(handler, listener); - NetInfo.addEventListener( - eventName, - listener - ); + fetch: function(): Promise { + return NetInfo.fetch().then( + (connection) => _isConnected(connection) + ); + }, }, - removeEventListener: function( - eventName: ChangeEventName, - handler: Function - ): void { - var listener = _isConnectedSubscriptions.get(handler); - NetInfo.removeEventListener( - eventName, - listener - ); - _isConnectedSubscriptions.delete(handler); - }, - - fetch: function(): Promise { - return NetInfo.fetch().then( - (connection) => _isConnected(connection) - ); - }, + isConnectionMetered: ({}: {} | (callback:Function) => void), }; if (Platform.OS === 'android') { diff --git a/Libraries/StyleSheet/StyleSheetTypes.js b/Libraries/StyleSheet/StyleSheetTypes.js new file mode 100644 index 000000000..779e64772 --- /dev/null +++ b/Libraries/StyleSheet/StyleSheetTypes.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule StyleSheetTypes + * @flow + */ +'use strict'; + +type Atom = number | bool | Object | Array; +export type StyleObj = Atom | Array; diff --git a/Libraries/StyleSheet/flattenStyle.js b/Libraries/StyleSheet/flattenStyle.js index 50e061ede..621c614ff 100644 --- a/Libraries/StyleSheet/flattenStyle.js +++ b/Libraries/StyleSheet/flattenStyle.js @@ -14,8 +14,7 @@ var StyleSheetRegistry = require('StyleSheetRegistry'); var invariant = require('invariant'); -type Atom = number | bool | Object | Array -type StyleObj = Atom | Array +import type { StyleObj } from 'StyleSheetTypes'; function getStyle(style) { if (typeof style === 'number') { From 1f6f60582d64cd6b832040b04c7eabaf9ed8b823 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Sun, 6 Sep 2015 17:06:35 -0700 Subject: [PATCH 0026/2013] [RN] Prevent reloading library photos on every animation frame --- Libraries/Image/RCTImageView.m | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index 9b5ba8f80..75bd62ebe 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -31,6 +31,7 @@ @implementation RCTImageView { RCTBridge *_bridge; + CGSize _targetSize; } - (instancetype)initWithBridge:(RCTBridge *)bridge @@ -173,19 +174,17 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) { [super reactSetFrame:frame]; if (self.image == nil) { + _targetSize = frame.size; [self reloadImage]; } else if ([RCTImageView srcNeedsReload:_src]) { - - // Get optimal image size - CGSize currentSize = self.image.size; CGSize idealSize = RCTTargetSize(self.image.size, self.image.scale, frame.size, RCTScreenScale(), self.contentMode, YES); - - CGFloat widthChangeFraction = ABS(currentSize.width - idealSize.width) / currentSize.width; - CGFloat heightChangeFraction = ABS(currentSize.height - idealSize.height) / currentSize.height; + CGFloat widthChangeFraction = ABS(_targetSize.width - idealSize.width) / _targetSize.width; + CGFloat heightChangeFraction = ABS(_targetSize.height - idealSize.height) / _targetSize.height; // If the combined change is more than 20%, reload the asset in case there is a better size. if (widthChangeFraction + heightChangeFraction > 0.2) { + _targetSize = idealSize; [self reloadImage]; } } From 59b9dc8829377e9b8a048669bde8fd737c6166f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Mon, 7 Sep 2015 01:23:21 -0700 Subject: [PATCH 0027/2013] [react-packager] Add command line option to reset cache on OSS --- packager/packager.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packager/packager.js b/packager/packager.js index 8f0f08007..b5f789699 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -59,6 +59,14 @@ var options = parseCommandLine([{ type: 'string', default: require.resolve('./transformer.js'), description: 'Specify a custom transformer to be used (absolute path)' +}, { + command: 'resetCache', + description: 'Removes cached files', + default: false, +}, { + command: 'reset-cache', + description: 'Removes cached files', + default: false, }]); if (options.projectRoots) { @@ -229,6 +237,7 @@ function getAppMiddleware(options) { transformModulePath: transformerPath, assetRoots: options.assetRoots, assetExts: ['png', 'jpeg', 'jpg'], + resetCache: options.resetCache || options['reset-cache'], polyfillModuleNames: [ require.resolve( '../Libraries/JavaScriptAppEngine/polyfills/document.js' From bceab6c1c243c31abcc3297613ce46b93eaea7c5 Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Mon, 7 Sep 2015 10:44:02 -0700 Subject: [PATCH 0028/2013] [react-packager] Allow a longer startup time before the server dies Summary: 1. When the server starts up, it only gives itself 30 second to live before receiving any connections/jobs 2. There is a startup cost with starting the server and handshaking 3. The server dies before the client has a chance to connect to it Solution: 1. While the server should die pretty fast after it's done it's work, we should have a longer timeout for starting it 2. I also added accompanying server logs with client connection errors --- .../react-packager/src/SocketInterface/SocketClient.js | 8 +++++++- .../react-packager/src/SocketInterface/SocketServer.js | 7 ++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packager/react-packager/src/SocketInterface/SocketClient.js b/packager/react-packager/src/SocketInterface/SocketClient.js index 6ccfd4819..32e3b25d7 100644 --- a/packager/react-packager/src/SocketInterface/SocketClient.js +++ b/packager/react-packager/src/SocketInterface/SocketClient.js @@ -12,6 +12,7 @@ const Bundle = require('../Bundler/Bundle'); const Promise = require('promise'); const bser = require('bser'); const debug = require('debug')('ReactPackager:SocketClient'); +const fs = require('fs'); const net = require('net'); const path = require('path'); const tmpdir = require('os').tmpdir(); @@ -30,8 +31,13 @@ class SocketClient { this._ready = new Promise((resolve, reject) => { this._sock.on('connect', () => resolve(this)); this._sock.on('error', (e) => { - e.message = `Error connecting to server on ${sockPath}` + + e.message = `Error connecting to server on ${sockPath} ` + `with error: ${e.message}`; + + if (fs.existsSync(LOG_PATH)) { + e.message += '\nServer logs:\n' + fs.readFileSync(LOG_PATH, 'utf8'); + } + reject(e); }); }); diff --git a/packager/react-packager/src/SocketInterface/SocketServer.js b/packager/react-packager/src/SocketInterface/SocketServer.js index abdc094b7..888dcf78a 100644 --- a/packager/react-packager/src/SocketInterface/SocketServer.js +++ b/packager/react-packager/src/SocketInterface/SocketServer.js @@ -16,6 +16,7 @@ const fs = require('fs'); const net = require('net'); const MAX_IDLE_TIME = 30 * 1000; +const MAX_STARTUP_TIME = 5 * 60 * 1000; class SocketServer { constructor(sockPath, options) { @@ -43,7 +44,7 @@ class SocketServer { options.nonPersistent = true; this._packagerServer = new Server(options); this._jobs = 0; - this._dieEventually(); + this._dieEventually(MAX_STARTUP_TIME); } onReady() { @@ -118,7 +119,7 @@ class SocketServer { })); } - _dieEventually() { + _dieEventually(delay = MAX_IDLE_TIME) { clearTimeout(this._deathTimer); this._deathTimer = setTimeout(() => { if (this._jobs <= 0 && this._numConnections <= 0) { @@ -126,7 +127,7 @@ class SocketServer { process.exit(); } this._dieEventually(); - }, MAX_IDLE_TIME); + }, delay); } static listenOnServerIPCMessages() { From 8586f8932294b60f0160ba724dc40ca2971066e8 Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Tue, 8 Sep 2015 01:34:44 -0700 Subject: [PATCH 0029/2013] [react-packager] Update sane to fix bug and add new features Summary: Updates sane to get: 1. Fix error plumbing (surface watchman errors correctly) 2. Better capability testing 3. Use watchman's globbing 4. Other minor fixes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3723c5141..d4eff15d4 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "react-tools": "git://github.com/facebook/react#b4e74e38e43ac53af8acd62c78c9213be0194245", "rebound": "0.0.13", "regenerator": "0.8.36", - "sane": "1.1.2", + "sane": "^1.2.0", "semver": "5.0.1", "source-map": "0.4.4", "stacktrace-parser": "0.1.3", From ad0c97f25b64914256b2791b17dd19c8f9886f31 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 8 Sep 2015 03:27:44 -0700 Subject: [PATCH 0030/2013] Fixed PickerIOS onChange event --- Libraries/Picker/PickerIOS.ios.js | 8 +++++++- React/Views/RCTPickerManager.m | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Libraries/Picker/PickerIOS.ios.js b/Libraries/Picker/PickerIOS.ios.js index 42302fa26..1a965b32f 100644 --- a/Libraries/Picker/PickerIOS.ios.js +++ b/Libraries/Picker/PickerIOS.ios.js @@ -111,6 +111,12 @@ var styles = StyleSheet.create({ }, }); -var RCTPickerIOS = requireNativeComponent('RCTPicker', null); +var RCTPickerIOS = requireNativeComponent('RCTPicker', PickerIOS, { + nativeOnly: { + items: true, + onChange: true, + selectedIndex: true, + }, +}); module.exports = PickerIOS; diff --git a/React/Views/RCTPickerManager.m b/React/Views/RCTPickerManager.m index 8d1e18120..d43c58ad9 100644 --- a/React/Views/RCTPickerManager.m +++ b/React/Views/RCTPickerManager.m @@ -24,6 +24,7 @@ RCT_EXPORT_MODULE() RCT_EXPORT_VIEW_PROPERTY(items, NSDictionaryArray) RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSInteger) +RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) - (NSDictionary *)constantsToExport { From 20618992240107003285696d0dbd8b85c78d2b8d Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 8 Sep 2015 03:26:40 -0700 Subject: [PATCH 0031/2013] Fixed networker crash due to threading bug --- Libraries/Network/RCTHTTPRequestHandler.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Libraries/Network/RCTHTTPRequestHandler.m b/Libraries/Network/RCTHTTPRequestHandler.m index e1ad88ab1..fe83e3f7c 100644 --- a/Libraries/Network/RCTHTTPRequestHandler.m +++ b/Libraries/Network/RCTHTTPRequestHandler.m @@ -56,6 +56,7 @@ RCT_EXPORT_MODULE() // Lazy setup if (!_session && [self isValid]) { NSOperationQueue *callbackQueue = [NSOperationQueue new]; + callbackQueue.maxConcurrentOperationCount = 1; NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; _session = [NSURLSession sessionWithConfiguration:configuration delegate:self From 817bf1f50ffab60b8af48327a111612525b435f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Tue, 8 Sep 2015 04:56:45 -0700 Subject: [PATCH 0032/2013] [react-packager] Bump ipc timeout --- packager/react-packager/src/SocketInterface/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packager/react-packager/src/SocketInterface/index.js b/packager/react-packager/src/SocketInterface/index.js index f3dce21a0..eab83fa4f 100644 --- a/packager/react-packager/src/SocketInterface/index.js +++ b/packager/react-packager/src/SocketInterface/index.js @@ -20,7 +20,7 @@ const path = require('path'); const tmpdir = require('os').tmpdir(); const {spawn} = require('child_process'); -const CREATE_SERVER_TIMEOUT = 60000; +const CREATE_SERVER_TIMEOUT = 5 * 60 * 1000; const SocketInterface = { getOrCreateSocketFor(options) { From f61ea911b5faf4aeecb6d045318db571015b85ea Mon Sep 17 00:00:00 2001 From: James Ide Date: Tue, 8 Sep 2015 06:44:23 -0700 Subject: [PATCH 0033/2013] [Profiler] Fix makefile target Summary: Simple rename. Closes https://github.com/facebook/react-native/pull/2595 Github Author: James Ide --- JSCLegacyProfiler/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JSCLegacyProfiler/Makefile b/JSCLegacyProfiler/Makefile index 306c6632c..4b36fbfac 100644 --- a/JSCLegacyProfiler/Makefile +++ b/JSCLegacyProfiler/Makefile @@ -26,13 +26,13 @@ else cp $^ endif -/tmp/JSCProfiler: +/tmp/RCTJSCProfiler: mkdir -p $@ .PRECIOUS: RCTJSCProfiler.ios8.dylib RCTJSCProfiler.ios8.dylib: RCTJSCProfiler_unsigned.ios8.dylib cp $< $@ - codesign -f -s ${CERT} $@ || rm $@ + codesign -f -s "${CERT}" $@ || rm $@ .PRECIOUS: RCTJSCProfiler_unsigned.ios8.dylib RCTJSCProfiler_unsigned.ios8.dylib: $(patsubst %,RCTJSCProfiler_%.ios8.dylib,$(ARCHS)) From f9ca103ecbd3d52acc9d104ef80c1a59ef2bcb51 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Tue, 8 Sep 2015 06:44:28 -0700 Subject: [PATCH 0034/2013] [ReactNative][Profiler] Update OSS build script to work with new makefile Summary: There was some recent changes to the Makefile, but the open source build phase script wasn't update. Update it to copy the files to the right location. --- JSCLegacyProfiler/Makefile | 9 ++++----- React/React.xcodeproj/project.pbxproj | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/JSCLegacyProfiler/Makefile b/JSCLegacyProfiler/Makefile index 4b36fbfac..7121d6e27 100644 --- a/JSCLegacyProfiler/Makefile +++ b/JSCLegacyProfiler/Makefile @@ -4,7 +4,7 @@ SDK_PATH = /Applications/Xcode.app/Contents/Developer/Platforms/$1.platform/Deve SDK_VERSION = $(shell plutil -convert json -o - $(call SDK_PATH,iPhoneOS)/SDKSettings.plist | awk -f parseSDKVersion.awk) -CERT ?= "iPhone Developer" +CERT ?= iPhone Developer ARCHS = x86_64 arm64 armv7 i386 @@ -29,10 +29,9 @@ endif /tmp/RCTJSCProfiler: mkdir -p $@ -.PRECIOUS: RCTJSCProfiler.ios8.dylib RCTJSCProfiler.ios8.dylib: RCTJSCProfiler_unsigned.ios8.dylib cp $< $@ - codesign -f -s "${CERT}" $@ || rm $@ + codesign -f -s "${CERT}" $@ .PRECIOUS: RCTJSCProfiler_unsigned.ios8.dylib RCTJSCProfiler_unsigned.ios8.dylib: $(patsubst %,RCTJSCProfiler_%.ios8.dylib,$(ARCHS)) @@ -77,7 +76,7 @@ libyajl_%.a: download/yajl-2.1.0 .PRECIOUS: download/yajl-2.1.0 download/yajl-2.1.0: download/yajl-2.1.0.tar.gz - tar -zxvf $< -C download + tar -zxvf $< -C download > /dev/null mkdir -p download/yajl-2.1.0/build && cd download/yajl-2.1.0/build && cmake .. .PRECIOUS: download/yajl-2.1.0.tar.gz @@ -87,7 +86,7 @@ download/yajl-2.1.0.tar.gz: .PRECIOUS: download/% download/%: download/%.tar.gz - tar -zxvf $< -C `dirname $@` + tar -zxvf $< -C `dirname $@` > /dev/null .PRECIOUS: %.tar.gz %.tar.gz: diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index ce803446d..276e3943d 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -589,7 +589,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ \"$CONFIGURATION\" == \"Debug\" ]] && [[ -d \"/tmp/RCTJSCProfiler\" ]]; then\n find \"${CONFIGURATION_BUILD_DIR}\" -name '*.app' | xargs -I{} sh -c 'mkdir -p \"$1/Frameworks\" && cp -r /tmp/RCTJSCProfiler/* \"$1/Frameworks\"' -- {}\nfi"; + shellScript = "if [[ \"$CONFIGURATION\" == \"Debug\" ]] && [[ -d \"/tmp/RCTJSCProfiler\" ]]; then\n find \"${CONFIGURATION_BUILD_DIR}\" -name '*.app' | xargs -I{} sh -c 'cp -r /tmp/RCTJSCProfiler \"$1\"' -- {}\nfi"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ From 93b9329b758cde3e921b26e11ba91d9700d2a06d Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Tue, 8 Sep 2015 08:13:51 -0700 Subject: [PATCH 0035/2013] [ReactNative] Enable displayName transformer in open source --- packager/transformer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packager/transformer.js b/packager/transformer.js index 8f7a48c29..b3ab96d08 100644 --- a/packager/transformer.js +++ b/packager/transformer.js @@ -39,6 +39,7 @@ function transform(srcTxt, filename, options) { 'es7.objectRestSpread', 'flow', 'react', + 'react.displayName', 'regenerator', ], plugins: plugins, From 56d25bbbdd69b83cf4f92863b086981290c3de61 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 8 Sep 2015 08:58:13 -0700 Subject: [PATCH 0036/2013] Moved CameraRoll-related classes into CameraRoll folder instead of Image Summary: The CameraRoll-related APIs were mixed in with the Image classes due to legacy coupling issues. Now that the APIs have been decoupled, it makes more sense for the CameraRoll classes to live in a separate library. This will be a breaking change for apps using the CameraRoll or related APIs. Fix is to add the RCTCameraRoll lib to your project. --- .../Movies/Movies.xcodeproj/project.pbxproj | 5 +- .../xcshareddata/xcschemes/Movies.xcscheme | 11 +- Examples/Movies/Movies/Info.plist | 13 +- .../UIExplorer.xcodeproj/project.pbxproj | 32 +- .../{Image => CameraRoll}/ImagePickerIOS.js | 0 .../RCTAssetsLibraryImageLoader.h | 0 .../RCTAssetsLibraryImageLoader.m | 0 .../RCTCameraRoll.xcodeproj/project.pbxproj | 296 ++++++++++++++++++ .../RCTCameraRollManager.h | 0 .../RCTCameraRollManager.m | 0 .../RCTImagePickerManager.h | 0 .../RCTImagePickerManager.m | 0 .../RCTPhotoLibraryImageLoader.h | 0 .../RCTPhotoLibraryImageLoader.m | 0 .../Image/RCTImage.xcodeproj/project.pbxproj | 24 -- 15 files changed, 345 insertions(+), 36 deletions(-) rename Libraries/{Image => CameraRoll}/ImagePickerIOS.js (100%) rename Libraries/{Image => CameraRoll}/RCTAssetsLibraryImageLoader.h (100%) rename Libraries/{Image => CameraRoll}/RCTAssetsLibraryImageLoader.m (100%) create mode 100644 Libraries/CameraRoll/RCTCameraRoll.xcodeproj/project.pbxproj rename Libraries/{Image => CameraRoll}/RCTCameraRollManager.h (100%) rename Libraries/{Image => CameraRoll}/RCTCameraRollManager.m (100%) rename Libraries/{Image => CameraRoll}/RCTImagePickerManager.h (100%) rename Libraries/{Image => CameraRoll}/RCTImagePickerManager.m (100%) rename Libraries/{Image => CameraRoll}/RCTPhotoLibraryImageLoader.h (100%) rename Libraries/{Image => CameraRoll}/RCTPhotoLibraryImageLoader.m (100%) diff --git a/Examples/Movies/Movies.xcodeproj/project.pbxproj b/Examples/Movies/Movies.xcodeproj/project.pbxproj index 21fc0cb88..f3937e428 100644 --- a/Examples/Movies/Movies.xcodeproj/project.pbxproj +++ b/Examples/Movies/Movies.xcodeproj/project.pbxproj @@ -216,7 +216,7 @@ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0610; + LastUpgradeCheck = 0700; ORGANIZATIONNAME = Facebook; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Movies" */; @@ -358,6 +358,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = Movies; USER_HEADER_SEARCH_PATHS = "$(SRCROOT)/../../Libraries/**"; }; @@ -376,6 +377,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = Movies; USER_HEADER_SEARCH_PATHS = "$(SRCROOT)/../../Libraries/**"; }; @@ -401,6 +403,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; diff --git a/Examples/Movies/Movies.xcodeproj/xcshareddata/xcschemes/Movies.xcscheme b/Examples/Movies/Movies.xcodeproj/xcshareddata/xcschemes/Movies.xcscheme index 18b45cad0..4cb2fa20a 100644 --- a/Examples/Movies/Movies.xcodeproj/xcshareddata/xcschemes/Movies.xcscheme +++ b/Examples/Movies/Movies.xcodeproj/xcshareddata/xcschemes/Movies.xcscheme @@ -1,6 +1,6 @@ + + - + - + CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.facebook.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -33,6 +33,11 @@ 1 LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities @@ -47,11 +52,5 @@ UIViewControllerBasedStatusBarAppearance - NSAppTransportSecurity - - - NSAllowsArbitraryLoads - - diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 9ec430af5..02e2c9e13 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 134A8A2A1AACED7A00945AAE /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 134A8A251AACED6A00945AAE /* libRCTGeolocation.a */; }; 138D6A171B53CD440074A87E /* RCTCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 138D6A151B53CD440074A87E /* RCTCacheTests.m */; }; 138D6A181B53CD440074A87E /* RCTShadowViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 138D6A161B53CD440074A87E /* RCTShadowViewTests.m */; }; + 138DEE241B9EDFB6007F4EA5 /* libRCTCameraRoll.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 138DEE091B9EDDDB007F4EA5 /* libRCTCameraRoll.a */; }; 1393D0381B68CD1300E1B601 /* RCTModuleMethodTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1393D0371B68CD1300E1B601 /* RCTModuleMethodTests.m */; }; 139FDEDB1B0651FB00C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDED91B0651EA00C62182 /* libRCTWebSocket.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; @@ -96,6 +97,13 @@ remoteGlobalIDString = 134814201AA4EA6300B7C361; remoteInfo = RCTGeolocation; }; + 138DEE081B9EDDDB007F4EA5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5115D1A9E6B3D00147676; + remoteInfo = RCTImage; + }; 139FDED81B0651EA00C62182 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 139FDECA1B0651EA00C62182 /* RCTWebSocket.xcodeproj */; @@ -171,6 +179,7 @@ 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = ../../Libraries/Geolocation/RCTGeolocation.xcodeproj; sourceTree = ""; }; 138D6A151B53CD440074A87E /* RCTCacheTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCacheTests.m; sourceTree = ""; }; 138D6A161B53CD440074A87E /* RCTShadowViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowViewTests.m; sourceTree = ""; }; + 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTCameraRoll.xcodeproj; path = ../../Libraries/CameraRoll/RCTCameraRoll.xcodeproj; sourceTree = ""; }; 1393D0371B68CD1300E1B601 /* RCTModuleMethodTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleMethodTests.m; sourceTree = ""; }; 139FDECA1B0651EA00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = ../../Libraries/WebSocket/RCTWebSocket.xcodeproj; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* UIExplorer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UIExplorer.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -254,6 +263,7 @@ 14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */, 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */, 134454601AAFCABD003F0779 /* libRCTAdSupport.a in Frameworks */, + 138DEE241B9EDFB6007F4EA5 /* libRCTCameraRoll.a in Frameworks */, 134A8A2A1AACED7A00945AAE /* libRCTGeolocation.a in Frameworks */, 13417FE91AA91432003F314A /* libRCTImage.a in Frameworks */, 3578590A1B28D2CF00341EDB /* libRCTLinking.a in Frameworks */, @@ -281,13 +291,14 @@ isa = PBXGroup; children = ( 14AADEFF1AC3DB95002390C9 /* React.xcodeproj */, - 14DC67E71AB71876001358AB /* RCTPushNotification.xcodeproj */, 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */, 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */, + 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */, 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */, 13417FE31AA91428003F314A /* RCTImage.xcodeproj */, 357858F81B28D2C400341EDB /* RCTLinking.xcodeproj */, 134180261AA91779003F314A /* RCTNetwork.xcodeproj */, + 14DC67E71AB71876001358AB /* RCTPushNotification.xcodeproj */, 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */, 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */, 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */, @@ -337,6 +348,14 @@ name = Products; sourceTree = ""; }; + 138DEE031B9EDDDB007F4EA5 /* Products */ = { + isa = PBXGroup; + children = ( + 138DEE091B9EDDDB007F4EA5 /* libRCTCameraRoll.a */, + ); + name = Products; + sourceTree = ""; + }; 139FDECB1B0651EA00C62182 /* Products */ = { isa = PBXGroup; children = ( @@ -624,6 +643,10 @@ ProductGroup = 134454561AAFCAAE003F0779 /* Products */; ProjectRef = 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */; }, + { + ProductGroup = 138DEE031B9EDDDB007F4EA5 /* Products */; + ProjectRef = 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */; + }, { ProductGroup = 134A8A211AACED6A00945AAE /* Products */; ProjectRef = 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */; @@ -714,6 +737,13 @@ remoteRef = 134A8A241AACED6A00945AAE /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 138DEE091B9EDDDB007F4EA5 /* libRCTCameraRoll.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTCameraRoll.a; + remoteRef = 138DEE081B9EDDDB007F4EA5 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 139FDED91B0651EA00C62182 /* libRCTWebSocket.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Libraries/Image/ImagePickerIOS.js b/Libraries/CameraRoll/ImagePickerIOS.js similarity index 100% rename from Libraries/Image/ImagePickerIOS.js rename to Libraries/CameraRoll/ImagePickerIOS.js diff --git a/Libraries/Image/RCTAssetsLibraryImageLoader.h b/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.h similarity index 100% rename from Libraries/Image/RCTAssetsLibraryImageLoader.h rename to Libraries/CameraRoll/RCTAssetsLibraryImageLoader.h diff --git a/Libraries/Image/RCTAssetsLibraryImageLoader.m b/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.m similarity index 100% rename from Libraries/Image/RCTAssetsLibraryImageLoader.m rename to Libraries/CameraRoll/RCTAssetsLibraryImageLoader.m diff --git a/Libraries/CameraRoll/RCTCameraRoll.xcodeproj/project.pbxproj b/Libraries/CameraRoll/RCTCameraRoll.xcodeproj/project.pbxproj new file mode 100644 index 000000000..56cced214 --- /dev/null +++ b/Libraries/CameraRoll/RCTCameraRoll.xcodeproj/project.pbxproj @@ -0,0 +1,296 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137620341B31C53500677FF0 /* RCTImagePickerManager.m */; }; + 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; }; + 8312EAEE1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 8312EAED1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m */; }; + 8312EAF11B85F071001867A2 /* RCTPhotoLibraryImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 8312EAF01B85F071001867A2 /* RCTPhotoLibraryImageLoader.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B5115B1A9E6B3D00147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 137620331B31C53500677FF0 /* RCTImagePickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImagePickerManager.h; sourceTree = ""; }; + 137620341B31C53500677FF0 /* RCTImagePickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImagePickerManager.m; sourceTree = ""; }; + 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTCameraRollManager.h; sourceTree = ""; }; + 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCameraRollManager.m; sourceTree = ""; }; + 58B5115D1A9E6B3D00147676 /* libRCTCameraRoll.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTCameraRoll.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 8312EAEC1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAssetsLibraryImageLoader.h; sourceTree = ""; }; + 8312EAED1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAssetsLibraryImageLoader.m; sourceTree = ""; }; + 8312EAEF1B85F071001867A2 /* RCTPhotoLibraryImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPhotoLibraryImageLoader.h; sourceTree = ""; }; + 8312EAF01B85F071001867A2 /* RCTPhotoLibraryImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPhotoLibraryImageLoader.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B5115A1A9E6B3D00147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 58B511541A9E6B3D00147676 = { + isa = PBXGroup; + children = ( + 8312EAEC1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.h */, + 8312EAED1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m */, + 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */, + 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */, + 137620331B31C53500677FF0 /* RCTImagePickerManager.h */, + 137620341B31C53500677FF0 /* RCTImagePickerManager.m */, + 8312EAEF1B85F071001867A2 /* RCTPhotoLibraryImageLoader.h */, + 8312EAF01B85F071001867A2 /* RCTPhotoLibraryImageLoader.m */, + 58B5115E1A9E6B3D00147676 /* Products */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + }; + 58B5115E1A9E6B3D00147676 /* Products */ = { + isa = PBXGroup; + children = ( + 58B5115D1A9E6B3D00147676 /* libRCTCameraRoll.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B5115C1A9E6B3D00147676 /* RCTCameraRoll */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511711A9E6B3D00147676 /* Build configuration list for PBXNativeTarget "RCTCameraRoll" */; + buildPhases = ( + 58B511591A9E6B3D00147676 /* Sources */, + 58B5115A1A9E6B3D00147676 /* Frameworks */, + 58B5115B1A9E6B3D00147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTCameraRoll; + productName = RCTNetworkImage; + productReference = 58B5115D1A9E6B3D00147676 /* libRCTCameraRoll.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511551A9E6B3D00147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B5115C1A9E6B3D00147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511581A9E6B3D00147676 /* Build configuration list for PBXProject "RCTCameraRoll" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 58B511541A9E6B3D00147676; + productRefGroup = 58B5115E1A9E6B3D00147676 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B5115C1A9E6B3D00147676 /* RCTCameraRoll */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511591A9E6B3D00147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8312EAEE1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m in Sources */, + 8312EAF11B85F071001867A2 /* RCTPhotoLibraryImageLoader.m in Sources */, + 137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */, + 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B5116F1A9E6B3D00147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_SHADOW = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + WARNING_CFLAGS = ( + "-Werror", + "-Wall", + ); + }; + name = Debug; + }; + 58B511701A9E6B3D00147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_SHADOW = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + WARNING_CFLAGS = ( + "-Werror", + "-Wall", + ); + }; + name = Release; + }; + 58B511721A9E6B3D00147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + "$(SRCROOT)/../Image/**", + "$(SRCROOT)/../Network/**", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(USER_LIBRARY_DIR)/Developer/Xcode/DerivedData/UIExplorer-gjaibsjtheitasdxdtcvxxqavkvy/Build/Products/Debug-iphoneos", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTCameraRoll; + RUN_CLANG_STATIC_ANALYZER = YES; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 58B511731A9E6B3D00147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + "$(SRCROOT)/../Image/**", + "$(SRCROOT)/../Network/**", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(USER_LIBRARY_DIR)/Developer/Xcode/DerivedData/UIExplorer-gjaibsjtheitasdxdtcvxxqavkvy/Build/Products/Debug-iphoneos", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTCameraRoll; + RUN_CLANG_STATIC_ANALYZER = NO; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511581A9E6B3D00147676 /* Build configuration list for PBXProject "RCTCameraRoll" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B5116F1A9E6B3D00147676 /* Debug */, + 58B511701A9E6B3D00147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511711A9E6B3D00147676 /* Build configuration list for PBXNativeTarget "RCTCameraRoll" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511721A9E6B3D00147676 /* Debug */, + 58B511731A9E6B3D00147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511551A9E6B3D00147676 /* Project object */; +} diff --git a/Libraries/Image/RCTCameraRollManager.h b/Libraries/CameraRoll/RCTCameraRollManager.h similarity index 100% rename from Libraries/Image/RCTCameraRollManager.h rename to Libraries/CameraRoll/RCTCameraRollManager.h diff --git a/Libraries/Image/RCTCameraRollManager.m b/Libraries/CameraRoll/RCTCameraRollManager.m similarity index 100% rename from Libraries/Image/RCTCameraRollManager.m rename to Libraries/CameraRoll/RCTCameraRollManager.m diff --git a/Libraries/Image/RCTImagePickerManager.h b/Libraries/CameraRoll/RCTImagePickerManager.h similarity index 100% rename from Libraries/Image/RCTImagePickerManager.h rename to Libraries/CameraRoll/RCTImagePickerManager.h diff --git a/Libraries/Image/RCTImagePickerManager.m b/Libraries/CameraRoll/RCTImagePickerManager.m similarity index 100% rename from Libraries/Image/RCTImagePickerManager.m rename to Libraries/CameraRoll/RCTImagePickerManager.m diff --git a/Libraries/Image/RCTPhotoLibraryImageLoader.h b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.h similarity index 100% rename from Libraries/Image/RCTPhotoLibraryImageLoader.h rename to Libraries/CameraRoll/RCTPhotoLibraryImageLoader.h diff --git a/Libraries/Image/RCTPhotoLibraryImageLoader.m b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m similarity index 100% rename from Libraries/Image/RCTPhotoLibraryImageLoader.m rename to Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index 06029c731..183cb850e 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -11,14 +11,10 @@ 1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */; }; 1304D5B21AA8C50D0002E2BE /* RCTGIFImageDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */; }; 134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */; }; - 137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137620341B31C53500677FF0 /* RCTImagePickerManager.m */; }; - 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; }; 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; }; 35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */; }; 354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 354631671B69857700AA0B86 /* RCTImageEditingManager.m */; }; 58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */; }; - 8312EAEE1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 8312EAED1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m */; }; - 8312EAF11B85F071001867A2 /* RCTPhotoLibraryImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 8312EAF01B85F071001867A2 /* RCTPhotoLibraryImageLoader.m */; }; 83DDA1571B8DCA5800892A1C /* RCTAssetBundleImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 83DDA1561B8DCA5800892A1C /* RCTAssetBundleImageLoader.m */; }; /* End PBXBuildFile section */ @@ -43,10 +39,6 @@ 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImageDecoder.m; sourceTree = ""; }; 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageUtils.h; sourceTree = ""; }; 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageUtils.m; sourceTree = ""; }; - 137620331B31C53500677FF0 /* RCTImagePickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImagePickerManager.h; sourceTree = ""; }; - 137620341B31C53500677FF0 /* RCTImagePickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImagePickerManager.m; sourceTree = ""; }; - 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTCameraRollManager.h; sourceTree = ""; }; - 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCameraRollManager.m; sourceTree = ""; }; 143879361AAD32A300F088A5 /* RCTImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageLoader.h; sourceTree = ""; }; 143879371AAD32A300F088A5 /* RCTImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoader.m; sourceTree = ""; }; 35123E691B59C99D00EBAD80 /* RCTImageStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageStoreManager.h; sourceTree = ""; }; @@ -56,10 +48,6 @@ 58B5115D1A9E6B3D00147676 /* libRCTImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTImage.a; sourceTree = BUILT_PRODUCTS_DIR; }; 58B511891A9E6BD600147676 /* RCTImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageDownloader.h; sourceTree = ""; }; 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageDownloader.m; sourceTree = ""; }; - 8312EAEC1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAssetsLibraryImageLoader.h; sourceTree = ""; }; - 8312EAED1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAssetsLibraryImageLoader.m; sourceTree = ""; }; - 8312EAEF1B85F071001867A2 /* RCTPhotoLibraryImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPhotoLibraryImageLoader.h; sourceTree = ""; }; - 8312EAF01B85F071001867A2 /* RCTPhotoLibraryImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPhotoLibraryImageLoader.m; sourceTree = ""; }; 83DDA1551B8DCA5800892A1C /* RCTAssetBundleImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAssetBundleImageLoader.h; sourceTree = ""; }; 83DDA1561B8DCA5800892A1C /* RCTAssetBundleImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAssetBundleImageLoader.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -80,10 +68,6 @@ children = ( 83DDA1551B8DCA5800892A1C /* RCTAssetBundleImageLoader.h */, 83DDA1561B8DCA5800892A1C /* RCTAssetBundleImageLoader.m */, - 8312EAEC1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.h */, - 8312EAED1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m */, - 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */, - 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */, 1304D5B01AA8C50D0002E2BE /* RCTGIFImageDecoder.h */, 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */, 58B511891A9E6BD600147676 /* RCTImageDownloader.h */, @@ -92,8 +76,6 @@ 354631671B69857700AA0B86 /* RCTImageEditingManager.m */, 143879361AAD32A300F088A5 /* RCTImageLoader.h */, 143879371AAD32A300F088A5 /* RCTImageLoader.m */, - 137620331B31C53500677FF0 /* RCTImagePickerManager.h */, - 137620341B31C53500677FF0 /* RCTImagePickerManager.m */, 1304D5A71AA8C4A30002E2BE /* RCTImageView.h */, 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */, 1304D5A91AA8C4A30002E2BE /* RCTImageViewManager.h */, @@ -102,8 +84,6 @@ 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */, 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */, 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */, - 8312EAEF1B85F071001867A2 /* RCTPhotoLibraryImageLoader.h */, - 8312EAF01B85F071001867A2 /* RCTPhotoLibraryImageLoader.m */, 58B5115E1A9E6B3D00147676 /* Products */, ); indentWidth = 2; @@ -174,14 +154,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8312EAEE1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m in Sources */, 35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */, - 8312EAF11B85F071001867A2 /* RCTPhotoLibraryImageLoader.m in Sources */, 58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */, - 137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */, 1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */, 1304D5B21AA8C50D0002E2BE /* RCTGIFImageDecoder.m in Sources */, - 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */, 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */, 354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */, 1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */, From 551815c1cff2695a585ea7c0a3b0387fc3a4fde2 Mon Sep 17 00:00:00 2001 From: Nacho Lopez Sais Date: Tue, 8 Sep 2015 10:31:37 -0700 Subject: [PATCH 0037/2013] Adapted changes from android repo for TextInput numberOfLines --- Libraries/Components/TextInput/TextInput.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index f114ac18f..d7901aed0 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -49,6 +49,7 @@ var AndroidTextInputAttributes = { keyboardType: true, mostRecentEventCount: true, multiline: true, + numberOfLines: true, password: true, placeholder: true, placeholderTextColor: true, @@ -193,6 +194,12 @@ var TextInput = React.createClass({ * @platform ios */ maxLength: PropTypes.number, + /** + * Sets the number of lines for a TextInput. Use it with multiline set to + * true to be able to fill the lines. + * @platform android + */ + numberOfLines: PropTypes.number, /** * If true, the keyboard disables the return key when there is no text and * automatically enables it when there is text. The default value is false. @@ -484,6 +491,7 @@ var TextInput = React.createClass({ keyboardType={this.props.keyboardType} mostRecentEventCount={this.state.mostRecentEventCount} multiline={this.props.multiline} + numberOfLines={this.props.numberOfLines} onFocus={this._onFocus} onBlur={this._onBlur} onChange={this._onChange} From c971aae67613f2c061a94e17e5c8c256d4b7712b Mon Sep 17 00:00:00 2001 From: Chace Liang Date: Tue, 8 Sep 2015 10:09:29 -0700 Subject: [PATCH 0038/2013] [RN][Accessibility] typo in onAccessibilityTap --- Libraries/Components/View/View.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index bd5680805..7cb8d35f6 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -159,7 +159,7 @@ var View = React.createClass({ * When `accessible` is true, the system will try to invoke this function * when the user performs accessibility tap gesture. */ - onAcccessibilityTap: PropTypes.func, + onAccessibilityTap: PropTypes.func, /** * When `accessible` is true, the system will invoke this function when the From c0488c71d3d04a9674b65c19ebabbe79605fceec Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Tue, 8 Sep 2015 11:41:54 -0700 Subject: [PATCH 0039/2013] [npm] Fix connect/ Summary: We had an old version of connect internally and a new version on github. Unfortunately, internally we picked up the od one and externally we picked the new one. This diff removes the internal version and downgrades the external version. It also updates package.json to make sure we have the same versions that are installed, somehow they mismatch!? --- npm-shrinkwrap.json | 104 ++++++++++++++++++++++++++++---------------- package.json | 10 ++--- 2 files changed, 71 insertions(+), 43 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 2277ceccf..aa8bd79a8 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1214,48 +1214,76 @@ } }, "connect": { - "version": "3.4.0", - "from": "connect@3.4.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.4.0.tgz", + "version": "2.8.3", + "from": "connect@2.8.3", + "resolved": "https://registry.npmjs.org/connect/-/connect-2.8.3.tgz", "dependencies": { - "finalhandler": { - "version": "0.4.0", - "from": "finalhandler@0.4.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz", + "qs": { + "version": "0.6.5", + "from": "qs@0.6.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-0.6.5.tgz" + }, + "formidable": { + "version": "1.0.14", + "from": "formidable@1.0.14", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.14.tgz" + }, + "cookie-signature": { + "version": "1.0.1", + "from": "cookie-signature@1.0.1", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.1.tgz" + }, + "buffer-crc32": { + "version": "0.2.1", + "from": "buffer-crc32@0.2.1", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.1.tgz" + }, + "cookie": { + "version": "0.1.0", + "from": "cookie@0.1.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz" + }, + "send": { + "version": "0.1.2", + "from": "send@0.1.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.1.2.tgz", "dependencies": { - "escape-html": { - "version": "1.0.2", - "from": "escape-html@1.0.2", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz" + "mime": { + "version": "1.2.11", + "from": "mime@>=1.2.9 <1.3.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz" }, - "on-finished": { - "version": "2.3.0", - "from": "on-finished@>=2.3.0 <2.4.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "dependencies": { - "ee-first": { - "version": "1.1.1", - "from": "ee-first@1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - } - } - }, - "unpipe": { - "version": "1.0.0", - "from": "unpipe@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + "range-parser": { + "version": "0.0.4", + "from": "range-parser@0.0.4", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-0.0.4.tgz" } } }, - "parseurl": { - "version": "1.3.0", - "from": "parseurl@>=1.3.0 <1.4.0", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz" + "bytes": { + "version": "0.2.0", + "from": "bytes@0.2.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-0.2.0.tgz" }, - "utils-merge": { - "version": "1.0.0", - "from": "utils-merge@1.0.0", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz" + "fresh": { + "version": "0.1.0", + "from": "fresh@0.1.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.1.0.tgz" + }, + "pause": { + "version": "0.0.1", + "from": "pause@0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz" + }, + "uid2": { + "version": "0.0.2", + "from": "uid2@0.0.2", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.2.tgz" + }, + "methods": { + "version": "0.0.1", + "from": "methods@0.0.1", + "resolved": "https://registry.npmjs.org/methods/-/methods-0.0.1.tgz" } } }, @@ -3734,7 +3762,7 @@ }, "sane": { "version": "1.2.0", - "from": "sane@>=1.1.2 <2.0.0", + "from": "sane@>=1.2.0 <1.3.0", "resolved": "https://registry.npmjs.org/sane/-/sane-1.2.0.tgz", "dependencies": { "exec-sh": { @@ -4961,7 +4989,7 @@ }, "timed-out": { "version": "2.0.0", - "from": "timed-out@>=2.0.0 <3.0.0", + "from": "timed-out@2.0.0", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz" } } @@ -6783,7 +6811,7 @@ }, "timed-out": { "version": "2.0.0", - "from": "timed-out@>=2.0.0 <3.0.0", + "from": "timed-out@2.0.0", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz" } } diff --git a/package.json b/package.json index d4eff15d4..bb21b3e9f 100644 --- a/package.json +++ b/package.json @@ -51,18 +51,18 @@ "babel-core": "5.8.23", "bser": "1.0.2", "chalk": "1.1.1", - "connect": "3.4.0", + "connect": "2.8.3", "debug": "2.2.0", "graceful-fs": "4.1.2", "image-size": "0.3.5", - "immutable": "3.7.4", + "immutable": "3.7.5", "joi": "6.6.1", "jstransform": "11.0.3", "module-deps": "3.9.1", "optimist": "0.6.1", "progress": "1.1.8", - "promise": "7.0.3", - "react-timer-mixin": "0.13.1", + "promise": "7.0.4", + "react-timer-mixin": "0.13.2", "react-tools": "git://github.com/facebook/react#b4e74e38e43ac53af8acd62c78c9213be0194245", "rebound": "0.0.13", "regenerator": "0.8.36", @@ -77,7 +77,7 @@ "ws": "0.8.0", "yargs": "3.24.0", "yeoman-environment": "1.2.7", - "yeoman-generator": "0.20.2" + "yeoman-generator": "0.20.3" }, "devDependencies": { "jest-cli": "0.5.1", From fb7d7d68806179d26cabfbab06de71d26c2a28f0 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Tue, 8 Sep 2015 11:45:03 -0700 Subject: [PATCH 0040/2013] [ReactNative] Pipe `platform` option all the way to the asset server --- packager/react-packager/src/AssetServer/index.js | 4 ++-- packager/react-packager/src/Bundler/index.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packager/react-packager/src/AssetServer/index.js b/packager/react-packager/src/AssetServer/index.js index 5e4c11273..5783e9912 100644 --- a/packager/react-packager/src/AssetServer/index.js +++ b/packager/react-packager/src/AssetServer/index.js @@ -51,14 +51,14 @@ class AssetServer { }); } - getAssetData(assetPath) { + getAssetData(assetPath, platform = null) { const nameData = getAssetDataFromName(assetPath); const data = { name: nameData.name, type: nameData.type, }; - return this._getAssetRecord(assetPath).then(record => { + return this._getAssetRecord(assetPath, platform).then(record => { data.scales = record.scales; return Promise.all( diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js index 8f76ce103..5f2ead1cf 100644 --- a/packager/react-packager/src/Bundler/index.js +++ b/packager/react-packager/src/Bundler/index.js @@ -154,7 +154,7 @@ class Bundler { bundle.setMainModuleId(result.mainModuleId); return Promise.all( result.dependencies.map( - module => this._transformModule(bundle, module).then(transformed => { + module => this._transformModule(bundle, module, platform).then(transformed => { if (bar) { bar.tick(); } @@ -182,13 +182,13 @@ class Bundler { return this._resolver.getDependencies(main, { dev: isDev, platform }); } - _transformModule(bundle, module) { + _transformModule(bundle, module, platform = null) { let transform; if (module.isAsset_DEPRECATED()) { transform = this.generateAssetModule_DEPRECATED(bundle, module); } else if (module.isAsset()) { - transform = this.generateAssetModule(bundle, module); + transform = this.generateAssetModule(bundle, module, platform); } else if (module.isJSON()) { transform = generateJSONModule(module); } else { @@ -243,12 +243,12 @@ class Bundler { }); } - generateAssetModule(bundle, module) { + generateAssetModule(bundle, module, platform = null) { const relPath = getPathRelativeToRoot(this._projectRoots, module.path); return Promise.all([ sizeOf(module.path), - this._assetServer.getAssetData(relPath), + this._assetServer.getAssetData(relPath, platform), ]).then(function(res) { const dimensions = res[0]; const assetData = res[1]; From 4f89c61bd83f63bba732a7912a8ca8cc4a4e8241 Mon Sep 17 00:00:00 2001 From: Nick Simmons Date: Sun, 6 Sep 2015 17:06:33 -0400 Subject: [PATCH 0041/2013] [CLI] Add version argument --- react-native-cli/index.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/react-native-cli/index.js b/react-native-cli/index.js index d80c7b766..73de9418f 100755 --- a/react-native-cli/index.js +++ b/react-native-cli/index.js @@ -4,10 +4,12 @@ * Copyright 2004-present Facebook. All Rights Reserved. */ +'use strict'; + var fs = require('fs'); var path = require('path'); var exec = require('child_process').exec; -var prompt = require("prompt"); +var prompt = require('prompt'); var CLI_MODULE_PATH = function() { return path.resolve( @@ -18,6 +20,8 @@ var CLI_MODULE_PATH = function() { ); }; +checkForVersionArgument(); + var cli; try { cli = require(CLI_MODULE_PATH()); @@ -80,7 +84,7 @@ function init(name) { validatePackageName(name); if (fs.existsSync(name)) { - createAfterConfirmation(name) + createAfterConfirmation(name); } else { createProject(name); } @@ -140,7 +144,15 @@ function createProject(name) { process.exit(1); } - var cli = require(CLI_MODULE_PATH()); + cli = require(CLI_MODULE_PATH()); cli.init(root, projectName); }); } + +function checkForVersionArgument() { + if (process.argv.indexOf('-v') >= 0 || process.argv.indexOf('--version') >= 0) { + var pjson = require('./package.json'); + console.log(pjson.version); + process.exit(); + } +} From f9b2709c8d39b1ec6f729d315855f05d427ab75b Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Tue, 8 Sep 2015 20:00:42 -0700 Subject: [PATCH 0042/2013] unbreak tests --- Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj | 4 ---- .../react-packager/src/lib/__tests__/declareOpts-test.js | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 02e2c9e13..d783facad 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -24,7 +24,6 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */; }; 13DF61B61B67A45000EDB188 /* RCTMethodArgumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */; }; - 141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 141FC1201B222EBB004D5FFB /* IntegrationTests.m */; }; 143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */; }; 144D21241B2204C5006DB32B /* RCTImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTImageUtilTests.m */; }; 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; }; @@ -192,7 +191,6 @@ 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSONTests.m; sourceTree = ""; }; 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMethodArgumentTests.m; sourceTree = ""; }; - 141FC1201B222EBB004D5FFB /* IntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IntegrationTests.m; sourceTree = ""; }; 143BC57E1B21E18100462512 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 143BC5811B21E18100462512 /* testLayoutExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testLayoutExampleSnapshot_1@2x.png"; sourceTree = ""; }; 143BC5821B21E18100462512 /* testSliderExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testSliderExampleSnapshot_1@2x.png"; sourceTree = ""; }; @@ -431,7 +429,6 @@ 143BC5961B21E3E100462512 /* UIExplorerIntegrationTests */ = { isa = PBXGroup; children = ( - 141FC1201B222EBB004D5FFB /* IntegrationTests.m */, 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */, 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */, 143BC5971B21E3E100462512 /* Supporting Files */, @@ -867,7 +864,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */, 83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */, 143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */, ); diff --git a/packager/react-packager/src/lib/__tests__/declareOpts-test.js b/packager/react-packager/src/lib/__tests__/declareOpts-test.js index cb9a35dd7..0fa1b7c81 100644 --- a/packager/react-packager/src/lib/__tests__/declareOpts-test.js +++ b/packager/react-packager/src/lib/__tests__/declareOpts-test.js @@ -61,7 +61,7 @@ describe('declareOpts', function() { expect(function() { validate({}); - }).toThrow('Error validating module options: foo is required'); + }).toThrow(); }); it('should throw on invalid type', function() { @@ -74,7 +74,7 @@ describe('declareOpts', function() { expect(function() { validate({foo: 'lol'}); - }).toThrow('Error validating module options: foo must be a number'); + }).toThrow(); }); it('should throw on extra options', function() { @@ -87,6 +87,6 @@ describe('declareOpts', function() { expect(function() { validate({foo: 1, lol: 1}); - }).toThrow('Error validating module options: lol is not allowed'); + }).toThrow(); }); }); From 635b15b45403c7f04077bca03e43a941a384b787 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Wed, 9 Sep 2015 09:59:56 -0700 Subject: [PATCH 0043/2013] Testing new export script Differential Revision: D2420548 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 57d3f90dd..751199630 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ The GitHub issues are intended for bug reports and feature requests. For help an For more information about contributing, see our [Contribution Guidelines](https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md). + ## License React is [BSD licensed](./LICENSE). We also provide an additional [patent grant](./PATENTS). From c71c94080354df08f7ad14f2d9b47210a75bb8d5 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Wed, 9 Sep 2015 11:10:50 -0700 Subject: [PATCH 0044/2013] Revert "unbreak tests" This reverts commit f9b2709c8d39b1ec6f729d315855f05d427ab75b. --- Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj | 4 ++++ .../react-packager/src/lib/__tests__/declareOpts-test.js | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index d783facad..02e2c9e13 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */; }; 13DF61B61B67A45000EDB188 /* RCTMethodArgumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */; }; + 141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 141FC1201B222EBB004D5FFB /* IntegrationTests.m */; }; 143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */; }; 144D21241B2204C5006DB32B /* RCTImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTImageUtilTests.m */; }; 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; }; @@ -191,6 +192,7 @@ 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSONTests.m; sourceTree = ""; }; 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMethodArgumentTests.m; sourceTree = ""; }; + 141FC1201B222EBB004D5FFB /* IntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IntegrationTests.m; sourceTree = ""; }; 143BC57E1B21E18100462512 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 143BC5811B21E18100462512 /* testLayoutExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testLayoutExampleSnapshot_1@2x.png"; sourceTree = ""; }; 143BC5821B21E18100462512 /* testSliderExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testSliderExampleSnapshot_1@2x.png"; sourceTree = ""; }; @@ -429,6 +431,7 @@ 143BC5961B21E3E100462512 /* UIExplorerIntegrationTests */ = { isa = PBXGroup; children = ( + 141FC1201B222EBB004D5FFB /* IntegrationTests.m */, 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */, 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */, 143BC5971B21E3E100462512 /* Supporting Files */, @@ -864,6 +867,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */, 83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */, 143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */, ); diff --git a/packager/react-packager/src/lib/__tests__/declareOpts-test.js b/packager/react-packager/src/lib/__tests__/declareOpts-test.js index 0fa1b7c81..cb9a35dd7 100644 --- a/packager/react-packager/src/lib/__tests__/declareOpts-test.js +++ b/packager/react-packager/src/lib/__tests__/declareOpts-test.js @@ -61,7 +61,7 @@ describe('declareOpts', function() { expect(function() { validate({}); - }).toThrow(); + }).toThrow('Error validating module options: foo is required'); }); it('should throw on invalid type', function() { @@ -74,7 +74,7 @@ describe('declareOpts', function() { expect(function() { validate({foo: 'lol'}); - }).toThrow(); + }).toThrow('Error validating module options: foo must be a number'); }); it('should throw on extra options', function() { @@ -87,6 +87,6 @@ describe('declareOpts', function() { expect(function() { validate({foo: 1, lol: 1}); - }).toThrow(); + }).toThrow('Error validating module options: lol is not allowed'); }); }); From d82af3cb921650688e175c59c8ce4ce7b9cfaef5 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Wed, 9 Sep 2015 01:37:01 -0700 Subject: [PATCH 0045/2013] Fix "global-strict" lint Reviewed By: @vjeux Differential Revision: D2420561 --- .eslintrc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.eslintrc b/.eslintrc index e1ec93029..5a2f08c52 100644 --- a/.eslintrc +++ b/.eslintrc @@ -126,9 +126,7 @@ // Strict Mode // These rules relate to using strict mode. - "global-strict": [2, "always"], // require or disallow the "use strict" pragma in the global scope (off by default in the node environment) - "no-extra-strict": 1, // disallow unnecessary use of "use strict"; when already in strict mode - "strict": 0, // require that all functions are run in strict mode + "strict": [2, "global"], // require or disallow the "use strict" pragma in the global scope (off by default in the node environment) // Variables // These rules have to do with variable declarations. From 4a4f087a9c17db8a9a329b87d30f982a6b2a3d21 Mon Sep 17 00:00:00 2001 From: Dorota Kapturkiewicz Date: Wed, 9 Sep 2015 06:41:00 -0700 Subject: [PATCH 0046/2013] move Toast to oss Summary: Importing JS changes to fbobjc which should be maintained as a source of truth for React Native JS files. For more details about this change, please refer to the original diff in fbandroid repo: D2410797 Reviewed By: @andreicoman11 Differential Revision: D2424679 --- .../Components/ToastAndroid/ToastAndroid.ios.js | 13 +++++++++++++ Libraries/react-native/react-native.js | 1 + 2 files changed, 14 insertions(+) create mode 100644 Libraries/Components/ToastAndroid/ToastAndroid.ios.js diff --git a/Libraries/Components/ToastAndroid/ToastAndroid.ios.js b/Libraries/Components/ToastAndroid/ToastAndroid.ios.js new file mode 100644 index 000000000..249767a91 --- /dev/null +++ b/Libraries/Components/ToastAndroid/ToastAndroid.ios.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ToastAndroid + */ +'use strict'; + +module.exports = require('UnimplementedView'); diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index f0b492980..7b2286782 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -39,6 +39,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { TabBarIOS: require('TabBarIOS'), Text: require('Text'), TextInput: require('TextInput'), + ToastAndroid: require('ToastAndroid'), ToolbarAndroid: require('ToolbarAndroid'), TouchableHighlight: require('TouchableHighlight'), TouchableNativeFeedback: require('TouchableNativeFeedback'), From c4305fe9af70755932b8634839a70ed2d7f0f6d9 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Wed, 9 Sep 2015 08:50:15 -0700 Subject: [PATCH 0047/2013] Move systrace helper out of the packager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: @​public The profiler helper shouldn't live inside the packager itself, move it to the packager.js file with other middlewares. Reviewed By: @martinbigio Differential Revision: D2424878 --- packager/packager.js | 46 ++++++++++++++++++++- packager/react-packager/src/Server/index.js | 41 ------------------ 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/packager/packager.js b/packager/packager.js index b5f789699..2a95dfc91 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -10,7 +10,7 @@ var fs = require('fs'); var path = require('path'); -var execFile = require('child_process').execFile; +var childProcess = require('child_process'); var http = require('http'); var isAbsolutePath = require('absolute-path'); @@ -198,7 +198,7 @@ function getDevToolsLauncher(options) { var debuggerURL = 'http://localhost:' + options.port + '/debugger-ui'; var script = 'launchChromeDevTools.applescript'; console.log('Launching Dev Tools...'); - execFile(path.join(__dirname, script), [debuggerURL], function(err, stdout, stderr) { + childProcess.execFile(path.join(__dirname, script), [debuggerURL], function(err, stdout, stderr) { if (err) { console.log('Failed to run ' + script, err); } @@ -223,6 +223,47 @@ function statusPageMiddleware(req, res, next) { } } +function systraceProfileMiddleware(req, res, next) { + if (req.url !== '/profile') { + next(); + return; + } + + console.log('Dumping profile information...'); + const dumpName = '/tmp/dump_' + Date.now() + '.json'; + const prefix = process.env.TRACE_VIEWER_PATH || ''; + const cmd = path.join(prefix, 'trace2html') + ' ' + dumpName; + fs.writeFileSync(dumpName, req.rawBody); + childProcess.exec(cmd, function(error) { + if (error) { + if (error.code === 127) { + console.error( + '\n** Failed executing `' + cmd + '` **\n\n' + + 'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' + + 'You can get it at:\n\n' + + ' https://github.com/google/trace-viewer\n\n' + + 'If it\'s not in your path, you can set a custom path with:\n\n' + + ' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' + + 'NOTE: Your profile data was kept at:\n\n' + + ' ' + dumpName + ); + } else { + console.error('Unknown error', error); + } + res.end(); + return; + } else { + childProcess.exec('rm ' + dumpName); + childProcess.exec('open ' + dumpName.replace(/json$/, 'html'), function(err) { + if (err) { + console.error(err); + } + res.end(); + }); + } + }); +} + function getAppMiddleware(options) { var transformerPath = options.transformer; if (!isAbsolutePath(transformerPath)) { @@ -255,6 +296,7 @@ function runServer( .use(openStackFrameInEditor) .use(getDevToolsLauncher(options)) .use(statusPageMiddleware) + .use(systraceProfileMiddleware) // Temporarily disable flow check until it's more stable //.use(getFlowTypeCheckMiddleware(options)) .use(getAppMiddleware(options)); diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index e889a357e..da8ba0054 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -16,8 +16,6 @@ const Promise = require('promise'); const _ = require('underscore'); const declareOpts = require('../lib/declareOpts'); -const exec = require('child_process').exec; -const fs = require('fs'); const path = require('path'); const url = require('url'); @@ -290,42 +288,6 @@ class Server { ).done(() => Activity.endEvent(assetEvent)); } - _processProfile(req, res) { - console.log('Dumping profile information...'); - const dumpName = '/tmp/dump_' + Date.now() + '.json'; - const prefix = process.env.TRACE_VIEWER_PATH || ''; - const cmd = path.join(prefix, 'trace2html') + ' ' + dumpName; - fs.writeFileSync(dumpName, req.rawBody); - exec(cmd, error => { - if (error) { - if (error.code === 127) { - console.error( - '\n** Failed executing `' + cmd + '` **\n\n' + - 'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' + - 'You can get it at:\n\n' + - ' https://github.com/google/trace-viewer\n\n' + - 'If it\'s not in your path, you can set a custom path with:\n\n' + - ' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' + - 'NOTE: Your profile data was kept at:\n\n' + - ' ' + dumpName - ); - } else { - console.error('Unknown error', error); - } - res.end(); - return; - } else { - exec('rm ' + dumpName); - exec('open ' + dumpName.replace(/json$/, 'html'), err => { - if (err) { - console.error(err); - } - res.end(); - }); - } - }); - } - processRequest(req, res, next) { const urlObj = url.parse(req.url, true); var pathname = urlObj.pathname; @@ -346,9 +308,6 @@ class Server { } else if (pathname.match(/^\/assets\//)) { this._processAssetsRequest(req, res); return; - } else if (pathname.match(/^\/profile\/?$/)) { - this._processProfile(req, res); - return; } else { next(); return; From ce47e56b7b56b81c549e348b8e71b7ad335aaf74 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Wed, 9 Sep 2015 08:49:45 -0700 Subject: [PATCH 0048/2013] Attach platform to asset url Summary: Depends on D2420548 Reviewed By: @martinbigio Differential Revision: D2421696 --- .../__tests__/resolveAssetSource-test.js | 26 +++---------------- Libraries/Image/resolveAssetSource.js | 3 ++- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/Libraries/Image/__tests__/resolveAssetSource-test.js b/Libraries/Image/__tests__/resolveAssetSource-test.js index 5854dae0b..cd1cbfc05 100644 --- a/Libraries/Image/__tests__/resolveAssetSource-test.js +++ b/Libraries/Image/__tests__/resolveAssetSource-test.js @@ -25,6 +25,7 @@ function expectResolvesAsset(input, expectedSource) { describe('resolveAssetSource', () => { beforeEach(() => { jest.resetModuleRegistry(); + __DEV__ = true; AssetRegistry = require('AssetRegistry'); Platform = require('Platform'); SourceCode = require('NativeModules').SourceCode; @@ -64,6 +65,7 @@ describe('resolveAssetSource', () => { describe('bundle was loaded from network (DEV)', () => { beforeEach(() => { SourceCode.scriptURL = 'http://10.0.0.1:8081/main.bundle'; + Platform.OS = 'ios'; }); it('uses network image', () => { @@ -81,7 +83,7 @@ describe('resolveAssetSource', () => { isStatic: false, width: 100, height: 200, - uri: 'http://10.0.0.1:8081/assets/module/a/logo.png?hash=5b6f00f', + uri: 'http://10.0.0.1:8081/assets/module/a/logo.png?platform=ios&hash=5b6f00f', }); }); @@ -100,29 +102,19 @@ describe('resolveAssetSource', () => { isStatic: false, width: 100, height: 200, - uri: 'http://10.0.0.1:8081/assets/module/a/logo@2x.png?hash=5b6f00f', + uri: 'http://10.0.0.1:8081/assets/module/a/logo@2x.png?platform=ios&hash=5b6f00f', }); }); }); describe('bundle was loaded from file (PROD) on iOS', () => { - var originalDevMode; - var originalPlatform; - beforeEach(() => { SourceCode.scriptURL = 'file:///Path/To/Simulator/main.bundle'; - originalDevMode = __DEV__; - originalPlatform = Platform.OS; __DEV__ = false; Platform.OS = 'ios'; }); - afterEach(() => { - __DEV__ = originalDevMode; - Platform.OS = originalPlatform; - }); - it('uses pre-packed image', () => { expectResolvesAsset({ __packager_asset: true, @@ -144,22 +136,12 @@ describe('resolveAssetSource', () => { }); describe('bundle was loaded from file (PROD) on Android', () => { - var originalDevMode; - var originalPlatform; - beforeEach(() => { SourceCode.scriptURL = 'file:///Path/To/Simulator/main.bundle'; - originalDevMode = __DEV__; - originalPlatform = Platform.OS; __DEV__ = false; Platform.OS = 'android'; }); - afterEach(() => { - __DEV__ = originalDevMode; - Platform.OS = originalPlatform; - }); - it('uses pre-packed image', () => { expectResolvesAsset({ __packager_asset: true, diff --git a/Libraries/Image/resolveAssetSource.js b/Libraries/Image/resolveAssetSource.js index 301d70dd9..00fb8c84d 100644 --- a/Libraries/Image/resolveAssetSource.js +++ b/Libraries/Image/resolveAssetSource.js @@ -63,7 +63,8 @@ function getPathInArchive(asset) { * from the devserver */ function getPathOnDevserver(devServerUrl, asset) { - return devServerUrl + getScaledAssetPath(asset) + '?hash=' + asset.hash; + return devServerUrl + getScaledAssetPath(asset) + '?platform=' + Platform.OS + + '&hash=' + asset.hash; } /** From d3800c6615551c673b604f99bdf0ad3ca26e9bb1 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Wed, 9 Sep 2015 09:43:44 -0700 Subject: [PATCH 0049/2013] Fix declareOpts test Summary: When we updated joi, the error message was changed. I removed the content to prevent similar errors in the future. Reviewed By: @amasad Differential Revision: D2424048 --- .../react-packager/src/lib/__tests__/declareOpts-test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packager/react-packager/src/lib/__tests__/declareOpts-test.js b/packager/react-packager/src/lib/__tests__/declareOpts-test.js index cb9a35dd7..0fa1b7c81 100644 --- a/packager/react-packager/src/lib/__tests__/declareOpts-test.js +++ b/packager/react-packager/src/lib/__tests__/declareOpts-test.js @@ -61,7 +61,7 @@ describe('declareOpts', function() { expect(function() { validate({}); - }).toThrow('Error validating module options: foo is required'); + }).toThrow(); }); it('should throw on invalid type', function() { @@ -74,7 +74,7 @@ describe('declareOpts', function() { expect(function() { validate({foo: 'lol'}); - }).toThrow('Error validating module options: foo must be a number'); + }).toThrow(); }); it('should throw on extra options', function() { @@ -87,6 +87,6 @@ describe('declareOpts', function() { expect(function() { validate({foo: 1, lol: 1}); - }).toThrow('Error validating module options: lol is not allowed'); + }).toThrow(); }); }); From 89b7bd4e5eda4a05ee9b161d9831152e8a9d605b Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 9 Sep 2015 10:28:08 -0700 Subject: [PATCH 0050/2013] Remove IntegrationTests.m reference Summary: In https://github.com/facebook/react-native/commit/7a6f116ed49b9671a2245aff878c1126f4798e7b, IntegrationsTests.m got renamed but for some reason the xcode project still referenced that file and broke the tests. I opened xcode, found the red file, deleted it and saved. Tests are now passing again :) I already landed it: https://github.com/facebook/react-native/commit/f9b2709c8d39b1ec6f729d315855f05d427ab75b Reviewed By: @vjeux Differential Revision: D2424063 --- Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 02e2c9e13..a259da744 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -24,7 +24,6 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */; }; 13DF61B61B67A45000EDB188 /* RCTMethodArgumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */; }; - 141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 141FC1201B222EBB004D5FFB /* IntegrationTests.m */; }; 143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */; }; 144D21241B2204C5006DB32B /* RCTImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTImageUtilTests.m */; }; 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; }; @@ -53,6 +52,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 */; }; + 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 */; }; 8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */; }; @@ -192,7 +192,6 @@ 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSONTests.m; sourceTree = ""; }; 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMethodArgumentTests.m; sourceTree = ""; }; - 141FC1201B222EBB004D5FFB /* IntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IntegrationTests.m; sourceTree = ""; }; 143BC57E1B21E18100462512 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 143BC5811B21E18100462512 /* testLayoutExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testLayoutExampleSnapshot_1@2x.png"; sourceTree = ""; }; 143BC5821B21E18100462512 /* testSliderExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testSliderExampleSnapshot_1@2x.png"; sourceTree = ""; }; @@ -226,6 +225,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 = ""; }; + 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 = ""; }; 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderTests.m; sourceTree = ""; }; @@ -431,7 +431,7 @@ 143BC5961B21E3E100462512 /* UIExplorerIntegrationTests */ = { isa = PBXGroup; children = ( - 141FC1201B222EBB004D5FFB /* IntegrationTests.m */, + 3DB99D0B1BA0340600302749 /* UIExplorerIntegrationTests.m */, 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */, 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */, 143BC5971B21E3E100462512 /* Supporting Files */, @@ -867,7 +867,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */, + 3DB99D0C1BA0340600302749 /* UIExplorerIntegrationTests.m in Sources */, 83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */, 143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */, ); From 6eaa789ec0b74fda3d51bdd6ec4600c23cfdce3e Mon Sep 17 00:00:00 2001 From: JonathanHayward Date: Wed, 9 Sep 2015 15:59:22 -0500 Subject: [PATCH 0051/2013] Specify missing step The instructions as given did not work. The reason they did not work was that, while my .bashrc was made aware of nvm, my running bash process did not know about nvm until I ran ". ~/bashrc", after which things worked as expected. --- docs/GettingStarted.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index f29d1fe49..408ea5086 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -13,7 +13,7 @@ next: tutorial 2. [Xcode](https://developer.apple.com/xcode/downloads/) 6.3 or higher is recommended. 3. [Homebrew](http://brew.sh/) is the recommended way to install io.js, watchman, and flow. 4. Install [io.js](https://iojs.org/) 1.0 or newer. io.js is the modern version of Node. - - Install **nvm** with [its setup instructions here](https://github.com/creationix/nvm#installation). Then run `nvm install iojs-v2 && nvm alias default iojs-v2`, which installs the latest compatible version of io.js and sets up your terminal so that typing `node` runs io.js. With nvm you can install multiple versions of Node and io.js and easily switch between them. + - Install **nvm** with [its setup instructions here](https://github.com/creationix/nvm#installation). To benefit from the changes to your .bashrc, run `source ~/.bashrc`. Then run `nvm install iojs-v2 && nvm alias default iojs-v2`, which installs the latest compatible version of io.js and sets up your terminal so that typing `node` runs io.js. With nvm you can install multiple versions of Node and io.js and easily switch between them. - New to [npm](https://docs.npmjs.com/)? 5. `brew install watchman`. We recommend installing [watchman](https://facebook.github.io/watchman/docs/install.html), otherwise you might hit a node file watching bug. 6. `brew install flow`. If you want to use [flow](http://www.flowtype.org). From cd4e8a9fae001ab4c834d7c846c13fe24198802e Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Wed, 9 Sep 2015 11:13:50 -0700 Subject: [PATCH 0052/2013] Pass in platform argument in offline building Summary: how did this ever work? All build jobs must pass in the platform argument. This also turns the "platform" argument into a required one. I added a task to infer the platform argument from the filename here: t8306875 Reviewed By: @martinbigio Differential Revision: D2425114 --- packager/react-packager/src/Server/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index da8ba0054..2b33f20b3 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -92,7 +92,7 @@ const bundleOpts = declareOpts({ }, platform: { type: 'string', - required: false, + required: true, } }); From b7f5b8c98f077be958e9bd401d8807fe6f442729 Mon Sep 17 00:00:00 2001 From: JonathanHayward Date: Wed, 9 Sep 2015 17:12:58 -0500 Subject: [PATCH 0053/2013] Update GettingStarted.md --- docs/GettingStarted.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 408ea5086..166700f46 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -13,7 +13,7 @@ next: tutorial 2. [Xcode](https://developer.apple.com/xcode/downloads/) 6.3 or higher is recommended. 3. [Homebrew](http://brew.sh/) is the recommended way to install io.js, watchman, and flow. 4. Install [io.js](https://iojs.org/) 1.0 or newer. io.js is the modern version of Node. - - Install **nvm** with [its setup instructions here](https://github.com/creationix/nvm#installation). To benefit from the changes to your .bashrc, run `source ~/.bashrc`. Then run `nvm install iojs-v2 && nvm alias default iojs-v2`, which installs the latest compatible version of io.js and sets up your terminal so that typing `node` runs io.js. With nvm you can install multiple versions of Node and io.js and easily switch between them. + - Install **nvm** with [its setup instructions here](https://github.com/creationix/nvm#installation). To benefit from the changes to your .bashrc, close and reopen your terminal window. Then run `nvm install iojs-v2 && nvm alias default iojs-v2`, which installs the latest compatible version of io.js and sets up your terminal so that typing `node` runs io.js. With nvm you can install multiple versions of Node and io.js and easily switch between them. - New to [npm](https://docs.npmjs.com/)? 5. `brew install watchman`. We recommend installing [watchman](https://facebook.github.io/watchman/docs/install.html), otherwise you might hit a node file watching bug. 6. `brew install flow`. If you want to use [flow](http://www.flowtype.org). From 3ee65225be6f961c26df0fd930439fa34dd4f8f0 Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Wed, 9 Sep 2015 15:59:35 -0700 Subject: [PATCH 0054/2013] Protect against races in deleting corrupt cache Reviewed By: @cpojer Differential Revision: D2426450 --- packager/react-packager/src/lib/loadCacheSync.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packager/react-packager/src/lib/loadCacheSync.js b/packager/react-packager/src/lib/loadCacheSync.js index 64188365c..d04ec0936 100644 --- a/packager/react-packager/src/lib/loadCacheSync.js +++ b/packager/react-packager/src/lib/loadCacheSync.js @@ -20,7 +20,11 @@ function loadCacheSync(cachePath) { } catch (e) { if (e instanceof SyntaxError) { console.warn('Unable to parse cache file. Will clear and continue.'); - fs.unlinkSync(cachePath); + try { + fs.unlinkSync(cachePath); + } catch (err) { + // Someone else might've deleted it. + } return Object.create(null); } throw e; From 3cfac35fd8a3a4446c527fe7a9aceafd38d6ab0e Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Wed, 9 Sep 2015 16:06:13 -0700 Subject: [PATCH 0055/2013] Handle EEXIST error when starting the server Reviewed By: @cpojer Differential Revision: D2426373 --- packager/react-packager/src/SocketInterface/SocketServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packager/react-packager/src/SocketInterface/SocketServer.js b/packager/react-packager/src/SocketInterface/SocketServer.js index 888dcf78a..5778eba02 100644 --- a/packager/react-packager/src/SocketInterface/SocketServer.js +++ b/packager/react-packager/src/SocketInterface/SocketServer.js @@ -148,7 +148,7 @@ class SocketServer { process.send({ type: 'createdServer' }); }, error => { - if (error.code === 'EADDRINUSE') { + if (error.code === 'EADDRINUSE' || error.code === 'EEXIST') { // Server already listening, this may happen if multiple // clients where started in quick succussion (buck). process.send({ type: 'createdServer' }); From 9b2711679880aaf6dfa75c7d24ed886e0bf1f764 Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Wed, 9 Sep 2015 16:30:22 -0700 Subject: [PATCH 0056/2013] Client should throw when server unexpectedly closes the connection Reviewed By: @natthu Differential Revision: D2425357 --- packager/react-packager/src/Server/index.js | 18 +++++++++------- .../src/SocketInterface/SocketClient.js | 21 ++++++++++++++----- .../src/SocketInterface/SocketServer.js | 7 ++++++- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 2b33f20b3..f8113dbbf 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -157,14 +157,16 @@ class Server { } buildBundle(options) { - const opts = bundleOpts(options); - return this._bundler.bundle( - opts.entryFile, - opts.runModule, - opts.sourceMapUrl, - opts.dev, - opts.platform - ); + return Promise.resolve().then(() => { + const opts = bundleOpts(options); + return this._bundler.bundle( + opts.entryFile, + opts.runModule, + opts.sourceMapUrl, + opts.dev, + opts.platform + ); + }); } buildBundleFromUrl(reqUrl) { diff --git a/packager/react-packager/src/SocketInterface/SocketClient.js b/packager/react-packager/src/SocketInterface/SocketClient.js index 32e3b25d7..837e7a2c2 100644 --- a/packager/react-packager/src/SocketInterface/SocketClient.js +++ b/packager/react-packager/src/SocketInterface/SocketClient.js @@ -33,10 +33,7 @@ class SocketClient { this._sock.on('error', (e) => { e.message = `Error connecting to server on ${sockPath} ` + `with error: ${e.message}`; - - if (fs.existsSync(LOG_PATH)) { - e.message += '\nServer logs:\n' + fs.readFileSync(LOG_PATH, 'utf8'); - } + e.message += getServerLogs(); reject(e); }); @@ -45,8 +42,13 @@ class SocketClient { this._resolvers = Object.create(null); const bunser = new bser.BunserBuf(); this._sock.on('data', (buf) => bunser.append(buf)); - bunser.on('value', (message) => this._handleMessage(message)); + + this._sock.on('close', () => { + if (!this._closing) { + throw new Error('Server closed unexpectedly' + getServerLogs()); + } + }); } onReady() { @@ -105,6 +107,7 @@ class SocketClient { close() { debug('closing connection'); + this._closing = true; this._sock.end(); } } @@ -115,3 +118,11 @@ function uid(len) { len = len || 7; return Math.random().toString(35).substr(2, len); } + +function getServerLogs() { + if (fs.existsSync(LOG_PATH)) { + return '\nServer logs:\n' + fs.readFileSync(LOG_PATH, 'utf8'); + } + + return ''; +} diff --git a/packager/react-packager/src/SocketInterface/SocketServer.js b/packager/react-packager/src/SocketInterface/SocketServer.js index 5778eba02..19b4b29cf 100644 --- a/packager/react-packager/src/SocketInterface/SocketServer.js +++ b/packager/react-packager/src/SocketInterface/SocketServer.js @@ -59,7 +59,12 @@ class SocketServer { const bunser = new bser.BunserBuf(); sock.on('data', (buf) => bunser.append(buf)); bunser.on('value', (m) => this._handleMessage(sock, m)); - bunser.on('error', (e) => console.error(e)); + bunser.on('error', (e) => { + e.message = 'Unhandled error from the bser buffer. ' + + 'Either error on encoding or message handling: \n' + + e.message; + throw e; + }); } _handleMessage(sock, m) { From ee1cbf4c9861ffaa03651c374c3ffe6941365c06 Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Wed, 9 Sep 2015 21:08:58 -0700 Subject: [PATCH 0057/2013] [Docs] xcodeproj for new projects is now in iOS/ --- docs/GettingStarted.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 166700f46..f6599edb8 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -27,10 +27,10 @@ We recommend periodically running `brew update && brew upgrade` to keep your pro In the newly created folder `AwesomeProject/` -- Open `AwesomeProject.xcodeproj` and hit run in Xcode. +- Open `iOS/AwesomeProject.xcodeproj` and hit run in Xcode. - Open `index.ios.js` in your text editor of choice and edit some lines. - Hit cmd+R in your iOS simulator to reload the app and see your change! -Congratulations! You've just successfully run and modified your first React Native app. +Congratulations! You've successfully run and modified your first React Native app. _If you run into any issues getting started, see the [troubleshooting page](/react-native/docs/troubleshooting.html#content)._ From 493cb359660e9fd9c69121f5a40a32908738db5c Mon Sep 17 00:00:00 2001 From: Bill Fisher Date: Wed, 9 Sep 2015 23:52:41 -0700 Subject: [PATCH 0058/2013] Fix flow typing of TimingAnimationConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: @​public Make the flow type of TimingAnimationConfig and TimingAnimation the same as SpringAnimationConfig and SpringAnimation. This is a more accurate flow type as both are multiplexed through maybeVectorAnim(). Reviewed By: @sahrens Differential Revision: D2410166 --- Libraries/Animated/Animated.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Libraries/Animated/Animated.js b/Libraries/Animated/Animated.js index 1a357f033..b4d814eff 100644 --- a/Libraries/Animated/Animated.js +++ b/Libraries/Animated/Animated.js @@ -131,7 +131,14 @@ function _flush(rootNode: AnimatedValue): void { } type TimingAnimationConfig = { - toValue: number; + toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY; + easing?: (value: number) => number; + duration?: number; + delay?: number; +}; + +type TimingAnimationConfigSingle = { + toValue: number | AnimatedValue; easing?: (value: number) => number; duration?: number; delay?: number; @@ -142,7 +149,7 @@ var easeInOut = Easing.inOut(Easing.ease); class TimingAnimation extends Animation { _startTime: number; _fromValue: number; - _toValue: number; + _toValue: any; _duration: number; _delay: number; _easing: (value: number) => number; @@ -151,7 +158,7 @@ class TimingAnimation extends Animation { _timeout: any; constructor( - config: TimingAnimationConfig, + config: TimingAnimationConfigSingle, ) { super(); this._toValue = config.toValue; From 19e421a8dcd4f7dd613d0e22cef7fecea7c2fae9 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Thu, 10 Sep 2015 04:45:11 -0700 Subject: [PATCH 0059/2013] Fixed segmented control Reviewed By: @javache Differential Revision: D2428947 --- React/Views/RCTSegmentedControl.m | 1 + 1 file changed, 1 insertion(+) diff --git a/React/Views/RCTSegmentedControl.m b/React/Views/RCTSegmentedControl.m index 543de9c4f..1857bb0ad 100644 --- a/React/Views/RCTSegmentedControl.m +++ b/React/Views/RCTSegmentedControl.m @@ -43,6 +43,7 @@ - (void)didChange { + _selectedIndex = self.selectedSegmentIndex; if (_onChange) { _onChange(@{ @"value": [self titleForSegmentAtIndex:_selectedIndex], From bc697875769caa91c7271a5506bb5a8a73ccab91 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Thu, 10 Sep 2015 14:41:16 +0100 Subject: [PATCH 0060/2013] Use new packager URL in e2e test --- local-cli/generator-ios/templates/app/AppDelegate.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-cli/generator-ios/templates/app/AppDelegate.m b/local-cli/generator-ios/templates/app/AppDelegate.m index 49055c198..3f96e60fe 100644 --- a/local-cli/generator-ios/templates/app/AppDelegate.m +++ b/local-cli/generator-ios/templates/app/AppDelegate.m @@ -31,7 +31,7 @@ * on the same Wi-Fi network. */ - jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle"]; + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"]; /** * OPTION 2 From 2a34239c172479cec4b2b33e8efeff097f81b952 Mon Sep 17 00:00:00 2001 From: Dorota Kapturkiewicz Date: Thu, 10 Sep 2015 08:34:45 -0700 Subject: [PATCH 0061/2013] move AccessibilityExample to oss Differential Revision: D2424931 --- .../UIExplorer/AccessibilityAndroidExample.js | 256 ------------------ 1 file changed, 256 deletions(-) delete mode 100644 Examples/UIExplorer/AccessibilityAndroidExample.js diff --git a/Examples/UIExplorer/AccessibilityAndroidExample.js b/Examples/UIExplorer/AccessibilityAndroidExample.js deleted file mode 100644 index d75907a86..000000000 --- a/Examples/UIExplorer/AccessibilityAndroidExample.js +++ /dev/null @@ -1,256 +0,0 @@ -/** - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ -'use strict'; - -var React = require('react-native'); -var { - StyleSheet, - Text, - View, - TouchableWithoutFeedback, -} = React; -var ToastAndroid = require('ToastAndroid'); -var AccessibilityInfo = require('AccessibilityInfo'); - -var UIExplorerBlock = require('./UIExplorerBlock'); -var UIExplorerPage = require('./UIExplorerPage'); - -var importantForAccessibilityValues = ['auto', 'yes', 'no', 'no-hide-descendants']; - -var AccessibilityAndroidExample = React.createClass({ - - statics: { - title: 'Accessibility', - description: 'Examples of using Accessibility API.', - }, - - getInitialState: function() { - return { - count: 0, - talkbackEnabled: false, - backgroundImportantForAcc: 0, - forgroundImportantForAcc: 0, - }; - }, - - componentDidMount: function() { - AccessibilityInfo.addEventListener( - 'change', - this._handleTouchExplorationChange - ); - AccessibilityInfo.fetch().done((enabled) => { - this.setState({ - count: this.state.count, - talkbackEnabled: enabled}); } - ); - }, - - componentWillUnmount: function() { - AccessibilityInfo.removeEventListener( - 'change', - this._handleTouchExplorationChange - ); - }, - - _handleTouchExplorationChange: function(isEnabled) { - this.setState({ - count: this.state.count, - talkbackEnabled: isEnabled, - }); - }, - - _showAccessibilityToast: function() { - var text = 'TouchExploration is ' + (this.state.talkbackEnabled ? 'enabled' : 'disabled'); - ToastAndroid.show(text, ToastAndroid.SHORT); - }, - - _addOne: function() { - this.setState({ - count: ++this.state.count, - talkbackEnabled: this.state.talkbackEnabled, - }); - }, - - _changeBackgroundImportantForAcc: function() { - this.setState({ - backgroundImportantForAcc: (this.state.backgroundImportantForAcc + 1) % 4, - }); - }, - - _changeForgroundImportantForAcc: function() { - this.setState({ - forgroundImportantForAcc: (this.state.forgroundImportantForAcc + 1) % 4, - }); - }, - - render: function() { - return ( - - - - - - This is - - - nontouchable normal view. - - - - - - - - This is - - - nontouchable accessible view without label. - - - - - - - - This is - - - nontouchable accessible view with label. - - - - - - ToastAndroid.show('Toasts work by default', ToastAndroid.SHORT)}> - - Click me - Or not - - - - - - - - Click me - - - - Clicked {this.state.count} times - - - - - - - - Click to check TouchExploration - - - - - - - - - - - Hello - - - - - - - world - - - - - - - - Change importantForAccessibility for background layout. - - - - - - Background layout importantForAccessibility - - - {importantForAccessibilityValues[this.state.backgroundImportantForAcc]} - - - - - - Change importantForAccessibility for forground layout. - - - - - - Forground layout importantForAccessibility - - - {importantForAccessibilityValues[this.state.forgroundImportantForAcc]} - - - - - - ); - }, -}); - -var styles = StyleSheet.create({ - embedded: { - backgroundColor: 'yellow', - padding:10, - }, - container: { - flex: 1, - backgroundColor: 'white', - padding: 10, - height:150, - }, -}); - -module.exports = AccessibilityAndroidExample; From af05af71257ab6cc04a2a2bab7871ba4f0cac834 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Thu, 10 Sep 2015 09:03:03 -0700 Subject: [PATCH 0062/2013] Update json generation code and save to file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: @​public * Change the JSON generation and remove the dependency on YAJL since it had a 128 depth limit * Enable the profiler bytecode generation to fix missing frames * Save the output to a file on the tmp dir instead of outputting it to the console Reviewed By: @jspahrsummers Differential Revision: D2420754 --- JSCLegacyProfiler/JSCLegacyProfiler.h | 3 +- JSCLegacyProfiler/JSCLegacyProfiler.mm | 311 +++++++++++++++++++------ JSCLegacyProfiler/Makefile | 36 +-- React/Executors/RCTContextExecutor.m | 53 +++-- 4 files changed, 284 insertions(+), 119 deletions(-) diff --git a/JSCLegacyProfiler/JSCLegacyProfiler.h b/JSCLegacyProfiler/JSCLegacyProfiler.h index 6705a1de4..b99d00ab1 100644 --- a/JSCLegacyProfiler/JSCLegacyProfiler.h +++ b/JSCLegacyProfiler/JSCLegacyProfiler.h @@ -6,7 +6,8 @@ extern "C" { +void nativeProfilerEnableBytecode(void); void nativeProfilerStart(JSContextRef ctx, const char *title); -const char *nativeProfilerEnd(JSContextRef ctx, const char *title); +void nativeProfilerEnd(JSContextRef ctx, const char *title, const char *filename); } diff --git a/JSCLegacyProfiler/JSCLegacyProfiler.mm b/JSCLegacyProfiler/JSCLegacyProfiler.mm index e906eda0f..d9966f32c 100644 --- a/JSCLegacyProfiler/JSCLegacyProfiler.mm +++ b/JSCLegacyProfiler/JSCLegacyProfiler.mm @@ -6,125 +6,298 @@ #include "JSProfilerPrivate.h" #include "JSStringRef.h" #include "String.h" +#include "Options.h" -#include +enum json_gen_status { + json_gen_status_ok = 0, + json_gen_status_error = 1, +}; + +enum json_entry { + json_entry_key, + json_entry_value, +}; + +namespace { + +struct json_state { + FILE *fileOut; + bool hasFirst; +}; + +} + +typedef json_state *json_gen; + +static void json_escaped_cstring_printf(json_gen gen, const char *str) { + const char *cursor = str; + fputc('"', gen->fileOut); + while (*cursor) { + const char *escape = nullptr; + switch (*cursor) { + case '"': + escape = "\\\""; + break; + case '\b': + escape = "\\b"; + break; + case '\f': + escape = "\\f"; + break; + case '\n': + escape = "\\n"; + break; + case '\r': + escape = "\\r"; + break; + case '\t': + escape = "\\t"; + break; + case '\\': + escape = "\\\\"; + break; + default: + break; + } + if (escape != nullptr) { + fwrite(escape, 1, strlen(escape), gen->fileOut); + } else { + fputc(*cursor, gen->fileOut); + } + cursor++; + } + fputc('"', gen->fileOut); +} + +static json_gen_status json_gen_key_cstring(json_gen gen, const char *buffer) { + if (gen->fileOut == nullptr) { + return json_gen_status_error; + } + + if (gen->hasFirst) { + fprintf(gen->fileOut, ","); + } + gen->hasFirst = true; + + json_escaped_cstring_printf(gen, buffer); + return json_gen_status_ok; +} + +static json_gen_status json_gen_map_open(json_gen gen, json_entry entryType) { + if (gen->fileOut == nullptr) { + return json_gen_status_error; + } + + if (entryType == json_entry_value) { + fprintf(gen->fileOut, ":"); + } else if (entryType == json_entry_key) { + if (gen->hasFirst) { + fprintf(gen->fileOut, ","); + } + } + fprintf(gen->fileOut, "{"); + gen->hasFirst = false; + return json_gen_status_ok; +} + +static json_gen_status json_gen_map_close(json_gen gen) { + if (gen->fileOut == nullptr) { + return json_gen_status_error; + } + + fprintf(gen->fileOut, "}"); + gen->hasFirst = true; + return json_gen_status_ok; +} + +static json_gen_status json_gen_array_open(json_gen gen, json_entry entryType) { + if (gen->fileOut == nullptr) { + return json_gen_status_error; + } + + if (entryType == json_entry_value) { + fprintf(gen->fileOut, ":"); + } else if (entryType == json_entry_key) { + if (gen->hasFirst) { + fprintf(gen->fileOut, ","); + } + } + fprintf(gen->fileOut, "["); + gen->hasFirst = false; + return json_gen_status_ok; +} + +static json_gen_status json_gen_array_close(json_gen gen) { + if (gen->fileOut == nullptr) { + return json_gen_status_error; + } + + fprintf(gen->fileOut, "]"); + gen->hasFirst = true; + return json_gen_status_ok; +} + +static json_gen_status json_gen_keyvalue_cstring(json_gen gen, const char *key, const char *value) { + if (gen->fileOut == nullptr) { + return json_gen_status_error; + } + + if (gen->hasFirst) { + fprintf(gen->fileOut, ","); + } + gen->hasFirst = true; + + fprintf(gen->fileOut, "\"%s\" : ", key); + json_escaped_cstring_printf(gen, value); + + return json_gen_status_ok; +} + + +static json_gen_status json_gen_keyvalue_integer(json_gen gen, const char *key, int value) { + if (gen->fileOut == nullptr) { + return json_gen_status_error; + } + + if (gen->hasFirst) { + fprintf(gen->fileOut, ","); + } + gen->hasFirst = true; + + fprintf(gen->fileOut, "\"%s\": %d", key, value); + return json_gen_status_ok; +} + +static json_gen_status json_gen_keyvalue_double(json_gen gen, const char *key, double value) { + if (gen->fileOut == nullptr) { + return json_gen_status_error; + } + + if (gen->hasFirst) { + fprintf(gen->fileOut, ","); + } + gen->hasFirst = true; + + fprintf(gen->fileOut, "\"%s\": %.20g", key, value); + return json_gen_status_ok; +} + +static json_gen json_gen_alloc(const char *fileName) { + json_gen gen = (json_gen)malloc(sizeof(json_state)); + memset(gen, 0, sizeof(json_state)); + gen->fileOut = fopen(fileName, "wb"); + return gen; +} + +static void json_gen_free(json_gen gen) { + if (gen->fileOut) { + fclose(gen->fileOut); + } + free(gen); +} #define GEN_AND_CHECK(expr) \ do { \ - yajl_gen_status GEN_AND_CHECK_status = (expr); \ - if (GEN_AND_CHECK_status != yajl_gen_status_ok) { \ + json_gen_status GEN_AND_CHECK_status = (expr); \ + if (GEN_AND_CHECK_status != json_gen_status_ok) { \ return GEN_AND_CHECK_status; \ } \ } while (false) -static inline yajl_gen_status yajl_gen_cstring(yajl_gen gen, const char *str) { - return yajl_gen_string(gen, (const unsigned char*)str, strlen(str)); -} -static yajl_gen_status append_children_array_json(yajl_gen gen, const JSC::ProfileNode *node); -static yajl_gen_status append_node_json(yajl_gen gen, const JSC::ProfileNode *node); +static json_gen_status append_children_array_json(json_gen gen, const JSC::ProfileNode *node); +static json_gen_status append_node_json(json_gen gen, const JSC::ProfileNode *node); -static yajl_gen_status append_root_json(yajl_gen gen, const JSC::Profile *profile) { - GEN_AND_CHECK(yajl_gen_map_open(gen)); - GEN_AND_CHECK(yajl_gen_cstring(gen, "rootNodes")); +static json_gen_status append_root_json(json_gen gen, const JSC::Profile *profile) { + GEN_AND_CHECK(json_gen_map_open(gen, json_entry_key)); + GEN_AND_CHECK(json_gen_key_cstring(gen, "rootNodes")); GEN_AND_CHECK(append_children_array_json(gen, profile->head())); - GEN_AND_CHECK(yajl_gen_map_close(gen)); + GEN_AND_CHECK(json_gen_map_close(gen)); - return yajl_gen_status_ok; + return json_gen_status_ok; } -static yajl_gen_status append_children_array_json(yajl_gen gen, const JSC::ProfileNode *node) { - GEN_AND_CHECK(yajl_gen_array_open(gen)); +static json_gen_status append_children_array_json(json_gen gen, const JSC::ProfileNode *node) { + GEN_AND_CHECK(json_gen_array_open(gen, json_entry_value)); for (RefPtr child : node->children()) { GEN_AND_CHECK(append_node_json(gen, child.get())); } - GEN_AND_CHECK(yajl_gen_array_close(gen)); + GEN_AND_CHECK(json_gen_array_close(gen)); - return yajl_gen_status_ok; + return json_gen_status_ok; } -static yajl_gen_status append_node_json(yajl_gen gen, const JSC::ProfileNode *node) { - GEN_AND_CHECK(yajl_gen_map_open(gen)); - GEN_AND_CHECK(yajl_gen_cstring(gen, "id")); - GEN_AND_CHECK(yajl_gen_integer(gen, node->id())); +static json_gen_status append_node_json(json_gen gen, const JSC::ProfileNode *node) { + GEN_AND_CHECK(json_gen_map_open(gen, json_entry_key)); + GEN_AND_CHECK(json_gen_keyvalue_integer(gen, "id", node->id())); if (!node->functionName().isEmpty()) { - GEN_AND_CHECK(yajl_gen_cstring(gen, "functionName")); - GEN_AND_CHECK(yajl_gen_cstring(gen, node->functionName().utf8().data())); + GEN_AND_CHECK(json_gen_keyvalue_cstring(gen, "functionName", node->functionName().utf8().data())); } if (!node->url().isEmpty()) { - GEN_AND_CHECK(yajl_gen_cstring(gen, "url")); - GEN_AND_CHECK(yajl_gen_cstring(gen, node->url().utf8().data())); - GEN_AND_CHECK(yajl_gen_cstring(gen, "lineNumber")); - GEN_AND_CHECK(yajl_gen_integer(gen, node->lineNumber())); - GEN_AND_CHECK(yajl_gen_cstring(gen, "columnNumber")); - GEN_AND_CHECK(yajl_gen_integer(gen, node->columnNumber())); + GEN_AND_CHECK(json_gen_keyvalue_cstring(gen, "url", node->url().utf8().data())); + GEN_AND_CHECK(json_gen_keyvalue_integer(gen, "lineNumber", node->lineNumber())); + GEN_AND_CHECK(json_gen_keyvalue_integer(gen, "columnNumber", node->columnNumber())); } - GEN_AND_CHECK(yajl_gen_cstring(gen, "calls")); - GEN_AND_CHECK(yajl_gen_array_open(gen)); + GEN_AND_CHECK(json_gen_key_cstring(gen, "calls")); + GEN_AND_CHECK(json_gen_array_open(gen, json_entry_value)); for (const JSC::ProfileNode::Call &call : node->calls()) { - GEN_AND_CHECK(yajl_gen_map_open(gen)); - GEN_AND_CHECK(yajl_gen_cstring(gen, "startTime")); - GEN_AND_CHECK(yajl_gen_double(gen, call.startTime())); - GEN_AND_CHECK(yajl_gen_cstring(gen, "totalTime")); - GEN_AND_CHECK(yajl_gen_double(gen, call.totalTime())); - GEN_AND_CHECK(yajl_gen_map_close(gen)); + GEN_AND_CHECK(json_gen_map_open(gen, json_entry_key)); + GEN_AND_CHECK(json_gen_keyvalue_double(gen, "startTime", call.startTime())); + GEN_AND_CHECK(json_gen_keyvalue_double(gen, "totalTime", call.totalTime())); + GEN_AND_CHECK(json_gen_map_close(gen)); } - GEN_AND_CHECK(yajl_gen_array_close(gen)); + GEN_AND_CHECK(json_gen_array_close(gen)); if (!node->children().isEmpty()) { - GEN_AND_CHECK(yajl_gen_cstring(gen, "children")); + GEN_AND_CHECK(json_gen_key_cstring(gen, "children")); GEN_AND_CHECK(append_children_array_json(gen, node)); } - GEN_AND_CHECK(yajl_gen_map_close(gen)); + GEN_AND_CHECK(json_gen_map_close(gen)); - return yajl_gen_status_ok; + return json_gen_status_ok; } -static char *render_error_code(yajl_gen_status status) { - char err[1024]; - snprintf(err, sizeof(err), "{\"error\": %d}", (int)status); - return strdup(err); -} - -static char *convert_to_json(const JSC::Profile *profile) { - yajl_gen_status status; - yajl_gen gen = yajl_gen_alloc(NULL); +static void convert_to_json(const JSC::Profile *profile, const char *filename) { + json_gen_status status; + json_gen gen = json_gen_alloc(filename); status = append_root_json(gen, profile); - if (status != yajl_gen_status_ok) { - yajl_gen_free(gen); - return render_error_code(status); + if (status != json_gen_status_ok) { + FILE *fileOut = fopen(filename, "wb"); + if (fileOut != nullptr) { + fprintf(fileOut, "{\"error\": %d}", (int)status); + fclose(fileOut); + } } - - const unsigned char *buf; - size_t buf_size; - status = yajl_gen_get_buf(gen, &buf, &buf_size); - if (status != yajl_gen_status_ok) { - yajl_gen_free(gen); - return render_error_code(status); - } - - char *json_copy = strdup((const char*)buf); - yajl_gen_free(gen); - return json_copy; + json_gen_free(gen); } -static const char *JSEndProfilingAndRender(JSContextRef ctx, const char *title) +// Based on JSEndProfiling, with a little extra code to return the profile as JSON. +static void JSEndProfilingAndRender(JSContextRef ctx, const char *title, const char *filename) { JSC::ExecState *exec = toJS(ctx); JSC::LegacyProfiler *profiler = JSC::LegacyProfiler::profiler(); RefPtr rawProfile = profiler->stopProfiling(exec, WTF::String(title)); - return convert_to_json(rawProfile.get()); + convert_to_json(rawProfile.get(), filename); +} + +extern "C" { + +void nativeProfilerEnableBytecode(void) +{ + JSC::Options::setOption("forceProfilerBytecodeGeneration=true"); } void nativeProfilerStart(JSContextRef ctx, const char *title) { JSStartProfiling(ctx, JSStringCreateWithUTF8CString(title)); } -const char *nativeProfilerEnd( JSContextRef ctx, const char *title) { - return JSEndProfilingAndRender(ctx, title); +void nativeProfilerEnd(JSContextRef ctx, const char *title, const char *filename) { + JSEndProfilingAndRender(ctx, title, filename); +} + } diff --git a/JSCLegacyProfiler/Makefile b/JSCLegacyProfiler/Makefile index 7121d6e27..015030735 100644 --- a/JSCLegacyProfiler/Makefile +++ b/JSCLegacyProfiler/Makefile @@ -17,7 +17,7 @@ PLATFORM = \ SYSROOT = -isysroot $(call SDK_PATH,$${PLATFORM}) -IOS8_LIBS = download/WebCore/WebCore-7600.1.25 download/WTF/WTF-7600.1.24 download/JavaScriptCore/JavaScriptCore-7600.1.17 download/JavaScriptCore/JavaScriptCore-7600.1.17/Bytecodes.h libyajl.a +IOS8_LIBS = download/WebCore/WebCore-7600.1.25 download/WTF/WTF-7600.1.24 download/JavaScriptCore/JavaScriptCore-7600.1.17 download/JavaScriptCore/JavaScriptCore-7600.1.17/Bytecodes.h ios8: RCTJSCProfiler.ios8.dylib /tmp/RCTJSCProfiler ifneq ($(SDK_VERSION), 8) @@ -47,43 +47,17 @@ RCTJSCProfiler_%.ios8.dylib: $(IOS8_LIBS) -I download \ -I download/WebCore/WebCore-7600.1.25/icu \ -I download/WTF/WTF-7600.1.24 \ - -I download/yajl-2.1.0/build/yajl-2.1.0/include \ -DNDEBUG=1\ -miphoneos-version-min=8.0 \ $(SYSROOT) \ $(HEADER_PATHS) \ -undefined dynamic_lookup \ - JSCLegacyProfiler.mm libyajl.a + JSCLegacyProfiler.mm .PRECIOUS: %/Bytecodes.h %/Bytecodes.h: python $*/generate-bytecode-files --bytecodes_h $@ $*/bytecode/BytecodeList.json -.PRECIOUS: libyajl.a -libyajl.a: $(patsubst %,libyajl_%.a,$(ARCHS)) - lipo -create $^ -output $@ - -.PRECIOUS: libyajl_%.a -libyajl_%.a: download/yajl-2.1.0 - $(PLATFORM) \ - cd download/yajl-2.1.0/src; \ - clang -arch $(*F) -std=c99 \ - -miphoneos-version-min=8.0 \ - $(SYSROOT) \ - -I ../build/yajl-2.1.0/include \ - -c `find . -name '*.c'` - find download/yajl-2.1.0/src/ -name '*.o' -exec libtool -static -o $@ {} + - -.PRECIOUS: download/yajl-2.1.0 -download/yajl-2.1.0: download/yajl-2.1.0.tar.gz - tar -zxvf $< -C download > /dev/null - mkdir -p download/yajl-2.1.0/build && cd download/yajl-2.1.0/build && cmake .. - -.PRECIOUS: download/yajl-2.1.0.tar.gz -download/yajl-2.1.0.tar.gz: - mkdir -p `dirname $@` - curl -o $@ https://codeload.github.com/lloyd/yajl/tar.gz/2.1.0 - .PRECIOUS: download/% download/%: download/%.tar.gz tar -zxvf $< -C `dirname $@` > /dev/null @@ -95,6 +69,6 @@ download/%: download/%.tar.gz .PHONY: clean clean: - @rm -rf $(wildcard *.dylib) - @rm -rf $(wildcard *.a) - @rm -rf download + -rm -rf $(wildcard *.dylib) + -rm -rf $(wildcard *.a) + -rm -rf download diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 85854bf64..60cdd3cd1 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -206,6 +206,40 @@ static JSValueRef RCTNativeTraceEndSection(JSContextRef context, __unused JSObje return JSValueMakeUndefined(context); } +static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) +{ +#if RCT_JSC_PROFILER + void *JSCProfiler = dlopen(RCT_JSC_PROFILER_DYLIB, RTLD_NOW); + if (JSCProfiler != NULL) { + void (*nativeProfilerStart)(JSContextRef, const char *) = + (__typeof__(nativeProfilerStart))dlsym(JSCProfiler, "nativeProfilerStart"); + void (*nativeProfilerEnd)(JSContextRef, const char *, const char *) = + (__typeof__(nativeProfilerEnd))dlsym(JSCProfiler, "nativeProfilerEnd"); + + if (nativeProfilerStart != NULL && nativeProfilerEnd != NULL) { + void (*nativeProfilerEnableByteCode)(void) = + (__typeof__(nativeProfilerEnableByteCode))dlsym(JSCProfiler, "nativeProfilerEnableByteCode"); + + if (nativeProfilerEnableByteCode != NULL) { + nativeProfilerEnableByteCode(); + } + + __block BOOL isProfiling = NO; + [bridge.devMenu addItem:@"Profile" handler:^{ + if (isProfiling) { + NSString *outputFile = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cpu_profile.json"]; + nativeProfilerEnd(context, "profile", outputFile.UTF8String); + RCTLogInfo(@"CPU profile outputed to '%@'", outputFile); + } else { + nativeProfilerStart(context, "profile"); + } + isProfiling = !isProfiling; + }]; + } + } +#endif +} + #endif + (void)runRunLoopThread @@ -284,24 +318,7 @@ static JSValueRef RCTNativeTraceEndSection(JSContextRef context, __unused JSObje [strongSelf _addNativeHook:RCTNativeTraceBeginSection withName:"nativeTraceBeginSection"]; [strongSelf _addNativeHook:RCTNativeTraceEndSection withName:"nativeTraceEndSection"]; -#if RCT_JSC_PROFILER - void *JSCProfiler = dlopen(RCT_JSC_PROFILER_DYLIB, RTLD_NOW); - if (JSCProfiler != NULL) { - void (*nativeProfilerStart)(JSContextRef, const char *) = (void (*)(JSContextRef, const char *))dlsym(JSCProfiler, "nativeProfilerStart"); - const char *(*nativeProfilerEnd)(JSContextRef, const char *) = (const char *(*)(JSContextRef, const char *))dlsym(JSCProfiler, "nativeProfilerEnd"); - if (nativeProfilerStart != NULL && nativeProfilerEnd != NULL) { - __block BOOL isProfiling = NO; - [_bridge.devMenu addItem:@"Profile" handler:^{ - if (isProfiling) { - RCTLogInfo(@"%s", nativeProfilerEnd(strongSelf->_context.ctx, "profile")); - } else { - nativeProfilerStart(strongSelf->_context.ctx, "profile"); - } - isProfiling = !isProfiling; - }]; - } - } -#endif + RCTInstallJSCProfiler(_bridge, strongSelf->_context.ctx); for (NSString *event in @[RCTProfileDidStartProfiling, RCTProfileDidEndProfiling]) { [[NSNotificationCenter defaultCenter] addObserver:strongSelf From e2ffac28e419fe6fe69c13b48bd4016401bb918d Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Thu, 10 Sep 2015 09:48:15 -0700 Subject: [PATCH 0063/2013] A deep dependency of yeoman just spams the console.log with giant json. This diff silences console.log around the require to kill them --- local-cli/__tests__/generator-ios-test.js | 13 ++++++++++++- local-cli/__tests__/generator-test.js | 15 +++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/local-cli/__tests__/generator-ios-test.js b/local-cli/__tests__/generator-ios-test.js index ed3bb9728..43bd6ca7c 100644 --- a/local-cli/__tests__/generator-ios-test.js +++ b/local-cli/__tests__/generator-ios-test.js @@ -5,10 +5,21 @@ jest.autoMockOff(); var path = require('path'); describe('react:ios', function() { - var assert = require('yeoman-generator').assert; + var assert; beforeEach(function() { + // A deep dependency of yeoman spams console.log with giant json objects. + // yeoman-generator/node_modules/ + // download/node_modules/ + // caw/node_modules/ + // get-proxy/node_modules/ + // rc/index.js + var log = console.log; + console.log = function() {}; + assert = require('yeoman-generator').assert; var helpers = require('yeoman-generator').test; + console.log = log; + var generated = false; runs(function() { diff --git a/local-cli/__tests__/generator-test.js b/local-cli/__tests__/generator-test.js index f138c8dd6..04c7c1c16 100644 --- a/local-cli/__tests__/generator-test.js +++ b/local-cli/__tests__/generator-test.js @@ -6,10 +6,21 @@ var path = require('path'); var fs = require('fs'); describe('react:react', function() { - var assert = require('yeoman-generator').assert; + var assert; beforeEach(function() { + // A deep dependency of yeoman spams console.log with giant json objects. + // yeoman-generator/node_modules/ + // download/node_modules/ + // caw/node_modules/ + // get-proxy/node_modules/ + // rc/index.js + var log = console.log; + console.log = function() {}; + assert = require('yeoman-generator').assert; var helpers = require('yeoman-generator').test; + console.log = log; + var generated = false; runs(function() { @@ -46,4 +57,4 @@ describe('react:react', function() { expect(stat.isDirectory()).toBe(true); }); -}); \ No newline at end of file +}); From 33036b1f01c1e3816745bfe9c243a478445559ee Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Thu, 10 Sep 2015 10:05:08 -0700 Subject: [PATCH 0064/2013] Fixed DatePickerIOS onChange event Reviewed By: @javache Differential Revision: D2429598 --- Libraries/Components/DatePicker/DatePickerIOS.ios.js | 4 +++- React/Views/RCTDatePickerManager.m | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Libraries/Components/DatePicker/DatePickerIOS.ios.js b/Libraries/Components/DatePicker/DatePickerIOS.ios.js index f184c6f79..f9a0a6d90 100644 --- a/Libraries/Components/DatePicker/DatePickerIOS.ios.js +++ b/Libraries/Components/DatePicker/DatePickerIOS.ios.js @@ -145,6 +145,8 @@ var styles = StyleSheet.create({ }, }); -var RCTDatePickerIOS = requireNativeComponent('RCTDatePicker', DatePickerIOS); +var RCTDatePickerIOS = requireNativeComponent('RCTDatePicker', DatePickerIOS, { + nativeOnly: { onChange: true }, +}); module.exports = DatePickerIOS; diff --git a/React/Views/RCTDatePickerManager.m b/React/Views/RCTDatePickerManager.m index ef9515e5a..5bf0ebad4 100644 --- a/React/Views/RCTDatePickerManager.m +++ b/React/Views/RCTDatePickerManager.m @@ -38,6 +38,7 @@ RCT_EXPORT_VIEW_PROPERTY(date, NSDate) RCT_EXPORT_VIEW_PROPERTY(minimumDate, NSDate) RCT_EXPORT_VIEW_PROPERTY(maximumDate, NSDate) RCT_EXPORT_VIEW_PROPERTY(minuteInterval, NSInteger) +RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) RCT_REMAP_VIEW_PROPERTY(mode, datePickerMode, UIDatePickerMode) RCT_REMAP_VIEW_PROPERTY(timeZoneOffsetInMinutes, timeZone, NSTimeZone) From f8f75ff61236bcf9dc6d58e54f6f33276dc5f10c Mon Sep 17 00:00:00 2001 From: Yamill Vallecillo Date: Thu, 10 Sep 2015 13:53:23 -0700 Subject: [PATCH 0065/2013] Navigator.NavigationBar Landscape Fix Summary: NavigationBar items are fixed to Portrait when Device is in Landscape. This supplements that fix by removing the `width` properties and just using `left` and `right` positioning respectively. Before: ![ios simulator screen shot sep 8 2015 1 27 02 pm](https://cloud.githubusercontent.com/assets/755943/9743817/3e6b858a-5636-11e5-80e8-81e62b46c46e.png) After: ![ios simulator screen shot sep 8 2015 1 29 21 pm](https://cloud.githubusercontent.com/assets/755943/9743822/43e7d4b4-5636-11e5-8e1c-9f13bdc492b2.png) Closes https://github.com/facebook/react-native/pull/2606 Reviewed By: @vjeux Differential Revision: D2426942 Pulled By: @ericvicenti --- .../Navigator/NavigatorNavigationBarStyles.ios.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/NavigatorNavigationBarStyles.ios.js b/Libraries/CustomComponents/Navigator/NavigatorNavigationBarStyles.ios.js index 769722a64..53ec69a4b 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorNavigationBarStyles.ios.js +++ b/Libraries/CustomComponents/Navigator/NavigatorNavigationBarStyles.ios.js @@ -41,8 +41,8 @@ var BASE_STYLES = { position: 'absolute', top: STATUS_BAR_HEIGHT, left: 0, + right: 0, alignItems: 'center', - width: SCREEN_WIDTH, height: NAV_BAR_HEIGHT, backgroundColor: 'transparent', }, @@ -52,18 +52,16 @@ var BASE_STYLES = { left: 0, overflow: 'hidden', opacity: 1, - width: SCREEN_WIDTH / 3, height: NAV_BAR_HEIGHT, backgroundColor: 'transparent', }, RightButton: { position: 'absolute', top: STATUS_BAR_HEIGHT, - left: 2 * SCREEN_WIDTH / 3, + right: 0, overflow: 'hidden', opacity: 1, alignItems: 'flex-end', - width: SCREEN_WIDTH / 3, height: NAV_BAR_HEIGHT, backgroundColor: 'transparent', }, From b45f89e69fbd07575b56830ef677407e92647dc9 Mon Sep 17 00:00:00 2001 From: Xiqi Liu Date: Fri, 11 Sep 2015 01:23:38 -0700 Subject: [PATCH 0066/2013] Add Jest test to check dependency version matches. Reviewed By: @vjeux Differential Revision: D2396634 --- package.json | 60 ++++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index bb21b3e9f..217af0aa8 100644 --- a/package.json +++ b/package.json @@ -46,38 +46,38 @@ "react-native-start": "packager/packager.sh" }, "dependencies": { - "absolute-path": "0.0.0", - "babel": "5.8.23", - "babel-core": "5.8.23", - "bser": "1.0.2", - "chalk": "1.1.1", - "connect": "2.8.3", - "debug": "2.2.0", - "graceful-fs": "4.1.2", - "image-size": "0.3.5", - "immutable": "3.7.5", - "joi": "6.6.1", - "jstransform": "11.0.3", - "module-deps": "3.9.1", - "optimist": "0.6.1", - "progress": "1.1.8", - "promise": "7.0.4", - "react-timer-mixin": "0.13.2", + "absolute-path": "^0.0.0", + "babel": "^5.8.23", + "babel-core": "^5.8.23", + "bser": "^1.0.2", + "chalk": "^1.1.1", + "connect": "^2.8.3", + "debug": "^2.2.0", + "graceful-fs": "^4.1.2", + "image-size": "^0.3.5", + "immutable": "^3.7.5", + "joi": "^6.6.1", + "jstransform": "^11.0.3", + "module-deps": "^3.9.1", + "optimist": "^0.6.1", + "progress": "^1.1.8", + "promise": "^7.0.4", + "react-timer-mixin": "^0.13.2", "react-tools": "git://github.com/facebook/react#b4e74e38e43ac53af8acd62c78c9213be0194245", - "rebound": "0.0.13", - "regenerator": "0.8.36", + "rebound": "^0.0.13", + "regenerator": "^0.8.36", "sane": "^1.2.0", - "semver": "5.0.1", - "source-map": "0.4.4", - "stacktrace-parser": "0.1.3", - "uglify-js": "2.4.24", - "underscore": "1.8.3", - "wordwrap": "1.0.0", - "worker-farm": "1.3.1", - "ws": "0.8.0", - "yargs": "3.24.0", - "yeoman-environment": "1.2.7", - "yeoman-generator": "0.20.3" + "semver": "^5.0.1", + "source-map": "^0.4.4", + "stacktrace-parser": "^0.1.3", + "uglify-js": "^2.4.24", + "underscore": "^1.8.3", + "wordwrap": "^1.0.0", + "worker-farm": "^1.3.1", + "ws": "^0.8.0", + "yargs": "^3.24.0", + "yeoman-environment": "^1.2.7", + "yeoman-generator": "^0.20.3" }, "devDependencies": { "jest-cli": "0.5.1", From 360d04e9c86cf7af7d946331dba91adcdc845ad6 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Fri, 11 Sep 2015 01:56:51 -0700 Subject: [PATCH 0067/2013] Add Timer example Differential Revision: D2433417 committer: Service User --- Examples/UIExplorer/TimerExample.js | 8 +++++++- Examples/UIExplorer/UIExplorerButton.js | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Examples/UIExplorer/TimerExample.js b/Examples/UIExplorer/TimerExample.js index 8ef94cafe..8d28e18ae 100644 --- a/Examples/UIExplorer/TimerExample.js +++ b/Examples/UIExplorer/TimerExample.js @@ -18,7 +18,9 @@ var React = require('react-native'); var { AlertIOS, + Platform, Text, + ToastAndroid, TouchableHighlight, View, } = React; @@ -73,7 +75,11 @@ var TimerTester = React.createClass({ var msg = 'Finished ' + this._ii + ' ' + this.props.type + ' calls.\n' + 'Elapsed time: ' + e + ' ms\n' + (e / this._ii) + ' ms / iter'; console.log(msg); - AlertIOS.alert(msg); + if (Platform.OS === 'ios') { + AlertIOS.alert(msg); + } else if (Platform.OS === 'android') { + ToastAndroid.show(msg, ToastAndroid.SHORT); + } this._start = 0; this.forceUpdate(() => { this._ii = 0; }); return; diff --git a/Examples/UIExplorer/UIExplorerButton.js b/Examples/UIExplorer/UIExplorerButton.js index 082fe86ba..21f8efc8c 100644 --- a/Examples/UIExplorer/UIExplorerButton.js +++ b/Examples/UIExplorer/UIExplorerButton.js @@ -42,14 +42,14 @@ var UIExplorerButton = React.createClass({ var styles = StyleSheet.create({ button: { - borderColor: 'dimgray', + borderColor: '#696969', borderRadius: 8, borderWidth: 1, padding: 10, margin: 5, alignItems: 'center', justifyContent: 'center', - backgroundColor: 'lightgrey', + backgroundColor: '#d3d3d3', }, }); From 6a9838a748c5911e4c3e69cba31eb89be22e6cf8 Mon Sep 17 00:00:00 2001 From: Liubko Date: Fri, 11 Sep 2015 16:17:56 +0300 Subject: [PATCH 0068/2013] Showcase: add 'FastPaper' app --- website/src/react-native/showcase.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index 1086060cd..48ba2b134 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -66,6 +66,12 @@ var apps = [ link: 'https://itunes.apple.com/us/app/facebook-ads-manager/id964397083?mt=8', author: 'Facebook', }, + { + name: 'FastPaper', + icon: 'http://a2.mzstatic.com/us/r30/Purple5/v4/72/b4/d8/72b4d866-90d2-3aad-d1dc-0315f2d9d045/icon350x350.jpeg', + link: 'https://itunes.apple.com/us/app/fast-paper/id1001174614', + author: 'Liubomyr Mykhalchenko (@liubko)', + }, { name: 'HSK Level 1 Chinese Flashcards', icon: 'http://is2.mzstatic.com/image/pf/us/r30/Purple1/v4/b2/4f/3a/b24f3ae3-2597-cc70-1040-731b425a5904/mzl.amxdcktl.jpg', From 20cd649553ed2cade18c693f2efb375c52cae96c Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Fri, 11 Sep 2015 06:35:25 -0700 Subject: [PATCH 0069/2013] Automatically save and convert JavaScript profile to chrome format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: @​public Migrate scripts to open source and add new route on the packager to directly convert profiler outputs to a devtools compatible format. Reviewed By: @jspahrsummers Differential Revision: D2425740 --- JSCLegacyProfiler/json2trace | 254 ++++++++++++++++++++ JSCLegacyProfiler/smap.py | 343 +++++++++++++++++++++++++++ JSCLegacyProfiler/trace_data.py | 244 +++++++++++++++++++ React/Base/RCTBatchedBridge.m | 36 +-- React/Base/RCTProfile.h | 8 + React/Base/RCTProfile.m | 41 ++++ React/Executors/RCTContextExecutor.m | 6 +- packager/packager.js | 46 +++- 8 files changed, 936 insertions(+), 42 deletions(-) create mode 100755 JSCLegacyProfiler/json2trace create mode 100644 JSCLegacyProfiler/smap.py create mode 100644 JSCLegacyProfiler/trace_data.py diff --git a/JSCLegacyProfiler/json2trace b/JSCLegacyProfiler/json2trace new file mode 100755 index 000000000..8dac589a7 --- /dev/null +++ b/JSCLegacyProfiler/json2trace @@ -0,0 +1,254 @@ +#!/usr/bin/env python +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import json +import smap +import trace_data +import urllib + +SECONDS_TO_NANOSECONDS = (1000*1000) +SAMPLE_DELTA_IN_SECONDS = 0.0001 + +class Marker(object): + def __init__(self, _name, _timestamp, _depth, _is_end, _ident, url, line, col): + self.name = _name + self.timestamp = _timestamp + self.depth = _depth + self.is_end = _is_end + self.ident = _ident + self.url = url + self.line = line + self.col = col + +# sort markers making sure they are ordered by timestamp then depth of function call +# and finally that markers of the same ident are sorted in the order begin then end + def __cmp__(self, other): + if self.timestamp < other.timestamp: + return -1 + if self.timestamp > other.timestamp: + return 1 + if self.depth < other.depth: + return -1 + if self.depth > other.depth: + return 1 + if self.ident == other.ident: + if self.is_end: + return 1 + return 0 + +# calculate marker name based on combination of function name and location +def _calcname(entry): + funcname = "" + if "functionName" in entry: + funcname = funcname + entry["functionName"] + return funcname + +def _calcurl(mapcache, entry, map_file): + if entry.url not in mapcache: + map_url = entry.url.replace('.bundle', '.map') + + if map_url != entry.url: + if map_file: + print('Loading sourcemap from:' + map_file) + map_url = map_file + + try: + url_file = urllib.urlopen(map_url) + if url_file != None: + entries = smap.parse(url_file) + mapcache[entry.url] = entries + except Exception, e: + mapcache[entry.url] = [] + + if entry.url in mapcache: + source_entry = smap.find(mapcache[entry.url], entry.line, entry.col) + if source_entry: + entry.url = 'file://' + source_entry.src + entry.line = source_entry.src_line + entry.col = source_entry.src_col + +def _compute_markers(markers, call_point, depth): + name = _calcname(call_point) + ident = len(markers) + url = "" + lineNumber = -1 + columnNumber = -1 + if "url" in call_point: + url = call_point["url"] + if "lineNumber" in call_point: + lineNumber = call_point["lineNumber"] + if "columnNumber" in call_point: + columnNumber = call_point["columnNumber"] + + for call in call_point["calls"]: + markers.append(Marker(name, call["startTime"], depth, 0, ident, url, lineNumber, columnNumber)) + markers.append(Marker(name, call["startTime"] + call["totalTime"], depth, 1, ident, url, lineNumber, columnNumber)) + ident = ident + 2 + if "children" in call_point: + for child in call_point["children"]: + _compute_markers(markers, child, depth+1); + +def _find_child(children, name): + for child in children: + if child['functionName'] == name: + return child + return None + +def _add_entry_cpuprofiler_program(newtime, cpuprofiler): + curnode = _find_child(cpuprofiler['head']['children'], '(program)') + if cpuprofiler['lastTime'] != None: + lastTime = cpuprofiler['lastTime'] + while lastTime < newtime: + curnode['hitCount'] += 1 + cpuprofiler['samples'].append(curnode['callUID']) + cpuprofiler['timestamps'].append(int(lastTime*SECONDS_TO_NANOSECONDS)) + lastTime += SAMPLE_DELTA_IN_SECONDS + cpuprofiler['lastTime'] = lastTime + else: + cpuprofiler['lastTime'] = newtime + + +def _add_entry_cpuprofiler(stack, newtime, cpuprofiler): + index = len(stack) - 1 + marker = stack[index] + + if marker.name not in cpuprofiler['markers']: + cpuprofiler['markers'][marker.name] = cpuprofiler['id'] + cpuprofiler['callUID'] += 1 + callUID = cpuprofiler['markers'][marker.name] + + curnode = cpuprofiler['head'] + index = 0 + while index < len(stack): + newnode = _find_child(curnode['children'], stack[index].name) + if newnode == None: + newnode = {} + newnode['callUID'] = callUID + newnode['url'] = marker.url + newnode['functionName'] = stack[index].name + newnode['hitCount'] = 0 + newnode['lineNumber'] = marker.line + newnode['columnNumber'] = marker.col + newnode['scriptId'] = callUID + newnode['positionTicks'] = [] + newnode['id'] = cpuprofiler['id'] + cpuprofiler['id'] += 1 + newnode['children'] = [] + curnode['children'].append(newnode) + curnode['deoptReason'] = '' + curnode = newnode + index += 1 + + if cpuprofiler['lastTime'] == None: + cpuprofiler['lastTime'] = newtime + + if cpuprofiler['lastTime'] != None: + lastTime = cpuprofiler['lastTime'] + while lastTime < newtime: + curnode['hitCount'] += 1 + if len(curnode['positionTicks']) == 0: + ticks = {} + ticks['line'] = curnode['callUID'] + ticks['ticks'] = 0 + curnode['positionTicks'].append(ticks) + curnode['positionTicks'][0]['ticks'] += 1 + cpuprofiler['samples'].append(curnode['callUID']) + cpuprofiler['timestamps'].append(int(lastTime*1000*1000)) + lastTime += 0.0001 + cpuprofiler['lastTime'] = lastTime + +def _create_default_cpuprofiler_node(name, _id, _uid): + return {'functionName': name, + 'scriptId':'0', + 'url':'', + 'lineNumber':0, + 'columnNumber':0, + 'positionTicks':[], + 'id':_id, + 'callUID':_uid, + 'children': [], + 'hitCount': 0, + 'deoptReason':''} + +def main(): + parser = argparse.ArgumentParser(description="Converts JSON profile format to fbsystrace text output") + + parser.add_argument( + "-o", + dest = "output_file", + default = None, + help = "Output file for trace data") + parser.add_argument( + "-cpuprofiler", + dest = "output_cpuprofiler", + default = None, + help = "Output file for cpuprofiler data") + parser.add_argument( + "-map", + dest = "map_file", + default = None, + help = "Map file for symbolicating") + parser.add_argument( "file", help = "JSON trace input_file") + + args = parser.parse_args() + + markers = [] + with open(args.file, "r") as trace_file: + trace = json.load(trace_file) + for root_entry in trace["rootNodes"]: + _compute_markers(markers, root_entry, 0) + + mapcache = {} + for m in markers: + _calcurl(mapcache, m, args.map_file) + + sorted_markers = list(sorted(markers)); + + if args.output_cpuprofiler != None: + cpuprofiler = {} + cpuprofiler['startTime'] = None + cpuprofiler['endTime'] = None + cpuprofiler['lastTime'] = None + cpuprofiler['id'] = 4 + cpuprofiler['callUID'] = 4 + cpuprofiler['samples'] = [] + cpuprofiler['timestamps'] = [] + cpuprofiler['markers'] = {} + cpuprofiler['head'] = _create_default_cpuprofiler_node('(root)', 1, 1) + cpuprofiler['head']['children'].append(_create_default_cpuprofiler_node('(root)', 2, 2)) + cpuprofiler['head']['children'].append(_create_default_cpuprofiler_node('(program)', 3, 3)) + marker_stack = [] + with open(args.output_cpuprofiler, 'w') as file_out: + for marker in sorted_markers: + if len(marker_stack): + _add_entry_cpuprofiler(marker_stack, marker.timestamp, cpuprofiler) + else: + _add_entry_cpuprofiler_program(marker.timestamp, cpuprofiler) + if marker.is_end: + marker_stack.pop() + else: + marker_stack.append(marker) + cpuprofiler['startTime'] = cpuprofiler['timestamps'][0] / 1000000.0 + cpuprofiler['endTime'] = cpuprofiler['timestamps'][len(cpuprofiler['timestamps']) - 1] / 1000000.0 + json.dump(cpuprofiler, file_out, sort_keys=False, indent=4, separators=(',', ': ')) + + + if args.output_file != None: + with open(args.output_file,"w") as trace_file: + for marker in sorted_markers: + start_or_end = None + if marker.is_end: + start_or_end = "E" + else: + start_or_end = "B" + #output with timestamp at high level of precision + trace_file.write("json-0 [000] .... {0:.12f}: tracing_mark_write: {1}|0|{2}\n".format( + marker.timestamp, + start_or_end, + marker.name)) + +main() diff --git a/JSCLegacyProfiler/smap.py b/JSCLegacyProfiler/smap.py new file mode 100644 index 000000000..6c8f8e4ad --- /dev/null +++ b/JSCLegacyProfiler/smap.py @@ -0,0 +1,343 @@ + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + + +""" + adapted from https://github.com/martine/python-sourcemap into a reuasable module +""" + +""" + + Apache License + Version 2.0, January 2010 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + """ + +"""A module for parsing source maps, as output by the Closure and +CoffeeScript compilers and consumed by browsers. See + http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/ +""" + +import collections +import json +import sys +import bisect + +class entry(object): + def __init__(self, dst_line, dst_col, src, src_line, src_col): + self.dst_line = dst_line + self.dst_col = dst_col + self.src = src + self.src_line = src_line + self.src_col = src_col + + def __cmp__(self, other): + #print(self) + #print(other) + if self.dst_line < other.dst_line: + return -1 + if self.dst_line > other.dst_line: + return 1 + if self.dst_col < other.dst_col: + return -1 + if self.dst_col > other.dst_col: + return 1 + return 0 + +SmapState = collections.namedtuple( + 'SmapState', ['dst_line', 'dst_col', + 'src', 'src_line', 'src_col', + 'name']) + +# Mapping of base64 letter -> integer value. +B64 = dict((c, i) for i, c in + enumerate('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + '0123456789+/')) + + +def _parse_vlq(segment): + """Parse a string of VLQ-encoded data. + + Returns: + a list of integers. + """ + + values = [] + + cur, shift = 0, 0 + for c in segment: + val = B64[c] + # Each character is 6 bits: + # 5 of value and the high bit is the continuation. + val, cont = val & 0b11111, val >> 5 + cur += val << shift + shift += 5 + + if not cont: + # The low bit of the unpacked value is the sign. + cur, sign = cur >> 1, cur & 1 + if sign: + cur = -cur + values.append(cur) + cur, shift = 0, 0 + + if cur or shift: + raise Exception('leftover cur/shift in vlq decode') + + return values + + +def _parse_smap(file): + """Given a file-like object, yield SmapState()s as they are read from it.""" + + smap = json.load(file) + sources = smap['sources'] + names = smap['names'] + mappings = smap['mappings'] + lines = mappings.split(';') + + dst_col, src_id, src_line, src_col, name_id = 0, 0, 0, 0, 0 + for dst_line, line in enumerate(lines): + segments = line.split(',') + dst_col = 0 + for segment in segments: + if not segment: + continue + parsed = _parse_vlq(segment) + dst_col += parsed[0] + + src = None + name = None + if len(parsed) > 1: + src_id += parsed[1] + src = sources[src_id] + src_line += parsed[2] + src_col += parsed[3] + + if len(parsed) > 4: + name_id += parsed[4] + name = names[name_id] + + assert dst_line >= 0 + assert dst_col >= 0 + assert src_line >= 0 + assert src_col >= 0 + + yield SmapState(dst_line, dst_col, src, src_line, src_col, name) + +def find(entries, line, col): + test = entry(line, col, '', 0, 0) + index = bisect.bisect_right(entries, test) + if index == 0: + return None + return entries[index - 1] + +def parse(file): + # Simple demo that shows files that most contribute to total size. + lookup = [] + for state in _parse_smap(file): + lookup.append(entry(state.dst_line, state.dst_col, state.src, state.src_line, state.src_col)) + + sorted_lookup = list(sorted(lookup)) + return sorted_lookup diff --git a/JSCLegacyProfiler/trace_data.py b/JSCLegacyProfiler/trace_data.py new file mode 100644 index 000000000..7cda25adf --- /dev/null +++ b/JSCLegacyProfiler/trace_data.py @@ -0,0 +1,244 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import re +import unittest + +""" +# _-----=> irqs-off +# / _----=> need-resched +# | / _---=> hardirq/softirq +# || / _--=> preempt-depth +# ||| / delay +# TASK-PID CPU# |||| TIMESTAMP FUNCTION +# | | | |||| | | + -0 [001] ...2 3269.291072: sched_switch: prev_comm=swapper/1 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=mmcqd/0 next_pid=120 next_prio=120 +""" +TRACE_LINE_PATTERN = re.compile( + r'^\s*(?P.+)-(?P\d+)\s+(?:\((?P.+)\)\s+)?\[(?P\d+)\]\s+(?:(?P\S{4})\s+)?(?P[0-9.]+):\s+(?P.+)$') + +""" +Example lines from custom app traces: +0: B|27295|providerRemove +0: E +tracing_mark_write: S|27311|NNFColdStart|1112249168 +""" +APP_TRACE_LINE_PATTERN = re.compile( + r'^(?P.+?): (?P.+)$') + +""" +Example section names: +NNFColdStart +NNFColdStart<0> +NNFColdStart +NNFColdStart +""" +DECORATED_SECTION_NAME_PATTERN = re.compile(r'^(?P.*?)(?:<0>)?(?:<(?P.)(?P.*?)>)?$') + +SYSTRACE_LINE_TYPES = set(['0', 'tracing_mark_write']) + +class TraceLine(object): + def __init__(self, task, pid, tgid, cpu, flags, timestamp, function): + self.task = task + self.pid = pid + self.tgid = tgid + self.cpu = cpu + self.flags = flags + self.timestamp = timestamp + self.function = function + self.canceled = False + + @property + def is_app_trace_line(self): + return isinstance(self.function, AppTraceFunction) + + def cancel(self): + self.canceled = True + + def __str__(self): + if self.canceled: + return "" + elif self.tgid: + return "{task:>16s}-{pid:<5d} ({tgid:5s}) [{cpu:03d}] {flags:4s} {timestamp:12f}: {function}\n".format(**vars(self)) + elif self.flags: + return "{task:>16s}-{pid:<5d} [{cpu:03d}] {flags:4s} {timestamp:12f}: {function}\n".format(**vars(self)) + else: + return "{task:>16s}-{pid:<5d} [{cpu:03d}] {timestamp:12.6f}: {function}\n".format(**vars(self)) + + +class AppTraceFunction(object): + def __init__(self, type, args): + self.type = type + self.args = args + self.operation = args[0] + + if len(args) >= 2 and args[1]: + self.pid = int(args[1]) + if len(args) >= 3: + self._section_name, self.command, self.argument = _parse_section_name(args[2]) + args[2] = self._section_name + else: + self._section_name = None + self.command = None + self.argument = None + self.cookie = None + + @property + def section_name(self): + return self._section_name + + @section_name.setter + def section_name(self, value): + self._section_name = value + self.args[2] = value + + def __str__(self): + return "{type}: {args}".format(type=self.type, args='|'.join(self.args)) + + +class AsyncTraceFunction(AppTraceFunction): + def __init__(self, type, args): + super(AsyncTraceFunction, self).__init__(type, args) + + self.cookie = int(args[3]) + + +TRACE_TYPE_MAP = { + 'S': AsyncTraceFunction, + 'T': AsyncTraceFunction, + 'F': AsyncTraceFunction, +} + +def parse_line(line): + match = TRACE_LINE_PATTERN.match(line.strip()) + if not match: + return None + + task = match.group("task") + pid = int(match.group("pid")) + tgid = match.group("tgid") + cpu = int(match.group("cpu")) + flags = match.group("flags") + timestamp = float(match.group("timestamp")) + function = match.group("function") + + app_trace = _parse_function(function) + if app_trace: + function = app_trace + + return TraceLine(task, pid, tgid, cpu, flags, timestamp, function) + +def parse_dextr_line(line): + task = line["name"] + pid = line["pid"] + tgid = line["tid"] + cpu = None + flags = None + timestamp = line["ts"] + function = AppTraceFunction("DextrTrace", [line["ph"], line["pid"], line["name"]]) + + return TraceLine(task, pid, tgid, cpu, flags, timestamp, function) + + +def _parse_function(function): + line_match = APP_TRACE_LINE_PATTERN.match(function) + if not line_match: + return None + + type = line_match.group("type") + if not type in SYSTRACE_LINE_TYPES: + return None + + args = line_match.group("args").split('|') + if len(args) == 1 and len(args[0]) == 0: + args = None + + constructor = TRACE_TYPE_MAP.get(args[0], AppTraceFunction) + return constructor(type, args) + + +def _parse_section_name(section_name): + if section_name is None: + return section_name, None, None + + section_name_match = DECORATED_SECTION_NAME_PATTERN.match(section_name) + section_name = section_name_match.group("section_name") + command = section_name_match.group("command") + argument = section_name_match.group("argument") + return section_name, command, argument + + +def _format_section_name(section_name, command, argument): + if not command: + return section_name + + return "{section_name}<{command}{argument}>".format(**vars()) + + +class RoundTripFormattingTests(unittest.TestCase): + def testPlainSectionName(self): + section_name = "SectionName12345-5562342fas" + + self.assertEqual(section_name, _format_section_name(*_parse_section_name(section_name))) + + def testDecoratedSectionName(self): + section_name = "SectionName12345-5562342fas" + + self.assertEqual(section_name, _format_section_name(*_parse_section_name(section_name))) + + def testSimpleFunction(self): + function = "0: E" + + self.assertEqual(function, str(_parse_function(function))) + + def testFunctionWithoutCookie(self): + function = "0: B|27295|providerRemove" + + self.assertEqual(function, str(_parse_function(function))) + + def testFunctionWithCookie(self): + function = "0: S|27311|NNFColdStart|1112249168" + + self.assertEqual(function, str(_parse_function(function))) + + def testFunctionWithCookieAndArgs(self): + function = "0: T|27311|NNFColdStart|1122|Start" + + self.assertEqual(function, str(_parse_function(function))) + + def testFunctionWithArgsButNoPid(self): + function = "0: E|||foo=bar" + + self.assertEqual(function, str(_parse_function(function))) + + def testKitKatFunction(self): + function = "tracing_mark_write: B|14127|Looper.dispatchMessage|arg=>>>>> Dispatching to Handler (android.os.Handler) {422ae980} null: 0|Java" + + self.assertEqual(function, str(_parse_function(function))) + + def testNonSysTraceFunctionIgnored(self): + function = "sched_switch: prev_comm=swapper/1 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=mmcqd/0 next_pid=120 next_prio=120" + + self.assertEqual(None, _parse_function(function)) + + def testLineWithFlagsAndTGID(self): + line = " -0 ( 550) [000] d..2 7953.258473: cpu_idle: state=1 cpu_id=0\n" + + self.assertEqual(line, str(parse_line(line))) + + def testLineWithFlagsAndNoTGID(self): + line = " -0 (-----) [000] d..2 7953.258473: cpu_idle: state=1 cpu_id=0\n" + + self.assertEqual(line, str(parse_line(line))) + + def testLineWithFlags(self): + line = " -0 [001] ...2 3269.291072: sched_switch: prev_comm=swapper/1 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=mmcqd/0 next_pid=120 next_prio=120\n" + + self.assertEqual(line, str(parse_line(line))) + + def testLineWithoutFlags(self): + line = " -0 [001] 3269.291072: sched_switch: prev_comm=swapper/1 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=mmcqd/0 next_pid=120 next_prio=120\n" + + self.assertEqual(line, str(parse_line(line))) diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 482e213fc..19628f265 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -125,7 +125,7 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); [weakSelf stopLoadingWithError:error]; }); } - + sourceCode = source; dispatch_group_leave(initModulesAndLoadSource); }]; @@ -348,20 +348,20 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); - (void)stopLoadingWithError:(NSError *)error { RCTAssertMainThread(); - + if (!self.isValid || !self.loading) { return; } - + _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 @@ -853,30 +853,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ NSString *log = RCTProfileEnd(self); - NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", self.bundleURL.scheme, self.bundleURL.host, self.bundleURL.port]; - NSURL *URL = [NSURL URLWithString:URLString]; - NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL]; - URLRequest.HTTPMethod = @"POST"; - [URLRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - NSURLSessionTask *task = - [[NSURLSession sharedSession] uploadTaskWithRequest:URLRequest - fromData:[log dataUsingEncoding:NSUTF8StringEncoding] - completionHandler: - ^(__unused NSData *data, __unused NSURLResponse *response, NSError *error) { - if (error) { - RCTLogError(@"%@", error.localizedDescription); - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - [[[UIAlertView alloc] initWithTitle:@"Profile" - message:@"The profile has been generated, check the dev server log for instructions." - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil] show]; - }); - } - }]; - - [task resume]; + NSData *logData = [log dataUsingEncoding:NSUTF8StringEncoding]; + RCTProfileSendResult(self, @"systrace", logData); }]; } diff --git a/React/Base/RCTProfile.h b/React/Base/RCTProfile.h index 794f4a910..eb8045fb6 100644 --- a/React/Base/RCTProfile.h +++ b/React/Base/RCTProfile.h @@ -119,6 +119,12 @@ RCT_EXTERN void RCTProfileHookModules(RCTBridge *); */ 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); + #else #define RCTProfileBeginFlowEvent() @@ -144,4 +150,6 @@ RCT_EXTERN void RCTProfileUnhookModules(RCTBridge *); #define RCTProfileHookModules(...) #define RCTProfileUnhookModules(...) +#define RCTProfileSendResult(...) + #endif diff --git a/React/Base/RCTProfile.m b/React/Base/RCTProfile.m index 86aab5652..f905c0c8c 100644 --- a/React/Base/RCTProfile.m +++ b/React/Base/RCTProfile.m @@ -18,6 +18,7 @@ #import "RCTAssert.h" #import "RCTBridge.h" #import "RCTDefines.h" +#import "RCTLog.h" #import "RCTModuleData.h" #import "RCTUtils.h" @@ -402,4 +403,44 @@ void _RCTProfileEndFlowEvent(NSNumber *flowID) ); } +void RCTProfileSendResult(RCTBridge *bridge, NSString *route, NSData *data) +{ + if (![bridge.bundleURL.scheme hasPrefix:@"http"]) { + RCTLogError(@"Cannot update profile information"); + return; + } + + NSURL *URL = [NSURL URLWithString:[@"/" stringByAppendingString:route] relativeToURL:bridge.bundleURL]; + + NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL]; + URLRequest.HTTPMethod = @"POST"; + [URLRequest setValue:@"application/json" + forHTTPHeaderField:@"Content-Type"]; + + NSURLSessionTask *task = + [[NSURLSession sharedSession] uploadTaskWithRequest:URLRequest + fromData:data + completionHandler: + ^(NSData *responseData, __unused NSURLResponse *response, NSError *error) { + if (error) { + RCTLogError(@"%@", error.localizedDescription); + } else { + NSString *message = [[NSString alloc] initWithData:responseData + encoding:NSUTF8StringEncoding]; + + if (message.length) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[[UIAlertView alloc] initWithTitle:@"Profile" + message:message + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil] show]; + }); + } + } + }]; + + [task resume]; +} + #endif diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 60cdd3cd1..44be17af2 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -229,7 +229,11 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) if (isProfiling) { NSString *outputFile = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cpu_profile.json"]; nativeProfilerEnd(context, "profile", outputFile.UTF8String); - RCTLogInfo(@"CPU profile outputed to '%@'", outputFile); + NSData *profileData = [NSData dataWithContentsOfFile:outputFile + options:NSDataReadingMappedIfSafe + error:NULL]; + + RCTProfileSendResult(bridge, @"cpu-profile", profileData); } else { nativeProfilerStart(context, "profile"); } diff --git a/packager/packager.js b/packager/packager.js index 2a95dfc91..a543aba02 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -224,7 +224,7 @@ function statusPageMiddleware(req, res, next) { } function systraceProfileMiddleware(req, res, next) { - if (req.url !== '/profile') { + if (req.url !== '/systrace') { next(); return; } @@ -237,33 +237,54 @@ function systraceProfileMiddleware(req, res, next) { childProcess.exec(cmd, function(error) { if (error) { if (error.code === 127) { - console.error( + res.end( '\n** Failed executing `' + cmd + '` **\n\n' + - 'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' + - 'You can get it at:\n\n' + - ' https://github.com/google/trace-viewer\n\n' + - 'If it\'s not in your path, you can set a custom path with:\n\n' + - ' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' + - 'NOTE: Your profile data was kept at:\n\n' + - ' ' + dumpName + 'Google trace-viewer is required to visualize the data, You can install it with `brew install trace2html`\n\n' + + 'NOTE: Your profile data was kept at:\n' + dumpName ); } else { - console.error('Unknown error', error); + console.error(error); + res.end('Unknown error %s', error.message); } - res.end(); return; } else { childProcess.exec('rm ' + dumpName); childProcess.exec('open ' + dumpName.replace(/json$/, 'html'), function(err) { if (err) { console.error(err); + res.end(err.message); + } else { + res.end(); } - res.end(); }); } }); } +function cpuProfileMiddleware(req, res, next) { + if (req.url !== '/cpu-profile') { + next(); + return; + } + + console.log('Dumping CPU profile information...'); + const dumpName = '/tmp/cpu-profile_' + Date.now(); + fs.writeFileSync(dumpName + '.json', req.rawBody); + + const cmd = path.join(__dirname, '..', 'JSCLegacyProfiler', 'json2trace') + ' -cpuprofiler ' + dumpName + '.cpuprofile ' + dumpName + '.json'; + childProcess.exec(cmd, function(error) { + if (error) { + console.error(error); + res.end('Unknown error: %s', error.message); + } else { + res.end( + 'Your profile was generated at\n\n' + dumpName + '.cpuprofile\n\n' + + 'Open `Chrome Dev Tools > Profiles > Load` and select the profile to visualize it.' + ); + } + }); +} + function getAppMiddleware(options) { var transformerPath = options.transformer; if (!isAbsolutePath(transformerPath)) { @@ -297,6 +318,7 @@ function runServer( .use(getDevToolsLauncher(options)) .use(statusPageMiddleware) .use(systraceProfileMiddleware) + .use(cpuProfileMiddleware) // Temporarily disable flow check until it's more stable //.use(getFlowTypeCheckMiddleware(options)) .use(getAppMiddleware(options)); From 789a07c5a45b82229f13d09e5a8d9d541dd942ba Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Fri, 11 Sep 2015 07:11:23 -0700 Subject: [PATCH 0070/2013] Added code to sync __DEV__ value to RCT_DEBUG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: @​public When using bundled JS the `__DEV__` flag is set to false by default, which can cause errors to be missed if used for testing. This diff adds logic to override the `__DEV__` value when running in RCT_DEBUG configuration, so that the JS debug checks are enabled. Reviewed By: @tadeuzagallo Differential Revision: D2429533 --- React/Base/RCTBatchedBridge.m | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 19628f265..8f5de7330 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -186,6 +186,23 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); RCTSourceLoadBlock onSourceLoad = ^(NSError *error, NSString *source) { RCTProfileEndAsyncEvent(0, @"init,download", cookie, @"JavaScript download", nil); RCTPerformanceLoggerEnd(RCTPLScriptDownload); + + // Only override the value of __DEV__ if running in debug mode, and if we + // haven't explicitly overridden the packager dev setting in the bundleURL + BOOL shouldOverrideDev = RCT_DEBUG && ([self.bundleURL isFileURL] || + [self.bundleURL.absoluteString rangeOfString:@"dev="].location == NSNotFound); + + // Force JS __DEV__ value to match RCT_DEBUG + if (shouldOverrideDev) { + NSRange range = [source rangeOfString:@"__DEV__="]; + RCTAssert(range.location != NSNotFound, @"It looks like the implementation" + "of __DEV__ has changed. Update -[RCTBatchedBridge loadSource:]."); + NSRange valueRange = {range.location + range.length, 2}; + if ([[source substringWithRange:valueRange] isEqualToString:@"!1"]) { + source = [source stringByReplacingCharactersInRange:valueRange withString:@" 1"]; + } + } + _onSourceLoad(error, source); }; From d7ee28e0796efc7f62e9a4c2af93301bb04f779a Mon Sep 17 00:00:00 2001 From: Dorota Kapturkiewicz Date: Fri, 11 Sep 2015 07:47:34 -0700 Subject: [PATCH 0071/2013] fix ToastAndroid Differential Revision: D2433671 committer: Service User --- .../Components/ToastAndroid/ToastAndroid.ios.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Libraries/Components/ToastAndroid/ToastAndroid.ios.js b/Libraries/Components/ToastAndroid/ToastAndroid.ios.js index 249767a91..bf42aae64 100644 --- a/Libraries/Components/ToastAndroid/ToastAndroid.ios.js +++ b/Libraries/Components/ToastAndroid/ToastAndroid.ios.js @@ -10,4 +10,17 @@ */ 'use strict'; -module.exports = require('UnimplementedView'); +var warning = require('warning'); + +var ToastAndroid = { + + show: function ( + message: string, + duration: number + ): void { + warning(false, 'Cannot use ToastAndroid on iOS.'); + }, + +}; + +module.exports = ToastAndroid; From d5bce33f696606d639a7630403393de496760f63 Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Fri, 11 Sep 2015 13:26:28 -0700 Subject: [PATCH 0072/2013] Fix server tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: @​public Invoking an extra promise caused failures in the promise-based tests. This fixes them. Reviewed By: @vjeux Differential Revision: D2432431 --- .../src/Server/__tests__/Server-test.js | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/packager/react-packager/src/Server/__tests__/Server-test.js b/packager/react-packager/src/Server/__tests__/Server-test.js index af7542c7a..7d0c20a69 100644 --- a/packager/react-packager/src/Server/__tests__/Server-test.js +++ b/packager/react-packager/src/Server/__tests__/Server-test.js @@ -155,7 +155,7 @@ describe('processRequest', () => { }); }); - pit('rebuilds the bundles that contain a file when that file is changed', () => { + it('rebuilds the bundles that contain a file when that file is changed', () => { const bundleFunc = jest.genMockFunction(); bundleFunc .mockReturnValueOnce( @@ -178,21 +178,25 @@ describe('processRequest', () => { requestHandler = server.processRequest.bind(server); - return makeRequest(requestHandler, 'mybundle.bundle?runModule=true') - .then(response => { + makeRequest(requestHandler, 'mybundle.bundle?runModule=true') + .done(response => { expect(response).toEqual('this is the first source'); expect(bundleFunc.mock.calls.length).toBe(1); - triggerFileChange('all','path/file.js', options.projectRoots[0]); - jest.runAllTimers(); - jest.runAllTimers(); - }) - .then(() => { - expect(bundleFunc.mock.calls.length).toBe(2); - return makeRequest(requestHandler, 'mybundle.bundle?runModule=true') - .then(response => - expect(response).toEqual('this is the rebuilt source') - ); }); + + jest.runAllTicks(); + + triggerFileChange('all','path/file.js', options.projectRoots[0]); + jest.runAllTimers(); + jest.runAllTicks(); + + expect(bundleFunc.mock.calls.length).toBe(2); + + makeRequest(requestHandler, 'mybundle.bundle?runModule=true') + .done(response => + expect(response).toEqual('this is the rebuilt source') + ); + jest.runAllTicks(); }); }); @@ -259,30 +263,33 @@ describe('processRequest', () => { }); describe('buildbundle(options)', () => { - it('Calls the bundler with the correct args', () => { - server.buildBundle({ + pit('Calls the bundler with the correct args', () => { + return server.buildBundle({ entryFile: 'foo file' - }); - expect(Bundler.prototype.bundle).toBeCalledWith( - 'foo file', - true, - undefined, - true, - undefined + }).then(() => + expect(Bundler.prototype.bundle).toBeCalledWith( + 'foo file', + true, + undefined, + true, + undefined + ) ); }); }); describe('buildBundleFromUrl(options)', () => { - it('Calls the bundler with the correct args', () => { - server.buildBundleFromUrl('/path/to/foo.bundle?dev=false&runModule=false'); - expect(Bundler.prototype.bundle).toBeCalledWith( - 'path/to/foo.js', - false, - '/path/to/foo.map?dev=false&runModule=false', - false, - undefined - ); + pit('Calls the bundler with the correct args', () => { + return server.buildBundleFromUrl('/path/to/foo.bundle?dev=false&runModule=false') + .then(() => + expect(Bundler.prototype.bundle).toBeCalledWith( + 'path/to/foo.js', + false, + '/path/to/foo.map?dev=false&runModule=false', + false, + undefined + ) + ); }); }); }); From 169da2a1b7cedb0ab22a0f2c921c46cd0a86a3a3 Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Fri, 11 Sep 2015 14:05:24 -0700 Subject: [PATCH 0073/2013] Add simple ScrollView example to UI Explorer Reviewed By: @foghina Differential Revision: D2434588 --- .../UIExplorer/ScrollViewSimpleExample.js | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 Examples/UIExplorer/ScrollViewSimpleExample.js diff --git a/Examples/UIExplorer/ScrollViewSimpleExample.js b/Examples/UIExplorer/ScrollViewSimpleExample.js new file mode 100644 index 000000000..79673e6d0 --- /dev/null +++ b/Examples/UIExplorer/ScrollViewSimpleExample.js @@ -0,0 +1,82 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + ScrollView, + StyleSheet, + Text, + TouchableOpacity +} = React; + +var NUM_ITEMS = 20; + +var ScrollViewSimpleExample = React.createClass({ + statics: { + title: '', + description: 'Component that enables scrolling through child components.' + }, + makeItems: function(nItems, styles) { + var items = []; + for (var i = 0; i < nItems; i++) { + items[i] = ( + + {'Item ' + i} + + ); + } + return items; + }, + + render: function() { + // One of the items is a horizontal scroll view + var items = this.makeItems(NUM_ITEMS, styles.itemWrapper); + items[4] = ( + + {this.makeItems(NUM_ITEMS, [styles.itemWrapper, styles.horizontalItemWrapper])} + + ); + + var verticalScrollView = ( + + {items} + + ); + + return verticalScrollView; + } +}); + +var styles = StyleSheet.create({ + verticalScrollView: { + margin: 10, + }, + itemWrapper: { + backgroundColor: '#dddddd', + alignItems: 'center', + borderRadius: 5, + borderWidth: 5, + borderColor: '#a52a2a', + padding: 30, + margin: 5, + }, + horizontalItemWrapper: { + padding: 50 + } +}); + +module.exports = ScrollViewSimpleExample; From e9e3cd304b8c4f8ff35578ce58e8279e65b26eeb Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Fri, 11 Sep 2015 16:59:28 -0700 Subject: [PATCH 0074/2013] Revert internal files that didn't get pulled internally first See all the packager files in this commit: https://github.com/facebook/react-native/commit/f83675d1915b5e77638c117c82f8641cea367ffe#diff-7b5603771e245e5b0cd7223277db3db4 cc @foghina --- .../react-packager/src/Activity/__tests__/Activity-test.js | 3 --- .../DependencyGraph/__tests__/DependencyGraph-test.js | 3 +-- packager/react-packager/src/Server/__tests__/Server-test.js | 3 +-- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packager/react-packager/src/Activity/__tests__/Activity-test.js b/packager/react-packager/src/Activity/__tests__/Activity-test.js index 08254f792..90ac43e85 100644 --- a/packager/react-packager/src/Activity/__tests__/Activity-test.js +++ b/packager/react-packager/src/Activity/__tests__/Activity-test.js @@ -9,9 +9,6 @@ 'use strict'; jest.autoMockOff(); -jest.setMock('chalk', { - dim: function(s) { return s; }, -}); describe('Activity', () => { const origConsoleLog = console.log; diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js index 092a8c05b..6dc2bc7ee 100644 --- a/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js @@ -23,8 +23,7 @@ jest .dontMock('../../AssetModule') .dontMock('../../Module') .dontMock('../../Package') - .dontMock('../../ModuleCache') - .setMock('chalk', { dim: function(s) { return s; } }); + .dontMock('../../ModuleCache'); const Promise = require('promise'); diff --git a/packager/react-packager/src/Server/__tests__/Server-test.js b/packager/react-packager/src/Server/__tests__/Server-test.js index 7d0c20a69..be4feebfd 100644 --- a/packager/react-packager/src/Server/__tests__/Server-test.js +++ b/packager/react-packager/src/Server/__tests__/Server-test.js @@ -14,8 +14,7 @@ jest.setMock('worker-farm', function() { return () => {}; }) .dontMock('url') .setMock('timers', { setImmediate: (fn) => setTimeout(fn, 0) }) .setMock('uglify-js') - .dontMock('../') - .setMock('chalk', { dim: function(s) { return s; } }); + .dontMock('../'); const Promise = require('promise'); From 1bf78873229f33214d23854f25da1d31362ec07a Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Fri, 11 Sep 2015 14:36:12 -0700 Subject: [PATCH 0075/2013] Refactor DependencyResolver into request/response Reviewed By: @martinbigio Differential Revision: D2425842 --- .../src/Bundler/__tests__/Bundler-test.js | 2 +- packager/react-packager/src/Bundler/index.js | 23 +- .../BundlesLayoutIntegration-test.js | 21 +- .../DependencyGraph/DeprecatedAssetMap.js | 100 +++ .../DependencyGraph/HasteMap.js | 119 ++++ .../DependencyGraph/Helpers.js | 49 ++ .../DependencyGraph/ResolutionRequest.js | 347 ++++++++++ .../DependencyGraph/ResolutionResponse.js | 73 +++ .../__tests__/DependencyGraph-test.js | 76 ++- .../DependencyGraph/index.js | 593 ++---------------- .../__tests__/HasteDependencyResolver-test.js | 73 ++- .../src/DependencyResolver/fastfs.js | 34 +- .../src/DependencyResolver/index.js | 109 ++-- 13 files changed, 946 insertions(+), 673 deletions(-) create mode 100644 packager/react-packager/src/DependencyResolver/DependencyGraph/DeprecatedAssetMap.js create mode 100644 packager/react-packager/src/DependencyResolver/DependencyGraph/HasteMap.js create mode 100644 packager/react-packager/src/DependencyResolver/DependencyGraph/Helpers.js create mode 100644 packager/react-packager/src/DependencyResolver/DependencyGraph/ResolutionRequest.js create mode 100644 packager/react-packager/src/DependencyResolver/DependencyGraph/ResolutionResponse.js diff --git a/packager/react-packager/src/Bundler/__tests__/Bundler-test.js b/packager/react-packager/src/Bundler/__tests__/Bundler-test.js index f70eb4b58..60e21c072 100644 --- a/packager/react-packager/src/Bundler/__tests__/Bundler-test.js +++ b/packager/react-packager/src/Bundler/__tests__/Bundler-test.js @@ -125,7 +125,7 @@ describe('Bundler', function() { }); }); - wrapModule.mockImpl(function(module, code) { + wrapModule.mockImpl(function(response, module, code) { return Promise.resolve('lol ' + code + ' lol'); }); diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js index 5f2ead1cf..6927e90e8 100644 --- a/packager/react-packager/src/Bundler/index.js +++ b/packager/react-packager/src/Bundler/index.js @@ -137,7 +137,7 @@ class Bundler { const findEventId = Activity.startEvent('find dependencies'); let transformEventId; - return this.getDependencies(main, isDev, platform).then((result) => { + return this.getDependencies(main, isDev, platform).then((response) => { Activity.endEvent(findEventId); transformEventId = Activity.startEvent('transform'); @@ -147,14 +147,19 @@ class Bundler { complete: '=', incomplete: ' ', width: 40, - total: result.dependencies.length, + total: response.dependencies.length, }); } - bundle.setMainModuleId(result.mainModuleId); + bundle.setMainModuleId(response.mainModuleId); return Promise.all( - result.dependencies.map( - module => this._transformModule(bundle, module, platform).then(transformed => { + response.dependencies.map( + module => this._transformModule( + bundle, + response, + module, + platform + ).then(transformed => { if (bar) { bar.tick(); } @@ -182,7 +187,7 @@ class Bundler { return this._resolver.getDependencies(main, { dev: isDev, platform }); } - _transformModule(bundle, module, platform = null) { + _transformModule(bundle, response, module, platform = null) { let transform; if (module.isAsset_DEPRECATED()) { @@ -199,7 +204,11 @@ class Bundler { const resolver = this._resolver; return transform.then( - transformed => resolver.wrapModule(module, transformed.code).then( + transformed => resolver.wrapModule( + response, + module, + transformed.code + ).then( code => new ModuleTransport({ code: code, map: transformed.map, diff --git a/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js index a54ee2164..7fea8f8f7 100644 --- a/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js +++ b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js @@ -9,25 +9,8 @@ 'use strict'; jest - .dontMock('absolute-path') - .dontMock('crypto') - .dontMock('underscore') - .dontMock('path') - .dontMock('../index') - .dontMock('../../lib/getAssetDataFromName') - .dontMock('../../DependencyResolver/crawlers') - .dontMock('../../DependencyResolver/crawlers/node') - .dontMock('../../DependencyResolver/DependencyGraph/docblock') - .dontMock('../../DependencyResolver/fastfs') - .dontMock('../../DependencyResolver/replacePatterns') - .dontMock('../../DependencyResolver') - .dontMock('../../DependencyResolver/DependencyGraph') - .dontMock('../../DependencyResolver/AssetModule_DEPRECATED') - .dontMock('../../DependencyResolver/AssetModule') - .dontMock('../../DependencyResolver/Module') - .dontMock('../../DependencyResolver/Package') - .dontMock('../../DependencyResolver/Polyfill') - .dontMock('../../DependencyResolver/ModuleCache'); + .autoMockOff() + .mock('../../Cache'); const Promise = require('promise'); const path = require('path'); diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/DeprecatedAssetMap.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/DeprecatedAssetMap.js new file mode 100644 index 000000000..d4900b809 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/DeprecatedAssetMap.js @@ -0,0 +1,100 @@ + /** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const Activity = require('../../Activity'); +const AssetModule_DEPRECATED = require('../AssetModule_DEPRECATED'); +const Fastfs = require('../fastfs'); +const debug = require('debug')('ReactPackager:DependencyGraph'); +const path = require('path'); + +class DeprecatedAssetMap { + constructor({ fsCrawl, roots, assetExts, fileWatcher, ignoreFilePath, helpers }) { + if (roots == null || roots.length === 0) { + this._disabled = true; + return; + } + + this._helpers = helpers; + this._map = Object.create(null); + this._assetExts = assetExts; + this._fastfs = new Fastfs( + 'Assets', + roots, + fileWatcher, + { ignore: ignoreFilePath, crawling: fsCrawl } + ); + + this._fastfs.on('change', this._processFileChange.bind(this)); + } + + build() { + if (this._disabled) { + return Promise.resolve(); + } + + return this._fastfs.build().then( + () => { + const processAsset_DEPRECATEDActivity = Activity.startEvent( + 'Building (deprecated) Asset Map', + ); + + this._fastfs.findFilesByExts(this._assetExts).forEach( + file => this._processAsset(file) + ); + + Activity.endEvent(processAsset_DEPRECATEDActivity); + } + ); + } + + resolve(fromModule, toModuleName) { + if (this._disabled) { + return null; + } + + const assetMatch = toModuleName.match(/^image!(.+)/); + if (assetMatch && assetMatch[1]) { + if (!this._map[assetMatch[1]]) { + debug('WARINING: Cannot find asset:', assetMatch[1]); + return null; + } + return this._map[assetMatch[1]]; + } + } + + _processAsset(file) { + let ext = this._helpers.extname(file); + if (this._assetExts.indexOf(ext) !== -1) { + let name = assetName(file, ext); + if (this._map[name] != null) { + debug('Conflcting assets', name); + } + + this._map[name] = new AssetModule_DEPRECATED(file); + } + } + + _processFileChange(type, filePath, root, fstat) { + const name = assetName(filePath); + if (type === 'change' || type === 'delete') { + delete this._map[name]; + } + + if (type === 'change' || type === 'add') { + this._processAsset(path.join(root, filePath)); + } + } +} + +function assetName(file, ext) { + return path.basename(file, '.' + ext).replace(/@[\d\.]+x/, ''); +} + +module.exports = DeprecatedAssetMap; diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/HasteMap.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/HasteMap.js new file mode 100644 index 000000000..576ea3e03 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/HasteMap.js @@ -0,0 +1,119 @@ + /** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const path = require('path'); +const getPontentialPlatformExt = require('../../lib/getPlatformExtension'); + +class HasteMap { + constructor({ fastfs, moduleCache, helpers }) { + this._fastfs = fastfs; + this._moduleCache = moduleCache; + this._helpers = helpers; + this._map = Object.create(null); + } + + build() { + let promises = this._fastfs.findFilesByExt('js', { + ignore: (file) => this._helpers.isNodeModulesDir(file) + }).map(file => this._processHasteModule(file)); + + promises = promises.concat( + this._fastfs.findFilesByName('package.json', { + ignore: (file) => this._helpers.isNodeModulesDir(file) + }).map(file => this._processHastePackage(file)) + ); + + return Promise.all(promises); + } + + processFileChange(type, absPath) { + return Promise.resolve().then(() => { + /*eslint no-labels: 0 */ + if (type === 'delete' || type === 'change') { + loop: for (let name in this._map) { + let modules = this._map[name]; + for (var i = 0; i < modules.length; i++) { + if (modules[i].path === absPath) { + modules.splice(i, 1); + break loop; + } + } + } + + if (type === 'delete') { + return; + } + } + + if (this._helpers.extname(absPath) === 'js' || + this._helpers.extname(absPath) === 'json') { + if (path.basename(absPath) === 'package.json') { + return this._processHastePackage(absPath); + } else { + return this._processHasteModule(absPath); + } + } + }); + } + + getModule(name, platform = null) { + if (this._map[name]) { + const modules = this._map[name]; + if (platform != null) { + for (let i = 0; i < modules.length; i++) { + if (getPontentialPlatformExt(modules[i].path) === platform) { + return modules[i]; + } + } + } + + return modules[0]; + } + return null; + } + + _processHasteModule(file) { + const module = this._moduleCache.getModule(file); + return module.isHaste().then( + isHaste => isHaste && module.getName() + .then(name => this._updateHasteMap(name, module)) + ); + } + + _processHastePackage(file) { + file = path.resolve(file); + const p = this._moduleCache.getPackage(file, this._fastfs); + return p.isHaste() + .then(isHaste => isHaste && p.getName() + .then(name => this._updateHasteMap(name, p))) + .catch(e => { + if (e instanceof SyntaxError) { + // Malformed package.json. + return; + } + throw e; + }); + } + + _updateHasteMap(name, mod) { + if (this._map[name] == null) { + this._map[name] = []; + } + + if (mod.type === 'Module') { + // Modules takes precendence over packages. + this._map[name].unshift(mod); + } else { + this._map[name].push(mod); + } + } +} + +module.exports = HasteMap; diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/Helpers.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/Helpers.js new file mode 100644 index 000000000..fda560077 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/Helpers.js @@ -0,0 +1,49 @@ + /** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const path = require('path'); + +class Helpers { + constructor({ providesModuleNodeModules, assetExts }) { + this._providesModuleNodeModules = providesModuleNodeModules; + this._assetExts = assetExts; + } + + isNodeModulesDir(file) { + let parts = path.normalize(file).split(path.sep); + const indexOfNodeModules = parts.lastIndexOf('node_modules'); + + if (indexOfNodeModules === -1) { + return false; + } + + parts = parts.slice(indexOfNodeModules + 1); + + const dirs = this._providesModuleNodeModules; + + for (let i = 0; i < dirs.length; i++) { + if (parts.indexOf(dirs[i]) > -1) { + return false; + } + } + + return true; + } + + isAssetFile(file) { + return this._assetExts.indexOf(this.extname(file)) !== -1; + } + + extname(name) { + return path.extname(name).replace(/^\./, ''); + } +} + +module.exports = Helpers; diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/ResolutionRequest.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/ResolutionRequest.js new file mode 100644 index 000000000..4d0d0632f --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/ResolutionRequest.js @@ -0,0 +1,347 @@ + /** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const debug = require('debug')('ReactPackager:DependencyGraph'); +const util = require('util'); +const path = require('path'); +const isAbsolutePath = require('absolute-path'); +const getAssetDataFromName = require('../../lib/getAssetDataFromName'); + +class ResolutionRequest { + constructor({ + platform, + entryPath, + hasteMap, + deprecatedAssetMap, + helpers, + moduleCache, + fastfs, + }) { + this._platform = platform; + this._entryPath = entryPath; + this._hasteMap = hasteMap; + this._deprecatedAssetMap = deprecatedAssetMap; + this._helpers = helpers; + this._moduleCache = moduleCache; + this._fastfs = fastfs; + this._resetResolutionCache(); + } + + _tryResolve(action, secondaryAction) { + return action().catch((error) => { + if (error.type !== 'UnableToResolveError') { + throw error; + } + return secondaryAction(); + }); + } + + resolveDependency(fromModule, toModuleName) { + const resHash = resolutionHash(fromModule.path, toModuleName); + + if (this._immediateResolutionCache[resHash]) { + return Promise.resolve(this._immediateResolutionCache[resHash]); + } + + const asset_DEPRECATED = this._deprecatedAssetMap.resolve( + fromModule, + toModuleName + ); + if (asset_DEPRECATED) { + return Promise.resolve(asset_DEPRECATED); + } + + const cacheResult = (result) => { + this._immediateResolutionCache[resHash] = result; + return result; + }; + + const forgive = (error) => { + if (error.type !== 'UnableToResolveError') { + throw error; + } + + console.warn( + 'Unable to resolve module %s from %s', + toModuleName, + fromModule.path + ); + return null; + }; + + if (!this._helpers.isNodeModulesDir(fromModule.path) + && toModuleName[0] !== '.' && + toModuleName[0] !== '/') { + return this._tryResolve( + () => this._resolveHasteDependency(fromModule, toModuleName), + () => this._resolveNodeDependency(fromModule, toModuleName) + ).then( + cacheResult, + forgive, + ); + } + + return this._resolveNodeDependency(fromModule, toModuleName) + .then( + cacheResult, + forgive + ); + } + + getOrderedDependencies(response) { + return Promise.resolve().then(() => { + const entry = this._moduleCache.getModule(this._entryPath); + const visited = Object.create(null); + visited[entry.hash()] = true; + + const collect = (mod) => { + response.pushDependency(mod); + return mod.getDependencies().then( + depNames => Promise.all( + depNames.map(name => this.resolveDependency(mod, name)) + ).then((dependencies) => [depNames, dependencies]) + ).then(([depNames, dependencies]) => { + let p = Promise.resolve(); + + const filteredPairs = []; + + dependencies.forEach((modDep, i) => { + if (modDep == null) { + debug( + 'WARNING: Cannot find required module `%s` from module `%s`', + depNames[i], + mod.path + ); + return false; + } + return filteredPairs.push([depNames[i], modDep]); + }); + + response.setResolvedDependencyPairs(mod, filteredPairs); + + filteredPairs.forEach(([depName, modDep]) => { + p = p.then(() => { + if (!visited[modDep.hash()]) { + visited[modDep.hash()] = true; + return collect(modDep); + } + return null; + }); + }); + + return p; + }); + }; + + return collect(entry); + }); + } + + getAsyncDependencies(response) { + return Promise.resolve().then(() => { + const mod = this._moduleCache.getModule(this._entryPath); + return mod.getAsyncDependencies().then(bundles => + Promise + .all(bundles.map(bundle => + Promise.all(bundle.map( + dep => this.resolveDependency(mod, dep) + )) + )) + .then(bs => bs.map(bundle => bundle.map(dep => dep.path))) + ); + }).then(asyncDependencies => asyncDependencies.forEach( + (dependency) => response.pushAsyncDependency(dependency) + )); + } + + _resolveHasteDependency(fromModule, toModuleName) { + toModuleName = normalizePath(toModuleName); + + let p = fromModule.getPackage(); + if (p) { + p = p.redirectRequire(toModuleName); + } else { + p = Promise.resolve(toModuleName); + } + + return p.then((realModuleName) => { + let dep = this._hasteMap.getModule(realModuleName, this._platform); + if (dep && dep.type === 'Module') { + return dep; + } + + let packageName = realModuleName; + while (packageName && packageName !== '.') { + dep = this._hasteMap.getModule(packageName, this._platform); + if (dep && dep.type === 'Package') { + break; + } + packageName = path.dirname(packageName); + } + + if (dep && dep.type === 'Package') { + const potentialModulePath = path.join( + dep.root, + path.relative(packageName, realModuleName) + ); + return this._tryResolve( + () => this._loadAsFile(potentialModulePath), + () => this._loadAsDir(potentialModulePath), + ); + } + + throw new UnableToResolveError('Unable to resolve dependency'); + }); + } + + _redirectRequire(fromModule, modulePath) { + return Promise.resolve(fromModule.getPackage()).then(p => { + if (p) { + return p.redirectRequire(modulePath); + } + return modulePath; + }); + } + + _resolveNodeDependency(fromModule, toModuleName) { + if (toModuleName[0] === '.' || toModuleName[1] === '/') { + const potentialModulePath = isAbsolutePath(toModuleName) ? + toModuleName : + path.join(path.dirname(fromModule.path), toModuleName); + return this._redirectRequire(fromModule, potentialModulePath).then( + realModuleName => this._tryResolve( + () => this._loadAsFile(realModuleName), + () => this._loadAsDir(realModuleName) + ) + ); + } else { + return this._redirectRequire(fromModule, toModuleName).then( + realModuleName => { + const searchQueue = []; + for (let currDir = path.dirname(fromModule.path); + currDir !== '/'; + currDir = path.dirname(currDir)) { + searchQueue.push( + path.join(currDir, 'node_modules', realModuleName) + ); + } + + let p = Promise.reject(new UnableToResolveError('Node module not found')); + searchQueue.forEach(potentialModulePath => { + p = this._tryResolve( + () => this._tryResolve( + () => p, + () => this._loadAsFile(potentialModulePath), + ), + () => this._loadAsDir(potentialModulePath) + ); + }); + + return p; + }); + } + } + + _loadAsFile(potentialModulePath) { + return Promise.resolve().then(() => { + if (this._helpers.isAssetFile(potentialModulePath)) { + const {name, type} = getAssetDataFromName(potentialModulePath); + + let pattern = '^' + name + '(@[\\d\\.]+x)?'; + if (this._platform != null) { + pattern += '(\\.' + this._platform + ')?'; + } + pattern += '\\.' + type; + + // We arbitrarly grab the first one, because scale selection + // will happen somewhere + const [assetFile] = this._fastfs.matches( + path.dirname(potentialModulePath), + new RegExp(pattern) + ); + + if (assetFile) { + return this._moduleCache.getAssetModule(assetFile); + } + } + + let file; + if (this._fastfs.fileExists(potentialModulePath)) { + file = potentialModulePath; + } else if (this._platform != null && + this._fastfs.fileExists(potentialModulePath + '.' + this._platform + '.js')) { + file = potentialModulePath + '.' + this._platform + '.js'; + } else if (this._fastfs.fileExists(potentialModulePath + '.js')) { + file = potentialModulePath + '.js'; + } else if (this._fastfs.fileExists(potentialModulePath + '.json')) { + file = potentialModulePath + '.json'; + } else { + throw new UnableToResolveError(`File ${potentialModulePath} doesnt exist`); + } + + return this._moduleCache.getModule(file); + }); + } + + _loadAsDir(potentialDirPath) { + return Promise.resolve().then(() => { + if (!this._fastfs.dirExists(potentialDirPath)) { + throw new UnableToResolveError(`Invalid directory ${potentialDirPath}`); + } + + const packageJsonPath = path.join(potentialDirPath, 'package.json'); + if (this._fastfs.fileExists(packageJsonPath)) { + return this._moduleCache.getPackage(packageJsonPath) + .getMain().then( + (main) => this._tryResolve( + () => this._loadAsFile(main), + () => this._loadAsDir(main) + ) + ); + } + + return this._loadAsFile(path.join(potentialDirPath, 'index')); + }); + } + + _resetResolutionCache() { + this._immediateResolutionCache = Object.create(null); + } +} + + +function resolutionHash(modulePath, depName) { + return `${path.resolve(modulePath)}:${depName}`; +} + + +function UnableToResolveError() { + Error.call(this); + Error.captureStackTrace(this, this.constructor); + var msg = util.format.apply(util, arguments); + this.message = msg; + this.type = this.name = 'UnableToResolveError'; +} + +util.inherits(UnableToResolveError, Error); + + +function normalizePath(modulePath) { + if (path.sep === '/') { + modulePath = path.normalize(modulePath); + } else if (path.posix) { + modulePath = path.posix.normalize(modulePath); + } + + return modulePath.replace(/\/$/, ''); +} + + +module.exports = ResolutionRequest; diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/ResolutionResponse.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/ResolutionResponse.js new file mode 100644 index 000000000..d29ff225f --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/ResolutionResponse.js @@ -0,0 +1,73 @@ + /** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +class ResolutionResponse { + constructor() { + this.dependencies = []; + this.asyncDependencies = []; + this.mainModuleId = null; + this._mappings = Object.create(null); + this._finalized = false; + } + + _assertNotFinalized() { + if (this._finalized) { + throw new Error('Attempted to mutate finalized response.'); + } + } + + _assertFinalized() { + if (!this._finalized) { + throw new Error('Attempted to access unfinalized response.'); + } + } + + finalize() { + return this._mainModule.getName().then(id => { + this.mainModuleId = id; + this._finalized = true; + return this; + }); + } + + pushDependency(module) { + this._assertNotFinalized(); + if (this.dependencies.length === 0) { + this._mainModule = module; + } + + this.dependencies.push(module); + } + + prependDependency(module) { + this._assertNotFinalized(); + this.dependencies.unshift(module); + } + + pushAsyncDependency(dependency){ + this._assertNotFinalized(); + this.asyncDependencies.push(dependency); + } + + setResolvedDependencyPairs(module, pairs) { + this._assertNotFinalized(); + const hash = module.hash(); + if (this._mappings[hash] == null) { + this._mappings[hash] = pairs; + } + } + + getResolvedDependencyPairs(module) { + this._assertFinalized(); + return this._mappings[module.hash()]; + } +} + +module.exports = ResolutionResponse; diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js index 6dc2bc7ee..c456ba29b 100644 --- a/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js @@ -8,26 +8,13 @@ */ 'use strict'; -jest - .dontMock('../index') - .dontMock('crypto') - .dontMock('absolute-path') - .dontMock('../docblock') - .dontMock('../../crawlers') - .dontMock('../../crawlers/node') - .dontMock('../../replacePatterns') - .dontMock('../../../lib/getPlatformExtension') - .dontMock('../../../lib/getAssetDataFromName') - .dontMock('../../fastfs') - .dontMock('../../AssetModule_DEPRECATED') - .dontMock('../../AssetModule') - .dontMock('../../Module') - .dontMock('../../Package') - .dontMock('../../ModuleCache'); +jest.autoMockOff(); const Promise = require('promise'); -jest.mock('fs'); +jest + .mock('fs') + .mock('../../../Cache'); describe('DependencyGraph', function() { var cache; @@ -36,12 +23,13 @@ describe('DependencyGraph', function() { var fileWatcher; var fs; - function getOrderedDependenciesAsJSON(dgraph, entry) { - return dgraph.getOrderedDependencies(entry).then( - deps => Promise.all(deps.map(dep => Promise.all([ + function getOrderedDependenciesAsJSON(dgraph, entry, platform) { + return dgraph.getDependencies(entry, platform) + .then(response => response.finalize()) + .then(({ dependencies }) => Promise.all(dependencies.map(dep => Promise.all([ dep.getName(), dep.getDependencies(), - ]).then(([name, dependencies]) => ({ + ]).then(([name, moduleDependencies]) => ({ path: dep.path, isJSON: dep.isJSON(), isAsset: dep.isAsset(), @@ -49,7 +37,7 @@ describe('DependencyGraph', function() { isPolyfill: dep.isPolyfill(), resolution: dep.resolution, id: name, - dependencies + dependencies: moduleDependencies, }))) )); } @@ -66,10 +54,10 @@ describe('DependencyGraph', function() { isWatchman: () => Promise.resolve(false) }; - cache = new Cache({}); + cache = new Cache(); }); - describe('getOrderedDependencies', function() { + describe('get sync dependencies', function() { pit('should get dependencies', function() { var root = '/root'; fs.__setMockFilesystem({ @@ -455,9 +443,7 @@ describe('DependencyGraph', function() { cache: cache, }); - dgraph.setup({ platform: 'ios' }); - - return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js', 'ios').then(function(deps) { expect(deps) .toEqual([ { @@ -3699,4 +3685,40 @@ describe('DependencyGraph', function() { }); }); }); + + describe('getAsyncDependencies', () => { + pit('should get dependencies', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'System.import("a")' + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + ].join('\n'), + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + cache: cache, + }); + + return dgraph.getDependencies('/root/index.js') + .then(response => response.finalize()) + .then(({ asyncDependencies }) => { + expect(asyncDependencies).toEqual([ + ['/root/a.js'] + ]); + }); + }); + }); }); diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js index 0820c14a9..a03cd3dbe 100644 --- a/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js @@ -9,18 +9,20 @@ 'use strict'; const Activity = require('../../Activity'); -const AssetModule_DEPRECATED = require('../AssetModule_DEPRECATED'); const Fastfs = require('../fastfs'); const ModuleCache = require('../ModuleCache'); const Promise = require('promise'); const crawl = require('../crawlers'); -const debug = require('debug')('DependencyGraph'); const declareOpts = require('../../lib/declareOpts'); -const getAssetDataFromName = require('../../lib/getAssetDataFromName'); const getPontentialPlatformExt = require('../../lib/getPlatformExtension'); const isAbsolutePath = require('absolute-path'); const path = require('path'); const util = require('util'); +const Helpers = require('./Helpers'); +const ResolutionRequest = require('./ResolutionRequest'); +const ResolutionResponse = require('./ResolutionResponse'); +const HasteMap = require('./HasteMap'); +const DeprecatedAssetMap = require('./DeprecatedAssetMap'); const validateOpts = declareOpts({ roots: { @@ -69,9 +71,8 @@ const validateOpts = declareOpts({ class DependencyGraph { constructor(options) { this._opts = validateOpts(options); - this._hasteMap = Object.create(null); - this._resetResolutionCache(); this._cache = this._opts.cache; + this._helpers = new Helpers(this._opts); this.load(); } @@ -104,13 +105,29 @@ class DependencyGraph { this._moduleCache = new ModuleCache(this._fastfs, this._cache); + this._hasteMap = new HasteMap({ + fastfs: this._fastfs, + moduleCache: this._moduleCache, + assetExts: this._opts.exts, + helpers: this._helpers, + }); + + this._deprecatedAssetMap = new DeprecatedAssetMap({ + fsCrawl: this._crawling, + roots: this._opts.assetRoots_DEPRECATED, + helpers: this._helpers, + fileWatcher: this._opts.fileWatcher, + ignoreFilePath: this._opts.ignoreFilePath, + assetExts: this._opts.assetExts, + }); + this._loading = Promise.all([ this._fastfs.build() .then(() => { const hasteActivity = Activity.startEvent('Building Haste Map'); - return this._buildHasteMap().then(() => Activity.endEvent(hasteActivity)); + return this._hasteMap.build().then(() => Activity.endEvent(hasteActivity)); }), - this._buildAssetMap_DEPRECATED(), + this._deprecatedAssetMap.build(), ]).then(() => Activity.endEvent(depGraphActivity) ); @@ -118,536 +135,73 @@ class DependencyGraph { return this._loading; } - setup({ platform }) { - if (platform && this._opts.platforms.indexOf(platform) === -1) { + getDependencies(entryPath, platform) { + return this.load().then(() => { + platform = this._getRequestPlatform(entryPath, platform); + const absPath = this._getAbsolutePath(entryPath); + const req = new ResolutionRequest({ + platform, + entryPath: absPath, + deprecatedAssetMap: this._deprecatedAssetMap, + hasteMap: this._hasteMap, + helpers: this._helpers, + moduleCache: this._moduleCache, + fastfs: this._fastfs, + }); + + const response = new ResolutionResponse(); + + return Promise.all([ + req.getOrderedDependencies(response), + req.getAsyncDependencies(response), + ]).then(() => response); + }); + } + + _getRequestPlatform(entryPath, platform) { + if (platform == null) { + platform = getPontentialPlatformExt(entryPath); + if (platform == null || this._opts.platforms.indexOf(platform) === -1) { + platform = null; + } + } else if (this._opts.platforms.indexOf(platform) === -1) { throw new Error('Unrecognized platform: ' + platform); } - - // TODO(amasad): This is a potential race condition. Mutliple requests could - // interfere with each other. This needs a refactor to fix -- which will - // follow this diff. - if (this._platformExt !== platform) { - this._resetResolutionCache(); - } - this._platformExt = platform; - } - - resolveDependency(fromModule, toModuleName) { - const resHash = resolutionHash(fromModule.path, toModuleName); - - if (this._immediateResolutionCache[resHash]) { - return Promise.resolve(this._immediateResolutionCache[resHash]); - } - - const asset_DEPRECATED = this._resolveAsset_DEPRECATED( - fromModule, - toModuleName - ); - if (asset_DEPRECATED) { - return Promise.resolve(asset_DEPRECATED); - } - - const cacheResult = (result) => { - this._immediateResolutionCache[resHash] = result; - return result; - }; - - const forgive = () => { - console.warn( - 'Unable to resolve module %s from %s', - toModuleName, - fromModule.path - ); - return null; - }; - - if (!this._isNodeModulesDir(fromModule.path) - && toModuleName[0] !== '.' && - toModuleName[0] !== '/') { - return this._resolveHasteDependency(fromModule, toModuleName).catch( - () => this._resolveNodeDependency(fromModule, toModuleName) - ).then( - cacheResult, - forgive, - ); - } - - return this._resolveNodeDependency(fromModule, toModuleName) - .then( - cacheResult, - forgive - ); - } - - getOrderedDependencies(entryPath) { - return this.load().then(() => { - const entry = this._getModuleForEntryPath(entryPath); - const deps = []; - const visited = Object.create(null); - visited[entry.hash()] = true; - - const collect = (mod) => { - deps.push(mod); - return mod.getDependencies().then( - depNames => Promise.all( - depNames.map(name => this.resolveDependency(mod, name)) - ).then((dependencies) => [depNames, dependencies]) - ).then(([depNames, dependencies]) => { - let p = Promise.resolve(); - dependencies.forEach((modDep, i) => { - if (modDep == null) { - debug( - 'WARNING: Cannot find required module `%s` from module `%s`', - depNames[i], - mod.path - ); - return; - } - - p = p.then(() => { - if (!visited[modDep.hash()]) { - visited[modDep.hash()] = true; - return collect(modDep); - } - return null; - }); - }); - - return p; - }); - }; - - return collect(entry) - .then(() => deps); - }); - } - - getAsyncDependencies(entryPath) { - return this.load().then(() => { - const mod = this._getModuleForEntryPath(entryPath); - return mod.getAsyncDependencies().then(bundles => - Promise - .all(bundles.map(bundle => - Promise.all(bundle.map( - dep => this.resolveDependency(mod, dep) - )) - )) - .then(bs => bs.map(bundle => bundle.map(dep => dep.path))) - ); - }); + return platform; } _getAbsolutePath(filePath) { if (isAbsolutePath(filePath)) { - return filePath; + return path.resolve(filePath); } for (let i = 0; i < this._opts.roots.length; i++) { const root = this._opts.roots[i]; - const absPath = path.join(root, filePath); - if (this._fastfs.fileExists(absPath)) { - return absPath; + const potentialAbsPath = path.join(root, filePath); + if (this._fastfs.fileExists(potentialAbsPath)) { + return path.resolve(potentialAbsPath); } } - return null; - } - - _getModuleForEntryPath(entryPath) { - const absPath = this._getAbsolutePath(entryPath); - - if (absPath == null) { - throw new NotFoundError( - 'Could not find source file at %s', - entryPath - ); - } - - const absolutePath = path.resolve(absPath); - - if (absolutePath == null) { - throw new NotFoundError( - 'Cannot find entry file %s in any of the roots: %j', - entryPath, - this._opts.roots - ); - } - - // `platformExt` could be set in the `setup` method. - if (!this._platformExt) { - const platformExt = getPontentialPlatformExt(entryPath); - if (platformExt && this._opts.platforms.indexOf(platformExt) > -1) { - this._platformExt = platformExt; - } else { - this._platformExt = null; - } - } - - return this._moduleCache.getModule(absolutePath); - } - - _resolveHasteDependency(fromModule, toModuleName) { - toModuleName = normalizePath(toModuleName); - - let p = fromModule.getPackage(); - if (p) { - p = p.redirectRequire(toModuleName); - } else { - p = Promise.resolve(toModuleName); - } - - return p.then((realModuleName) => { - let dep = this._getHasteModule(realModuleName); - if (dep && dep.type === 'Module') { - return dep; - } - - let packageName = realModuleName; - while (packageName && packageName !== '.') { - dep = this._getHasteModule(packageName); - if (dep && dep.type === 'Package') { - break; - } - packageName = path.dirname(packageName); - } - - if (dep && dep.type === 'Package') { - const potentialModulePath = path.join( - dep.root, - path.relative(packageName, realModuleName) - ); - return this._loadAsFile(potentialModulePath) - .catch(() => this._loadAsDir(potentialModulePath)); - } - - throw new Error('Unable to resolve dependency'); - }); - } - - _redirectRequire(fromModule, modulePath) { - return Promise.resolve(fromModule.getPackage()).then(p => { - if (p) { - return p.redirectRequire(modulePath); - } - return modulePath; - }); - } - - _resolveNodeDependency(fromModule, toModuleName) { - if (toModuleName[0] === '.' || toModuleName[1] === '/') { - const potentialModulePath = isAbsolutePath(toModuleName) ? - toModuleName : - path.join(path.dirname(fromModule.path), toModuleName); - return this._redirectRequire(fromModule, potentialModulePath).then( - realModuleName => this._loadAsFile(realModuleName) - .catch(() => this._loadAsDir(realModuleName)) - ); - } else { - return this._redirectRequire(fromModule, toModuleName).then( - realModuleName => { - const searchQueue = []; - for (let currDir = path.dirname(fromModule.path); - currDir !== '/'; - currDir = path.dirname(currDir)) { - searchQueue.push( - path.join(currDir, 'node_modules', realModuleName) - ); - } - - let p = Promise.reject(new Error('Node module not found')); - searchQueue.forEach(potentialModulePath => { - p = p.catch( - () => this._loadAsFile(potentialModulePath) - ).catch( - () => this._loadAsDir(potentialModulePath) - ); - }); - - return p; - }); - } - } - - _resolveAsset_DEPRECATED(fromModule, toModuleName) { - if (this._assetMap_DEPRECATED != null) { - const assetMatch = toModuleName.match(/^image!(.+)/); - // Process DEPRECATED global asset requires. - if (assetMatch && assetMatch[1]) { - if (!this._assetMap_DEPRECATED[assetMatch[1]]) { - debug('WARINING: Cannot find asset:', assetMatch[1]); - return null; - } - return this._assetMap_DEPRECATED[assetMatch[1]]; - } - } - return null; - } - - _isAssetFile(file) { - return this._opts.assetExts.indexOf(extname(file)) !== -1; - } - - _loadAsFile(potentialModulePath) { - return Promise.resolve().then(() => { - if (this._isAssetFile(potentialModulePath)) { - const {name, type} = getAssetDataFromName(potentialModulePath); - - let pattern = '^' + name + '(@[\\d\\.]+x)?'; - if (this._platformExt != null) { - pattern += '(\\.' + this._platformExt + ')?'; - } - pattern += '\\.' + type; - - // We arbitrarly grab the first one, because scale selection - // will happen somewhere - const [assetFile] = this._fastfs.matches( - path.dirname(potentialModulePath), - new RegExp(pattern) - ); - - if (assetFile) { - return this._moduleCache.getAssetModule(assetFile); - } - } - - let file; - if (this._fastfs.fileExists(potentialModulePath)) { - file = potentialModulePath; - } else if (this._platformExt != null && - this._fastfs.fileExists(potentialModulePath + '.' + this._platformExt + '.js')) { - file = potentialModulePath + '.' + this._platformExt + '.js'; - } else if (this._fastfs.fileExists(potentialModulePath + '.js')) { - file = potentialModulePath + '.js'; - } else if (this._fastfs.fileExists(potentialModulePath + '.json')) { - file = potentialModulePath + '.json'; - } else { - throw new Error(`File ${potentialModulePath} doesnt exist`); - } - - return this._moduleCache.getModule(file); - }); - } - - _loadAsDir(potentialDirPath) { - return Promise.resolve().then(() => { - if (!this._fastfs.dirExists(potentialDirPath)) { - throw new Error(`Invalid directory ${potentialDirPath}`); - } - - const packageJsonPath = path.join(potentialDirPath, 'package.json'); - if (this._fastfs.fileExists(packageJsonPath)) { - return this._moduleCache.getPackage(packageJsonPath) - .getMain().then( - (main) => this._loadAsFile(main).catch( - () => this._loadAsDir(main) - ) - ); - } - - return this._loadAsFile(path.join(potentialDirPath, 'index')); - }); - } - - _buildHasteMap() { - let promises = this._fastfs.findFilesByExt('js', { - ignore: (file) => this._isNodeModulesDir(file) - }).map(file => this._processHasteModule(file)); - - promises = promises.concat( - this._fastfs.findFilesByName('package.json', { - ignore: (file) => this._isNodeModulesDir(file) - }).map(file => this._processHastePackage(file)) + throw new NotFoundError( + 'Cannot find entry file %s in any of the roots: %j', + filePath, + this._opts.roots ); - - return Promise.all(promises); - } - - _processHasteModule(file) { - const module = this._moduleCache.getModule(file); - return module.isHaste().then( - isHaste => isHaste && module.getName() - .then(name => this._updateHasteMap(name, module)) - ); - } - - _processHastePackage(file) { - file = path.resolve(file); - const p = this._moduleCache.getPackage(file, this._fastfs); - return p.isHaste() - .then(isHaste => isHaste && p.getName() - .then(name => this._updateHasteMap(name, p))) - .catch(e => { - if (e instanceof SyntaxError) { - // Malformed package.json. - return; - } - throw e; - }); - } - - _updateHasteMap(name, mod) { - if (this._hasteMap[name] == null) { - this._hasteMap[name] = []; - } - - if (mod.type === 'Module') { - // Modules takes precendence over packages. - this._hasteMap[name].unshift(mod); - } else { - this._hasteMap[name].push(mod); - } - } - - _getHasteModule(name) { - if (this._hasteMap[name]) { - const modules = this._hasteMap[name]; - if (this._platformExt != null) { - for (let i = 0; i < modules.length; i++) { - if (getPontentialPlatformExt(modules[i].path) === this._platformExt) { - return modules[i]; - } - } - } - - return modules[0]; - } - return null; - } - - _isNodeModulesDir(file) { - let parts = path.normalize(file).split(path.sep); - const indexOfNodeModules = parts.lastIndexOf('node_modules'); - - if (indexOfNodeModules === -1) { - return false; - } - - parts = parts.slice(indexOfNodeModules + 1); - - const dirs = this._opts.providesModuleNodeModules; - - for (let i = 0; i < dirs.length; i++) { - if (parts.indexOf(dirs[i]) > -1) { - return false; - } - } - - return true; - } - - _processAsset_DEPRECATED(file) { - let ext = extname(file); - if (this._opts.assetExts.indexOf(ext) !== -1) { - let name = assetName(file, ext); - if (this._assetMap_DEPRECATED[name] != null) { - debug('Conflcting assets', name); - } - - this._assetMap_DEPRECATED[name] = new AssetModule_DEPRECATED(file); - } - } - - _buildAssetMap_DEPRECATED() { - if (this._opts.assetRoots_DEPRECATED == null || - this._opts.assetRoots_DEPRECATED.length === 0) { - return Promise.resolve(); - } - - this._assetMap_DEPRECATED = Object.create(null); - - const fastfs = new Fastfs( - 'Assets', - this._opts.assetRoots_DEPRECATED, - this._opts.fileWatcher, - { ignore: this._opts.ignoreFilePath, crawling: this._crawling } - ); - - fastfs.on('change', this._processAssetChange_DEPRECATED.bind(this)); - - return fastfs.build().then( - () => { - const processAsset_DEPRECATEDActivity = Activity.startEvent( - 'Building (deprecated) Asset Map', - ); - - const assets = fastfs.findFilesByExts(this._opts.assetExts).map( - file => this._processAsset_DEPRECATED(file) - ); - - Activity.endEvent(processAsset_DEPRECATEDActivity); - return assets; - } - ); - } - - _processAssetChange_DEPRECATED(type, filePath, root, fstat) { - const name = assetName(filePath); - if (type === 'change' || type === 'delete') { - delete this._assetMap_DEPRECATED[name]; - } - - if (type === 'change' || type === 'add') { - this._loading = this._loading.then( - () => this._processAsset_DEPRECATED(path.join(root, filePath)) - ); - } } _processFileChange(type, filePath, root, fstat) { - // It's really hard to invalidate the right module resolution cache - // so we just blow it up with every file change. - this._resetResolutionCache(); - const absPath = path.join(root, filePath); - if ((fstat && fstat.isDirectory()) || + if (fstat && fstat.isDirectory() || this._opts.ignoreFilePath(absPath) || - this._isNodeModulesDir(absPath)) { + this._helpers.isNodeModulesDir(absPath)) { return; } - /*eslint no-labels: 0 */ - if (type === 'delete' || type === 'change') { - loop: for (let name in this._hasteMap) { - let modules = this._hasteMap[name]; - for (var i = 0; i < modules.length; i++) { - if (modules[i].path === absPath) { - modules.splice(i, 1); - break loop; - } - } - } - - if (type === 'delete') { - return; - } - } - - if (extname(absPath) === 'js' || extname(absPath) === 'json') { - this._loading = this._loading.then(() => { - if (path.basename(filePath) === 'package.json') { - return this._processHastePackage(absPath); - } else { - return this._processHasteModule(absPath); - } - }); - } + this._loading = this._loading.then( + () => this._hasteMap.processFileChange(type, absPath) + ); } - - _resetResolutionCache() { - this._immediateResolutionCache = Object.create(null); - } -} - -function assetName(file, ext) { - return path.basename(file, '.' + ext).replace(/@[\d\.]+x/, ''); -} - -function extname(name) { - return path.extname(name).replace(/^\./, ''); -} - -function resolutionHash(modulePath, depName) { - return `${path.resolve(modulePath)}:${depName}`; } function NotFoundError() { @@ -658,17 +212,6 @@ function NotFoundError() { this.type = this.name = 'NotFoundError'; this.status = 404; } - -function normalizePath(modulePath) { - if (path.sep === '/') { - modulePath = path.normalize(modulePath); - } else if (path.posix) { - modulePath = path.posix.normalize(modulePath); - } - - return modulePath.replace(/\/$/, ''); -} - util.inherits(NotFoundError, Error); module.exports = DependencyGraph; diff --git a/packager/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js b/packager/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js index be71b3a00..edb79cbb8 100644 --- a/packager/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js +++ b/packager/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js @@ -34,6 +34,22 @@ describe('HasteDependencyResolver', function() { HasteDependencyResolver = require('../'); }); + class ResolutionResponseMock { + constructor({dependencies, mainModuleId, asyncDependencies}) { + this.dependencies = dependencies; + this.mainModuleId = mainModuleId; + this.asyncDependencies = asyncDependencies; + } + + prependDependency(dependency) { + this.dependencies.unshift(dependency); + } + + finalize() { + return Promise.resolve(this); + } + } + function createModule(id, dependencies) { var module = new Module(); module.getName.mockImpl(() => Promise.resolve(id)); @@ -52,11 +68,12 @@ describe('HasteDependencyResolver', function() { // Is there a better way? How can I mock the prototype instead? var depGraph = depResolver._depGraph; - depGraph.getOrderedDependencies.mockImpl(function() { - return Promise.resolve(deps); - }); - depGraph.load.mockImpl(function() { - return Promise.resolve(); + depGraph.getDependencies.mockImpl(function() { + return Promise.resolve(new ResolutionResponseMock({ + dependencies: deps, + mainModuleId: 'index', + asyncDependencies: [], + })); }); return depResolver.getDependencies('/root/index.js', { dev: false }) @@ -133,19 +150,19 @@ describe('HasteDependencyResolver', function() { projectRoot: '/root', }); - // Is there a better way? How can I mock the prototype instead? var depGraph = depResolver._depGraph; - depGraph.getOrderedDependencies.mockImpl(function() { - return Promise.resolve(deps); - }); - depGraph.load.mockImpl(function() { - return Promise.resolve(); + depGraph.getDependencies.mockImpl(function() { + return Promise.resolve(new ResolutionResponseMock({ + dependencies: deps, + mainModuleId: 'index', + asyncDependencies: [], + })); }); return depResolver.getDependencies('/root/index.js', { dev: true }) .then(function(result) { expect(result.mainModuleId).toEqual('index'); - expect(depGraph.getOrderedDependencies).toBeCalledWith('/root/index.js'); + expect(depGraph.getDependencies).toBeCalledWith('/root/index.js', undefined); expect(result.dependencies[0]).toBe(Polyfill.mock.instances[0]); expect(result.dependencies[result.dependencies.length - 1]) .toBe(module); @@ -161,13 +178,13 @@ describe('HasteDependencyResolver', function() { polyfillModuleNames: ['some module'], }); - // Is there a better way? How can I mock the prototype instead? var depGraph = depResolver._depGraph; - depGraph.getOrderedDependencies.mockImpl(function() { - return Promise.resolve(deps); - }); - depGraph.load.mockImpl(function() { - return Promise.resolve(); + depGraph.getDependencies.mockImpl(function() { + return Promise.resolve(new ResolutionResponseMock({ + dependencies: deps, + mainModuleId: 'index', + asyncDependencies: [], + })); }); return depResolver.getDependencies('/root/index.js', { dev: false }) @@ -343,17 +360,23 @@ describe('HasteDependencyResolver', function() { ].join('\n'); /*eslint-disable */ - depGraph.resolveDependency.mockImpl(function(fromModule, toModuleName) { - if (toModuleName === 'x') { - return Promise.resolve(createModule('changed')); - } else if (toModuleName === 'y') { - return Promise.resolve(createModule('Y')); - } + const module = createModule('test module', ['x', 'y']); - return Promise.resolve(null); + const resolutionResponse = new ResolutionResponseMock({ + dependencies: [module], + mainModuleId: 'test module', + asyncDependencies: [], }); + resolutionResponse.getResolvedDependencyPairs = (module) => { + return [ + ['x', createModule('changed')], + ['y', createModule('Y')], + ]; + } + return depResolver.wrapModule( + resolutionResponse, createModule('test module', ['x', 'y']), code ).then(processedCode => { diff --git a/packager/react-packager/src/DependencyResolver/fastfs.js b/packager/react-packager/src/DependencyResolver/fastfs.js index a4c04232e..623df2320 100644 --- a/packager/react-packager/src/DependencyResolver/fastfs.js +++ b/packager/react-packager/src/DependencyResolver/fastfs.js @@ -13,6 +13,8 @@ const stat = Promise.denodeify(fs.stat); const hasOwn = Object.prototype.hasOwnProperty; +const NOT_FOUND_IN_ROOTS = 'NotFoundInRootsError'; + class Fastfs extends EventEmitter { constructor(name, roots, fileWatcher, {ignore, crawling}) { super(); @@ -90,7 +92,11 @@ class Fastfs extends EventEmitter { } readFile(filePath) { - return this._getFile(filePath).read(); + const file = this._getFile(filePath); + if (!file) { + throw new Error(`Unable to find file with path: ${file}`); + } + return file.read(); } closest(filePath, name) { @@ -105,12 +111,30 @@ class Fastfs extends EventEmitter { } fileExists(filePath) { - const file = this._getFile(filePath); + let file; + try { + file = this._getFile(filePath); + } catch (e) { + if (e.type === NOT_FOUND_IN_ROOTS) { + return false; + } + throw e; + } + return file && !file.isDir; } dirExists(filePath) { - const file = this._getFile(filePath); + let file; + try { + file = this._getFile(filePath); + } catch (e) { + if (e.type === NOT_FOUND_IN_ROOTS) { + return false; + } + throw e; + } + return file && file.isDir; } @@ -138,7 +162,9 @@ class Fastfs extends EventEmitter { _getAndAssertRoot(filePath) { const root = this._getRoot(filePath); if (!root) { - throw new Error(`File ${filePath} not found in any of the roots`); + const error = new Error(`File ${filePath} not found in any of the roots`); + error.type = NOT_FOUND_IN_ROOTS; + throw error; } return root; } diff --git a/packager/react-packager/src/DependencyResolver/index.js b/packager/react-packager/src/DependencyResolver/index.js index cc7f9468a..c42fd36a0 100644 --- a/packager/react-packager/src/DependencyResolver/index.js +++ b/packager/react-packager/src/DependencyResolver/index.js @@ -82,36 +82,18 @@ var getDependenciesValidateOpts = declareOpts({ HasteDependencyResolver.prototype.getDependencies = function(main, options) { var opts = getDependenciesValidateOpts(options); - var depGraph = this._depGraph; - var self = this; + return this._depGraph.getDependencies(main, opts.platform).then( + resolutionResponse => { + this._getPolyfillDependencies(opts.dev).reverse().forEach( + polyfill => resolutionResponse.prependDependency(polyfill) + ); - depGraph.setup({ platform: opts.platform }); - - return Promise.all([ - depGraph.getOrderedDependencies(main), - depGraph.getAsyncDependencies(main), - ]).then( - ([dependencies, asyncDependencies]) => dependencies[0].getName().then( - mainModuleId => { - self._prependPolyfillDependencies( - dependencies, - opts.dev, - ); - - return { - mainModuleId, - dependencies, - asyncDependencies, - }; - } - ) + return resolutionResponse.finalize(); + } ); }; -HasteDependencyResolver.prototype._prependPolyfillDependencies = function( - dependencies, - isDev -) { +HasteDependencyResolver.prototype._getPolyfillDependencies = function(isDev) { var polyfillModuleNames = [ isDev ? path.join(__dirname, 'polyfills/prelude_dev.js') @@ -124,7 +106,7 @@ HasteDependencyResolver.prototype._prependPolyfillDependencies = function( path.join(__dirname, 'polyfills/Array.prototype.es6.js'), ].concat(this._polyfillModuleNames); - var polyfillModules = polyfillModuleNames.map( + return polyfillModuleNames.map( (polyfillModuleName, idx) => new Polyfill({ path: polyfillModuleName, id: polyfillModuleName, @@ -132,50 +114,47 @@ HasteDependencyResolver.prototype._prependPolyfillDependencies = function( isPolyfill: true, }) ); - - dependencies.unshift.apply(dependencies, polyfillModules); }; -HasteDependencyResolver.prototype.wrapModule = function(module, code) { - if (module.isPolyfill()) { - return Promise.resolve(code); - } +HasteDependencyResolver.prototype.wrapModule = function(resolutionResponse, module, code) { + return Promise.resolve().then(() => { + if (module.isPolyfill()) { + return Promise.resolve(code); + } - const resolvedDeps = Object.create(null); - const resolvedDepsArr = []; + const resolvedDeps = Object.create(null); + const resolvedDepsArr = []; - return module.getDependencies().then( - dependencies => Promise.all(dependencies.map( - depName => this._depGraph.resolveDependency(module, depName) - .then(depModule => { - if (depModule) { - return depModule.getName().then(name => { - resolvedDeps[depName] = name; - resolvedDepsArr.push(name); - }); - } - }) + return Promise.all( + resolutionResponse.getResolvedDependencyPairs(module).map( + ([depName, depModule]) => { + if (depModule) { + return depModule.getName().then(name => { + resolvedDeps[depName] = name; + resolvedDepsArr.push(name); + }); + } + } ) - ) - ).then(() => { - const relativizeCode = (codeMatch, pre, quot, depName, post) => { - const depId = resolvedDeps[depName]; - if (depId) { - return pre + quot + depId + post; - } else { - return codeMatch; - } - }; + ).then(() => { + const relativizeCode = (codeMatch, pre, quot, depName, post) => { + const depId = resolvedDeps[depName]; + if (depId) { + return pre + quot + depId + post; + } else { + return codeMatch; + } + }; - return module.getName().then( - name => defineModuleCode({ - code: code - .replace(replacePatterns.IMPORT_RE, relativizeCode) - .replace(replacePatterns.REQUIRE_RE, relativizeCode), - deps: JSON.stringify(resolvedDepsArr), - moduleName: name, - }) - ); + return module.getName().then( + name => defineModuleCode({ + code: code.replace(replacePatterns.IMPORT_RE, relativizeCode) + .replace(replacePatterns.REQUIRE_RE, relativizeCode), + deps: JSON.stringify(resolvedDepsArr), + moduleName: name, + }) + ); + }); }); }; From c03dad19e7ce2e6318b373d5a768856bfe55e7fd Mon Sep 17 00:00:00 2001 From: Gabe Levi Date: Fri, 11 Sep 2015 14:51:26 -0700 Subject: [PATCH 0076/2013] Deploy 0.15.0 Reviewed By: @jeffmo Differential Revision: D2421222 --- .flowconfig | 6 +++--- Examples/SampleApp/_flowconfig | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index 65df8bc75..0fde6ca2e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -43,9 +43,9 @@ suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FixMe -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-4]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-4]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-5]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-5]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy [version] -0.14.0 +0.15.0 diff --git a/Examples/SampleApp/_flowconfig b/Examples/SampleApp/_flowconfig index 9ca5deb8f..d5256a2a3 100644 --- a/Examples/SampleApp/_flowconfig +++ b/Examples/SampleApp/_flowconfig @@ -41,9 +41,9 @@ suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FixMe -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-4]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-4]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-5]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-5]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy [version] -0.14.0 +0.15.0 From 56f77ec6ee8d4e2c4c8d2d72f4a7be7b491ab791 Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Fri, 11 Sep 2015 15:27:07 -0700 Subject: [PATCH 0077/2013] Tweak debug menu labels Reviewed By: @kmagiera Differential Revision: D2430890 --- React/Modules/RCTDevMenu.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index b99bd5554..94e2edf9c 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -286,7 +286,7 @@ RCT_EXPORT_MODULE() }]]; } else { BOOL isDebuggingInChrome = _executorClass && _executorClass == chromeExecutorClass; - NSString *debugTitleChrome = isDebuggingInChrome ? @"Disable Chrome Debugging" : @"Debug in Chrome"; + NSString *debugTitleChrome = isDebuggingInChrome ? @"Stop Chrome Debugging" : @"Debug in Chrome"; [items addObject:[[RCTDevMenuItem alloc] initWithTitle:debugTitleChrome handler:^{ self.executorClass = isDebuggingInChrome ? Nil : chromeExecutorClass; }]]; @@ -294,7 +294,7 @@ RCT_EXPORT_MODULE() Class safariExecutorClass = NSClassFromString(@"RCTWebViewExecutor"); BOOL isDebuggingInSafari = _executorClass && _executorClass == safariExecutorClass; - NSString *debugTitleSafari = isDebuggingInSafari ? @"Disable Safari Debugging" : @"Debug in Safari"; + NSString *debugTitleSafari = isDebuggingInSafari ? @"Stop Safari Debugging" : @"Debug in Safari"; [items addObject:[[RCTDevMenuItem alloc] initWithTitle:debugTitleSafari handler:^{ self.executorClass = isDebuggingInSafari ? Nil : safariExecutorClass; }]]; From f1cf322c9e54a21c5d9ac41a9a842ef16c55e4b0 Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Fri, 11 Sep 2015 15:48:35 -0700 Subject: [PATCH 0078/2013] Fix blacklist Reviewed By: @amasad Differential Revision: D2432196 --- packager/blacklist.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packager/blacklist.js b/packager/blacklist.js index b79f2899c..741b7c1ff 100644 --- a/packager/blacklist.js +++ b/packager/blacklist.js @@ -13,16 +13,21 @@ var path = require('path'); // Don't forget to everything listed here to `testConfig.json` // modulePathIgnorePatterns. var sharedBlacklist = [ - 'website', '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', ]; +// Raw unescaped patterns in case you need to use wildcards +var sharedBlacklistWildcards = [ + 'website\/node_modules\/.*', +]; + var platformBlacklists = { web: [ - '.ios.js' + '.ios.js', + '.android.js', ], ios: [ '.web.js', @@ -45,6 +50,7 @@ function blacklist(platform, additionalBlacklist) { (additionalBlacklist || []).concat(sharedBlacklist) .concat(platformBlacklists[platform] || []) .map(escapeRegExp) + .concat(sharedBlacklistWildcards) .join('|') + ')$' ); From f88fe71ee6c8aed4f906c06b97a269fd1f759115 Mon Sep 17 00:00:00 2001 From: ericvera Date: Fri, 11 Sep 2015 18:44:16 -0700 Subject: [PATCH 0079/2013] Update Testing.md with pre-processing information With the added information a react-native developer will be able to run jest tests without errors. --- docs/Testing.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/Testing.md b/docs/Testing.md index 9c07d236f..0feb816c6 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -25,6 +25,33 @@ npm test from the react-native root, and we encourage you to add your own tests for any components you want to contribute to. See [`getImageSource-test.js`](https://github.com/facebook/react-native/blob/master/Examples/Movies/__tests__/getImageSource-test.js) for a basic example. +Note: In order to run your own tests, you will have to first follow the Getting Started instructions on the Jest page and then include the `jest` objects below in `package.json` so that the scripts are pre-processed before execution. + +``` +... +"scripts": { + ... + "test": "jest" +}, +... +"jest": { + "scriptPreprocessor": "node_modules/react-native/jestSupport/scriptPreprocess.js", + "setupEnvScriptFile": "node_modules/react-native/jestSupport/env.js", + "testPathIgnorePatterns": [ + "/node_modules/", + "packager/react-packager/src/Activity/" + ], + "testFileExtensions": [ + "js" + ], + "unmockedModulePathPatterns": [ + "promise", + "source-map" + ] +}, +... +``` + Note: you may have to install/upgrade/link io.js and other parts of your environment in order for the tests to run correctly. Check out the latest setup in [.travis.yml](https://github.com/facebook/react-native/blob/master/.travis.yml#L11-24) ## Integration Tests From 627d5a8e7e33673b675a04bcd6d4b0ce34144f45 Mon Sep 17 00:00:00 2001 From: Jason Brown Date: Sat, 12 Sep 2015 09:53:01 -0700 Subject: [PATCH 0080/2013] Fix documentation by adding propTypeCompositionHandler --- website/server/extractDocs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index c52cf3b7b..38b723f21 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -226,7 +226,7 @@ var styleDocs = styles.slice(2).reduce(function(docs, filepath) { docgen.parse( fs.readFileSync(filepath), docgenHelpers.findExportedObject, - [docgen.handlers.propTypeHandler] + [docgen.handlers.propTypeHandler, docgen.handlers.propTypeCompositionHandler] ); return docs; From df288564c6e2b1b268ee9dcf3254c7a6da08f41b Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Sat, 12 Sep 2015 15:32:12 -0700 Subject: [PATCH 0081/2013] Fix Flow annotations in ScrollViewSimpleExample Reviewed By: @jingc Differential Revision: D2437502 --- Examples/UIExplorer/ScrollViewSimpleExample.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/UIExplorer/ScrollViewSimpleExample.js b/Examples/UIExplorer/ScrollViewSimpleExample.js index 79673e6d0..c9bbe7407 100644 --- a/Examples/UIExplorer/ScrollViewSimpleExample.js +++ b/Examples/UIExplorer/ScrollViewSimpleExample.js @@ -30,7 +30,7 @@ var ScrollViewSimpleExample = React.createClass({ title: '', description: 'Component that enables scrolling through child components.' }, - makeItems: function(nItems, styles) { + makeItems: function(nItems: number, styles): Array { var items = []; for (var i = 0; i < nItems; i++) { items[i] = ( From b998e5a7b74905b30b1137a02e14cd5e6f97fccc Mon Sep 17 00:00:00 2001 From: Param Aggarwal Date: Sun, 13 Sep 2015 11:08:44 -0700 Subject: [PATCH 0082/2013] Use getters and setters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: As per discussion in #2423 - possible fix for crash. (cc: @​javache) Please share feedback regarding the PR, we are going to be using this diff in production to see if it fixes the crashes we are seeing. (fixes #2423) Closes https://github.com/facebook/react-native/pull/2494 Reviewed By: @javache Differential Revision: D2433515 Pulled By: @nicklockwood --- React/Base/RCTLog.m | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m index ebbf6a1b8..2e5c2c605 100644 --- a/React/Base/RCTLog.m +++ b/React/Base/RCTLog.m @@ -35,17 +35,16 @@ const char *RCTLogLevels[] = { static RCTLogFunction RCTCurrentLogFunction; static RCTLogLevel RCTCurrentLogThreshold; -__attribute__((constructor)) -static void RCTLogSetup() +RCTLogLevel RCTGetLogThreshold() { - RCTCurrentLogFunction = RCTDefaultLogFunction; - + if (!RCTCurrentLogThreshold) { #if RCT_DEBUG - RCTCurrentLogThreshold = RCTLogLevelInfo - 1; + RCTCurrentLogThreshold = RCTLogLevelInfo - 1; #else - RCTCurrentLogThreshold = RCTLogLevelError; + RCTCurrentLogThreshold = RCTLogLevelError; #endif - + } + return RCTCurrentLogThreshold; } RCTLogFunction RCTDefaultLogFunction = ^( @@ -88,23 +87,26 @@ void RCTSetLogFunction(RCTLogFunction logFunction) RCTLogFunction RCTGetLogFunction() { + if (!RCTCurrentLogFunction) { + RCTCurrentLogFunction = RCTDefaultLogFunction; + } return RCTCurrentLogFunction; } void RCTAddLogFunction(RCTLogFunction logFunction) { - RCTLogFunction existing = RCTCurrentLogFunction; + RCTLogFunction existing = RCTGetLogFunction(); if (existing) { - RCTCurrentLogFunction = ^(RCTLogLevel level, - NSString *fileName, - NSNumber *lineNumber, - NSString *message) { + RCTSetLogFunction(^(RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message) { existing(level, fileName, lineNumber, message); logFunction(level, fileName, lineNumber, message); - }; + }); } else { - RCTCurrentLogFunction = logFunction; + RCTSetLogFunction(logFunction); } } @@ -120,7 +122,7 @@ static RCTLogFunction RCTGetLocalLogFunction() if (logFunction) { return logFunction; } - return RCTCurrentLogFunction; + return RCTGetLogFunction(); } void RCTPerformBlockWithLogFunction(void (^block)(void), RCTLogFunction logFunction) @@ -194,7 +196,7 @@ void _RCTLogFormat( { RCTLogFunction logFunction = RCTGetLocalLogFunction(); BOOL log = RCT_DEBUG || (logFunction != nil); - if (log && level >= RCTCurrentLogThreshold) { + if (log && level >= RCTGetLogThreshold()) { // Get message va_list args; From c51bfdc50c54001ab62c909ff1952e7fed13e959 Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Mon, 14 Sep 2015 13:56:39 +0100 Subject: [PATCH 0083/2013] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 751199630..718387943 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# React Native [![Build Status](https://travis-ci.org/facebook/react-native.svg?branch=master)](https://travis-ci.org/facebook/react-native) [![npm version](https://badge.fury.io/js/react-native.svg)](http://badge.fury.io/js/react-native) +# React Native [![npm version](https://badge.fury.io/js/react-native.svg)](http://badge.fury.io/js/react-native) React Native enables you to build world-class application experiences on native platforms using a consistent developer experience based on JavaScript and [React](http://facebook.github.io/react). The focus of React Native is on developer efficiency across all the platforms you care about - learn once, write anywhere. Facebook uses React Native in multiple production apps and will continue investing in React Native. From eb48759675956d319f333c9757fbddea828bddc1 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Mon, 14 Sep 2015 07:34:16 -0700 Subject: [PATCH 0084/2013] Add License headers to .js files Differential Revision: D2438967 committer: Service User --- .../ProgressBarAndroid/ProgressBarAndroid.ios.js | 7 ++++++- Libraries/Utilities/BackAndroid.ios.js | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.ios.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.ios.js index de4dbf9d8..99f01c81f 100644 --- a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.ios.js +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.ios.js @@ -1,5 +1,10 @@ /** - * Copyright 2004-present Facebook. All Rights Reserved. + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ProgressBarAndroid */ diff --git a/Libraries/Utilities/BackAndroid.ios.js b/Libraries/Utilities/BackAndroid.ios.js index c5a56f40e..e1d464ff1 100644 --- a/Libraries/Utilities/BackAndroid.ios.js +++ b/Libraries/Utilities/BackAndroid.ios.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * * iOS stub for BackAndroid.android.js * * @providesModule BackAndroid From 515d5a5f4b48ffa4f7ac5af0f2dd1b91fb980bfc Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 14 Sep 2015 09:34:33 -0700 Subject: [PATCH 0085/2013] Added toggle items to dev menu Reviewed By: @tadeuzagallo Differential Revision: D2424595 --- React/Executors/RCTContextExecutor.m | 4 +- React/Modules/RCTDevMenu.h | 36 +++- React/Modules/RCTDevMenu.m | 298 +++++++++++++++++++-------- 3 files changed, 252 insertions(+), 86 deletions(-) diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 44be17af2..267213b20 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -225,7 +225,7 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) } __block BOOL isProfiling = NO; - [bridge.devMenu addItem:@"Profile" handler:^{ + [bridge.devMenu addItem:[RCTDevMenuItem buttonItemWithTitle:@"Profile" handler:^{ if (isProfiling) { NSString *outputFile = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cpu_profile.json"]; nativeProfilerEnd(context, "profile", outputFile.UTF8String); @@ -238,7 +238,7 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) nativeProfilerStart(context, "profile"); } isProfiling = !isProfiling; - }]; + }]]; } } #endif diff --git a/React/Modules/RCTDevMenu.h b/React/Modules/RCTDevMenu.h index 13ddb0689..f3d70661b 100644 --- a/React/Modules/RCTDevMenu.h +++ b/React/Modules/RCTDevMenu.h @@ -12,6 +12,8 @@ #import "RCTBridge.h" #import "RCTBridgeModule.h" +@class RCTDevMenuItem; + /** * Developer menu, useful for exposing extra functionality when debugging. */ @@ -35,7 +37,7 @@ @property (nonatomic, assign) BOOL liveReloadEnabled; /** - * Shows the FPS monitor for the JS and Main threads + * Shows the FPS monitor for the JS and Main threads. */ @property (nonatomic, assign) BOOL showFPS; @@ -50,14 +52,44 @@ */ - (void)reload; +/** + * Deprecated. Use the `-addItem:` method instead. + */ +- (void)addItem:(NSString *)title + handler:(void(^)(void))handler DEPRECATED_ATTRIBUTE; + /** * Add custom item to the development menu. The handler will be called * when user selects the item. */ -- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler; +- (void)addItem:(RCTDevMenuItem *)item; @end +/** + * Developer menu item, used to expose additional functionality via the menu. + */ +@interface RCTDevMenuItem : NSObject + +/** + * This creates an item with a simple push-button interface, used to trigger an + * action. + */ ++ (instancetype)buttonItemWithTitle:(NSString *)title + handler:(void(^)(void))handler; + +/** + * This creates an item with a toggle behavior. The key is used to store the + * state of the toggle. For toggle items, the handler will be called immediately + * after the item is added if the item was already selected when the module was + * last loaded. + */ ++ (instancetype)toggleItemWithKey:(NSString *)key + title:(NSString *)title + selectedTitle:(NSString *)selectedTitle + handler:(void(^)(BOOL selected))handler; +@end + /** * This category makes the developer menu instance available via the * RCTBridge, which is useful for any class that needs to access the menu. diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index 94e2edf9c..1ae0e7ffc 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -44,31 +44,88 @@ static NSString *const RCTDevMenuSettingsKey = @"RCTDevMenu"; @end -@interface RCTDevMenuItem : NSObject +typedef NS_ENUM(NSInteger, RCTDevMenuType) { + RCTDevMenuTypeButton, + RCTDevMenuTypeToggle +}; -@property (nonatomic, copy) NSString *title; -@property (nonatomic, copy) dispatch_block_t handler; +@interface RCTDevMenuItem () -- (instancetype)initWithTitle:(NSString *)title handler:(dispatch_block_t)handler NS_DESIGNATED_INITIALIZER; +@property (nonatomic, assign, readonly) RCTDevMenuType type; +@property (nonatomic, copy, readonly) NSString *key; +@property (nonatomic, copy, readonly) NSString *title; +@property (nonatomic, copy, readonly) NSString *selectedTitle; +@property (nonatomic, copy) id value; @end @implementation RCTDevMenuItem +{ + id _handler; // block +} -- (instancetype)initWithTitle:(NSString *)title handler:(dispatch_block_t)handler +- (instancetype)initWithType:(RCTDevMenuType)type + key:(NSString *)key + title:(NSString *)title + selectedTitle:(NSString *)selectedTitle + handler:(id /* block */)handler { if ((self = [super init])) { + _type = type; + _key = [key copy]; _title = [title copy]; + _selectedTitle = [selectedTitle copy]; _handler = [handler copy]; + _value = nil; } return self; } RCT_NOT_IMPLEMENTED(- (instancetype)init) ++ (instancetype)buttonItemWithTitle:(NSString *)title + handler:(void (^)(void))handler +{ + return [[self alloc] initWithType:RCTDevMenuTypeButton + key:nil + title:title + selectedTitle:nil + handler:handler]; +} + ++ (instancetype)toggleItemWithKey:(NSString *)key + title:(NSString *)title + selectedTitle:(NSString *)selectedTitle + handler:(void (^)(BOOL selected))handler +{ + return [[self alloc] initWithType:RCTDevMenuTypeToggle + key:key + title:title + selectedTitle:selectedTitle + handler:handler]; +} + +- (void)callHandler +{ + switch (_type) { + case RCTDevMenuTypeButton: { + if (_handler) { + ((void(^)())_handler)(); + } + break; + } + case RCTDevMenuTypeToggle: { + if (_handler) { + ((void(^)(BOOL selected))_handler)([_value boolValue]); + } + break; + } + } +} + @end -@interface RCTDevMenu () +@interface RCTDevMenu () @property (nonatomic, strong) Class executorClass; @@ -125,15 +182,42 @@ RCT_EXPORT_MODULE() object:nil]; _defaults = [NSUserDefaults standardUserDefaults]; - _settings = [NSMutableDictionary new]; - _extraMenuItems = [NSMutableArray array]; + _settings = [[NSMutableDictionary alloc] initWithDictionary:[_defaults objectForKey:RCTDevMenuSettingsKey]]; + _extraMenuItems = [NSMutableArray new]; + + __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" + handler:^(__unused BOOL enabled) + { + [weakSelf.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; + }]]; // Delay setup until after Bridge init - [self settingsDidChange]; + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf updateSettings:_settings]; + }); #if TARGET_IPHONE_SIMULATOR - __weak RCTDevMenu *weakSelf = self; RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; // Toggle debug menu @@ -173,14 +257,31 @@ RCT_EXPORT_MODULE() { // Needed to prevent a race condition when reloading __weak RCTDevMenu *weakSelf = self; + NSDictionary *settings = [_defaults objectForKey:RCTDevMenuSettingsKey]; dispatch_async(dispatch_get_main_queue(), ^{ - [weakSelf updateSettings]; + [weakSelf updateSettings:settings]; }); } -- (void)updateSettings +/** + * This method loads the settings from NSUserDefaults and overrides any local + * settings with them. It should only be called on app launch, or after the app + * has returned from the background, when the settings might have been edited + * outside of the app. + */ +- (void)updateSettings:(NSDictionary *)settings { - NSDictionary *settings = [_defaults objectForKey:RCTDevMenuSettingsKey]; + // Fire handlers for items whose values have changed + for (RCTDevMenuItem *item in _extraMenuItems) { + if (item.key) { + id value = settings[item.key]; + if (value != item.value && ![value isEqual:item.value]) { + item.value = value; + [item callHandler]; + } + } + } + if ([settings isEqualToDictionary:_settings]) { return; } @@ -193,6 +294,39 @@ RCT_EXPORT_MODULE() self.executorClass = NSClassFromString(_settings[@"executorClass"]); } +/** + * This updates a particular setting, and then saves the settings. Because all + * settings are overwritten by this, it's important that this is not called + * before settings have been loaded initially, otherwise the other settings + * will be reset. + */ +- (void)updateSetting:(NSString *)name value:(id)value +{ + // Fire handler for item whose values has changed + for (RCTDevMenuItem *item in _extraMenuItems) { + if ([item.key isEqualToString:name]) { + if (value != item.value && ![value isEqual:item.value]) { + item.value = value; + [item callHandler]; + } + break; + } + } + + // Save the setting + id currentValue = _settings[name]; + if (currentValue == value || [currentValue isEqual:value]) { + return; + } + if (value) { + _settings[name] = value; + } else { + [_settings removeObjectForKey:name]; + } + [_defaults setObject:_settings forKey:RCTDevMenuSettingsKey]; + [_defaults synchronize]; +} + - (void)jsLoaded:(NSNotification *)notification { if (notification.userInfo[@"bridge"] != _bridge) { @@ -220,31 +354,22 @@ RCT_EXPORT_MODULE() self.profilingEnabled = _profilingEnabled; self.liveReloadEnabled = _liveReloadEnabled; self.executorClass = _executorClass; + + // Inspector can only be shown after JS has loaded + if ([_settings[@"showInspector"] boolValue]) { + [self.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; + } }); } -- (void)dealloc +- (void)invalidate { + _presentedItems = nil; [_updateTask cancel]; [_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } -- (void)updateSetting:(NSString *)name value:(id)value -{ - id currentValue = _settings[name]; - if (currentValue == value || [currentValue isEqual:value]) { - return; - } - if (value) { - _settings[name] = value; - } else { - [_settings removeObjectForKey:name]; - } - [_defaults setObject:_settings forKey:RCTDevMenuSettingsKey]; - [_defaults synchronize]; -} - - (void)showOnShake { if (_shakeToShow) { @@ -262,22 +387,34 @@ RCT_EXPORT_MODULE() } } -- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler +- (void)addItem:(NSString *)title handler:(void(^)(void))handler { - [_extraMenuItems addObject:[[RCTDevMenuItem alloc] initWithTitle:title handler:handler]]; + [self addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:handler]]; +} + +- (void)addItem:(RCTDevMenuItem *)item +{ + [_extraMenuItems addObject:item]; + + // Fire handler for items whose saved value doesn't match the default + [self settingsDidChange]; } - (NSArray *)menuItems { - NSMutableArray *items = [NSMutableArray array]; + NSMutableArray *items = [NSMutableArray new]; - [items addObject:[[RCTDevMenuItem alloc] initWithTitle:@"Reload" handler:^{ - [self reload]; + // Add built-in items + + __weak RCTDevMenu *weakSelf = self; + + [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload" handler:^{ + [weakSelf reload]; }]]; Class chromeExecutorClass = NSClassFromString(@"RCTWebSocketExecutor"); if (!chromeExecutorClass) { - [items addObject:[[RCTDevMenuItem alloc] initWithTitle:@"Chrome Debugger Unavailable" handler:^{ + [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Chrome Debugger Unavailable" handler:^{ [[[UIAlertView alloc] initWithTitle:@"Chrome Debugger Unavailable" message:@"You need to include the RCTWebSocket library to enable Chrome debugging" delegate:nil @@ -286,37 +423,28 @@ RCT_EXPORT_MODULE() }]]; } else { BOOL isDebuggingInChrome = _executorClass && _executorClass == chromeExecutorClass; - NSString *debugTitleChrome = isDebuggingInChrome ? @"Stop Chrome Debugging" : @"Debug in Chrome"; - [items addObject:[[RCTDevMenuItem alloc] initWithTitle:debugTitleChrome handler:^{ - self.executorClass = isDebuggingInChrome ? Nil : chromeExecutorClass; + NSString *debugTitleChrome = isDebuggingInChrome ? @"Disable Chrome Debugging" : @"Debug in Chrome"; + [items addObject:[RCTDevMenuItem buttonItemWithTitle:debugTitleChrome handler:^{ + weakSelf.executorClass = isDebuggingInChrome ? Nil : chromeExecutorClass; }]]; } Class safariExecutorClass = NSClassFromString(@"RCTWebViewExecutor"); BOOL isDebuggingInSafari = _executorClass && _executorClass == safariExecutorClass; - NSString *debugTitleSafari = isDebuggingInSafari ? @"Stop Safari Debugging" : @"Debug in Safari"; - [items addObject:[[RCTDevMenuItem alloc] initWithTitle:debugTitleSafari handler:^{ - self.executorClass = isDebuggingInSafari ? Nil : safariExecutorClass; - }]]; - - NSString *fpsMonitor = _showFPS ? @"Hide FPS Monitor" : @"Show FPS Monitor"; - [items addObject:[[RCTDevMenuItem alloc] initWithTitle:fpsMonitor handler:^{ - self.showFPS = !_showFPS; - }]]; - - [items addObject:[[RCTDevMenuItem alloc] initWithTitle:@"Inspect Element" handler:^{ - [_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; + NSString *debugTitleSafari = isDebuggingInSafari ? @"Disable Safari Debugging" : @"Debug in Safari"; + [items addObject:[RCTDevMenuItem buttonItemWithTitle:debugTitleSafari handler:^{ + weakSelf.executorClass = isDebuggingInSafari ? Nil : safariExecutorClass; }]]; if (_liveReloadURL) { NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload"; - [items addObject:[[RCTDevMenuItem alloc] initWithTitle:liveReloadTitle handler:^{ - self.liveReloadEnabled = !_liveReloadEnabled; + [items addObject:[RCTDevMenuItem buttonItemWithTitle:liveReloadTitle handler:^{ + weakSelf.liveReloadEnabled = !_liveReloadEnabled; }]]; NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Systrace" : @"Start Systrace"; - [items addObject:[[RCTDevMenuItem alloc] initWithTitle:profilingTitle handler:^{ - self.profilingEnabled = !_profilingEnabled; + [items addObject:[RCTDevMenuItem buttonItemWithTitle:profilingTitle handler:^{ + weakSelf.profilingEnabled = !_profilingEnabled; }]]; } @@ -337,7 +465,17 @@ RCT_EXPORT_METHOD(show) NSArray *items = [self menuItems]; for (RCTDevMenuItem *item in items) { - [actionSheet addButtonWithTitle:item.title]; + switch (item.type) { + case RCTDevMenuTypeButton: { + [actionSheet addButtonWithTitle:item.title]; + break; + } + case RCTDevMenuTypeToggle: { + BOOL selected = [item.value boolValue]; + [actionSheet addButtonWithTitle:selected? item.selectedTitle : item.title]; + break; + } + } } [actionSheet addButtonWithTitle:@"Cancel"]; @@ -357,7 +495,17 @@ RCT_EXPORT_METHOD(show) } RCTDevMenuItem *item = _presentedItems[buttonIndex]; - item.handler(); + switch (item.type) { + case RCTDevMenuTypeButton: { + [item callHandler]; + break; + } + case RCTDevMenuTypeToggle: { + BOOL value = [_settings[item.key] boolValue]; + [self updateSetting:item.key value:@(!value)]; // will call handler + break; + } + } return; } @@ -370,18 +518,14 @@ RCT_EXPORT_METHOD(reload) - (void)setShakeToShow:(BOOL)shakeToShow { - if (_shakeToShow != shakeToShow) { - _shakeToShow = shakeToShow; - [self updateSetting:@"shakeToShow" value: @(_shakeToShow)]; - } + _shakeToShow = shakeToShow; + [self updateSetting:@"shakeToShow" value:@(_shakeToShow)]; } - (void)setProfilingEnabled:(BOOL)enabled { - if (_profilingEnabled != enabled) { - _profilingEnabled = enabled; - [self updateSetting:@"profilingEnabled" value: @(_profilingEnabled)]; - } + _profilingEnabled = enabled; + [self updateSetting:@"profilingEnabled" value:@(_profilingEnabled)]; if (_liveReloadURL && enabled != RCTProfileIsProfiling()) { if (enabled) { @@ -394,10 +538,8 @@ RCT_EXPORT_METHOD(reload) - (void)setLiveReloadEnabled:(BOOL)enabled { - if (_liveReloadEnabled != enabled) { - _liveReloadEnabled = enabled; - [self updateSetting:@"liveReloadEnabled" value: @(_liveReloadEnabled)]; - } + _liveReloadEnabled = enabled; + [self updateSetting:@"liveReloadEnabled" value:@(_liveReloadEnabled)]; if (_liveReloadEnabled) { [self checkForUpdates]; @@ -411,7 +553,7 @@ RCT_EXPORT_METHOD(reload) { if (_executorClass != executorClass) { _executorClass = executorClass; - [self updateSetting:@"executorClass" value: NSStringFromClass(executorClass)]; + [self updateSetting:@"executorClass" value:NSStringFromClass(executorClass)]; } if (_bridge.executorClass != executorClass) { @@ -423,8 +565,8 @@ RCT_EXPORT_METHOD(reload) if (executorClass == Nil && (_bridge.executorClass != NSClassFromString(@"RCTWebSocketExecutor") && _bridge.executorClass != NSClassFromString(@"RCTWebViewExecutor"))) { - return; - } + return; + } _bridge.executorClass = executorClass; [self reload]; @@ -433,17 +575,8 @@ RCT_EXPORT_METHOD(reload) - (void)setShowFPS:(BOOL)showFPS { - if (_showFPS != showFPS) { - _showFPS = showFPS; - - if (showFPS) { - [_bridge.perfStats show]; - } else { - [_bridge.perfStats hide]; - } - - [self updateSetting:@"showFPS" value:@(showFPS)]; - } + _showFPS = showFPS; + [self updateSetting:@"showFPS" value:@(showFPS)]; } - (void)checkForUpdates @@ -489,6 +622,7 @@ RCT_EXPORT_METHOD(reload) - (void)show {} - (void)reload {} - (void)addItem:(NSString *)title handler:(dispatch_block_t)handler {} +- (void)addItem:(RCTDevMenu *)item {} @end From c372dab213d3905831b0adda4e66acddee8035fb Mon Sep 17 00:00:00 2001 From: Peter Cottle Date: Mon, 14 Sep 2015 09:57:43 -0700 Subject: [PATCH 0086/2013] Fix various issues with packager editor launcher Summary: There are a few small bugs with the code that launches the editor from the packager: * First of all, the filepath is not escaped which means tokens like `(` or spaces will mess up the process execution. Dropbox unfortunately decided to use spaces in its enterprise product, so I was getting this error: ![screen shot 2015-07-11 at 3 20 54 pm](https://cloud.githubusercontent.com/assets/1135007/8635748/186e7f2e-27ea-11e5-8058-1f4dabb79634.png) * Next, the line number argument formatting was assumed to be in a specific format (`:%d`) which actually errors out vim and other editors. * Lastly, the process was started synchronously but not attached to the stdin / stdout of the parent process. This means that only editors like mvim, sublime, and others would work since they spawn a new window. Editors like emacs, vi, nano, etc wouldn't work and instead just hang at the command line. So I whipped up this diff to fix a number of these issues, demo here: http://recordit.co/M6zwiUj7hp The demo shows both Closes https://github.com/facebook/react-native/pull/1957 Reviewed By: @vjeux, @pcottle Differential Revision: D2420941 Pulled By: @frantic --- packager/launchEditor.js | 77 ++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/packager/launchEditor.js b/packager/launchEditor.js index b572b5cbd..f98faaf8b 100644 --- a/packager/launchEditor.js +++ b/packager/launchEditor.js @@ -10,7 +10,37 @@ var chalk = require('chalk'); var fs = require('fs'); -var exec = require('child_process').exec; +var spawn = require('child_process').spawn; + +function isTerminalEditor(editor) { + switch (editor) { + case 'vim': + case 'emacs': + case 'nano': + return true; + } + return false; +} + +function getArgumentsForLineNumber(editor, fileName, lineNumber) { + switch (editor) { + case 'vim': + case 'mvim': + return [fileName, '+' + lineNumber]; + case 'atom': + case 'subl': + case 'sublime': + return [fileName + ':' + lineNumber]; + case 'joe': + case 'emacs': + return ['+' + lineNumber, fileName]; + } + + // For all others, drop the lineNumber until we have + // a mapping above, since providing the lineNumber incorrectly + // can result in errors or confusing behavior. + return [fileName]; +} function printInstructions(title) { console.log([ @@ -25,28 +55,45 @@ function printInstructions(title) { ].join('\n')); } +var _childProcess = null; function launchEditor(fileName, lineNumber) { if (!fs.existsSync(fileName)) { return; } - var argument = fileName; - if (lineNumber) { - argument += ':' + lineNumber; + var editor = process.env.REACT_EDITOR || process.env.EDITOR; + if (!editor) { + printInstructions('PRO TIP'); + return; } - var editor = process.env.REACT_EDITOR || process.env.EDITOR; - if (editor) { - console.log('Opening ' + chalk.underline(fileName) + ' with ' + chalk.bold(editor)); - exec(editor + ' ' + argument, function(error) { - if (error) { - console.log(chalk.red(error.message)); - printInstructions('How to fix'); - } - }); - } else { - printInstructions('PRO TIP'); + var args = [fileName]; + if (lineNumber) { + args = getArgumentsForLineNumber(editor, fileName, lineNumber); } + console.log('Opening ' + chalk.underline(fileName) + ' with ' + chalk.bold(editor)); + + if (_childProcess && isTerminalEditor(editor)) { + // There's an existing editor process already and it's attached + // to the terminal, so go kill it. Otherwise two separate editor + // instances attach to the stdin/stdout which gets confusing. + _childProcess.kill('SIGKILL'); + } + + _childProcess = spawn(editor, args, {stdio: 'inherit'}); + _childProcess.on('exit', function(errorCode) { + _childProcess = null; + + if (errorCode) { + console.log(chalk.red('Your editor exited with an error!')); + printInstructions('Keep these instructions in mind:'); + } + }); + + _childProcess.on('error', function(error) { + console.log(chalk.red(error.message)); + printInstructions('How to fix:'); + }) } module.exports = launchEditor; From 42eb5464fd8a65ed84b799de5d4dc225349449be Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Mon, 14 Sep 2015 15:35:58 +0100 Subject: [PATCH 0087/2013] Release React Native for Android This is an early release and there are several things that are known not to work if you're porting your iOS app to Android. See the Known Issues guide on the website. We will work with the community to reach platform parity with iOS. --- .gitignore | 6 + Examples/Movies/Movies/AppDelegate.m | 4 +- Examples/Movies/MoviesApp.android.js | 94 ++ Examples/Movies/SearchBar.android.js | 104 ++ Examples/Movies/android/app/build.gradle | 35 + .../Movies/android/app/proguard-rules.pro | 17 + .../android/app/src/main/AndroidManifest.xml | 21 + .../facebook/react/movies/MoviesActivity.java | 89 ++ .../res/drawable-hdpi/android_back_white.png | Bin 0 -> 237 bytes .../drawable-hdpi/android_search_white.png | Bin 0 -> 575 bytes .../res/drawable-mdpi/android_back_white.png | Bin 0 -> 190 bytes .../drawable-mdpi/android_search_white.png | Bin 0 -> 337 bytes .../res/drawable-xhdpi/android_back_white.png | Bin 0 -> 266 bytes .../drawable-xhdpi/android_search_white.png | Bin 0 -> 581 bytes .../drawable-xxhdpi/android_back_white.png | Bin 0 -> 337 bytes .../drawable-xxhdpi/android_search_white.png | Bin 0 -> 930 bytes .../res/drawable/rotten_tomatoes_icon.png | Bin 0 -> 58467 bytes .../app/src/main/res/layout/activity_main.xml | 12 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 8 + Examples/SampleApp/android/app/build.gradle | 35 + .../SampleApp/android/app/proguard-rules.pro | 17 + .../android/app/src/main/AndroidManifest.xml | 21 + .../facebook/react/sample/MainActivity.java | 88 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 8 + Examples/SampleApp/index.android.js | 52 + .../AccessibilityAndroidExample.android.js | 213 ++++ .../ProgressBarAndroidExample.android.js | 62 + .../UIExplorer/ScrollViewSimpleExample.js | 1 + .../SwitchAndroidExample.android.js | 80 ++ Examples/UIExplorer/TextExample.android.js | 349 ++++++ .../UIExplorer/TextInputExample.android.js | 316 +++++ .../UIExplorer/ToastAndroidExample.android.js | 68 + .../ToolbarAndroidExample.android.js | 119 ++ Examples/UIExplorer/UIExplorerApp.android.js | 53 +- Examples/UIExplorer/UIExplorerList.android.js | 99 ++ Examples/UIExplorer/XHRExample.android.js | 326 +++++ Examples/UIExplorer/android/app/build.gradle | 35 + .../UIExplorer/android/app/proguard-rules.pro | 17 + .../android/app/src/main/AndroidManifest.xml | 23 + .../app/src/main/java/UIExplorerActivity.java | 89 ++ .../res/drawable/ic_create_black_48dp.png | Bin 0 -> 406 bytes .../main/res/drawable/ic_menu_black_24dp.png | Bin 0 -> 179 bytes .../res/drawable/ic_settings_black_48dp.png | Bin 0 -> 1257 bytes .../src/main/res/drawable/launcher_icon.png | Bin 0 -> 9578 bytes .../res/drawable/uie_comment_highlighted.png | Bin 0 -> 403 bytes .../main/res/drawable/uie_comment_normal.png | Bin 0 -> 420 bytes .../main/res/drawable/uie_thumb_normal.png | Bin 0 -> 850 bytes .../main/res/drawable/uie_thumb_selected.png | Bin 0 -> 1110 bytes .../app/src/main/res/layout/activity_main.xml | 12 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 8 + Libraries/AppStateIOS/AppStateIOS.android.js | 4 +- .../ActivityIndicatorIOS.android.js | 22 + .../DatePicker/DatePickerIOS.android.js | 46 + .../DrawerLayoutAndroid.android.js | 224 ++++ .../Navigation/NavigatorIOS.android.js | 13 + ...orBreadcrumbNavigationBarStyles.android.js | 217 ++++ .../NavigatorNavigationBarStyles.android.js | 159 +++ .../ProgressBarAndroid.android.js | 92 ++ .../Components/SliderIOS/SliderIOS.android.js | 14 + .../StatusBar/StatusBarIOS.android.js | 14 + .../SwitchAndroid/SwitchAndroid.android.js | 80 ++ .../ToastAndroid/ToastAndroid.android.js | 38 + .../ToolbarAndroid/ToolbarAndroid.android.js | 174 +++ .../TouchableNativeFeedback.android.js | 219 ++++ Libraries/Image/Image.android.js | 169 +++ Libraries/Network/RCTNetworking.android.js | 45 + Libraries/Network/XMLHttpRequest.android.js | 66 + .../ReactIOS/renderApplication.android.js | 132 ++ Libraries/Storage/AsyncStorage.android.js | 233 ++++ Libraries/Utilities/BackAndroid.android.js | 63 + Libraries/Utilities/Platform.android.js | 22 + .../core/ExecutionEnvironment.android.js | 51 + README.md | 4 +- React.podspec | 2 +- ReactAndroid/DevExperience.md | 23 + ReactAndroid/README.md | 101 ++ ReactAndroid/build.gradle | 246 ++++ ReactAndroid/gradle.properties | 6 + ReactAndroid/libs/infer-annotations-1.5.jar | Bin 0 -> 11990 bytes ReactAndroid/release.gradle | 128 ++ ReactAndroid/src/main/AndroidManifest.xml | 7 + .../java/com/facebook/csslayout/CSSAlign.java | 20 + .../com/facebook/csslayout/CSSConstants.java | 21 + .../com/facebook/csslayout/CSSDirection.java | 18 + .../facebook/csslayout/CSSFlexDirection.java | 19 + .../com/facebook/csslayout/CSSJustify.java | 20 + .../com/facebook/csslayout/CSSLayout.java | 60 + .../facebook/csslayout/CSSLayoutContext.java | 23 + .../java/com/facebook/csslayout/CSSNode.java | 396 ++++++ .../facebook/csslayout/CSSPositionType.java | 17 + .../java/com/facebook/csslayout/CSSStyle.java | 46 + .../java/com/facebook/csslayout/CSSWrap.java | 17 + .../facebook/csslayout/CachedCSSLayout.java | 24 + .../com/facebook/csslayout/FloatUtil.java | 24 + .../com/facebook/csslayout/LayoutEngine.java | 1100 +++++++++++++++++ .../com/facebook/csslayout/MeasureOutput.java | 21 + .../main/java/com/facebook/csslayout/README | 12 + .../com/facebook/csslayout/README.facebook | 14 + .../java/com/facebook/csslayout/Spacing.java | 162 +++ .../com/facebook/csslayout/syncFromGithub.sh | 99 ++ .../main/java/com/facebook/jni/Countable.java | 40 + .../java/com/facebook/jni/CppException.java | 20 + .../facebook/jni/CppSystemErrorException.java | 27 + .../java/com/facebook/jni/HybridData.java | 50 + .../java/com/facebook/jni/Prerequisites.java | 65 + .../com/facebook/jni/UnknownCppException.java | 25 + .../proguard/annotations/DoNotStrip.java | 26 + .../annotations/KeepGettersAndSetters.java | 30 + .../annotations/proguard_annotations.pro | 15 + .../facebook/react/CompositeReactPackage.java | 88 ++ .../facebook/react/CoreModulesPackage.java | 86 ++ .../com/facebook/react/LifecycleState.java | 27 + .../facebook/react/ReactInstanceManager.java | 564 +++++++++ .../java/com/facebook/react/ReactPackage.java | 54 + .../com/facebook/react/ReactRootView.java | 374 ++++++ .../AbstractFloatPairPropertyUpdater.java | 64 + .../AbstractSingleFloatProperyUpdater.java | 54 + .../facebook/react/animation/Animation.java | 107 ++ .../react/animation/AnimationListener.java | 27 + .../animation/AnimationPropertyUpdater.java | 46 + .../react/animation/AnimationRegistry.java | 43 + .../react/animation/ImmediateAnimation.java | 28 + .../NoopAnimationPropertyUpdater.java | 30 + .../OpacityAnimationPropertyUpdater.java | 36 + .../PositionAnimationPairPropertyUpdater.java | 42 + .../RotationAnimationPropertyUpdater.java | 32 + .../ScaleXAnimationPropertyUpdater.java | 36 + .../ScaleXYAnimationPairPropertyUpdater.java | 42 + .../ScaleYAnimationPropertyUpdater.java | 36 + .../com/facebook/react/bridge/Arguments.java | 142 +++ .../react/bridge/AssertionException.java | 22 + .../facebook/react/bridge/BaseJavaModule.java | 181 +++ .../com/facebook/react/bridge/Callback.java | 25 + .../facebook/react/bridge/CallbackImpl.java | 29 + .../react/bridge/CatalystInstance.java | 419 +++++++ .../react/bridge/GuardedAsyncTask.java | 43 + .../bridge/InvalidIteratorException.java | 25 + .../JSApplicationCausedNativeException.java | 43 + ...JSApplicationIllegalArgumentException.java | 20 + .../facebook/react/bridge/JSBundleLoader.java | 69 ++ .../react/bridge/JSCJavaScriptExecutor.java | 28 + .../bridge/JSDebuggerWebSocketClient.java | 269 ++++ .../react/bridge/JavaScriptExecutor.java | 25 + .../react/bridge/JavaScriptModule.java | 29 + .../bridge/JavaScriptModuleRegistration.java | 96 ++ .../bridge/JavaScriptModuleRegistry.java | 76 ++ .../react/bridge/JavaScriptModulesConfig.java | 84 ++ .../react/bridge/JsonGeneratorHelper.java | 54 + .../react/bridge/LifecycleEventListener.java | 32 + .../bridge/NativeArgumentsParseException.java | 26 + .../facebook/react/bridge/NativeArray.java | 36 + .../com/facebook/react/bridge/NativeMap.java | 34 + .../facebook/react/bridge/NativeModule.java | 57 + .../NativeModuleCallExceptionHandler.java | 28 + .../react/bridge/NativeModuleRegistry.java | 200 +++ .../react/bridge/NoSuchKeyException.java | 24 + .../NotThreadSafeBridgeIdleDebugListener.java | 33 + .../ObjectAlreadyConsumedException.java | 26 + .../react/bridge/OnBatchCompleteListener.java | 18 + .../react/bridge/ProxyJavaScriptExecutor.java | 96 ++ .../react/bridge/ReactApplicationContext.java | 25 + .../facebook/react/bridge/ReactBridge.java | 71 ++ .../facebook/react/bridge/ReactCallback.java | 22 + .../facebook/react/bridge/ReactContext.java | 202 +++ .../bridge/ReactContextBaseJavaModule.java | 30 + .../facebook/react/bridge/ReactMethod.java | 26 + .../facebook/react/bridge/ReadableArray.java | 27 + .../facebook/react/bridge/ReadableMap.java | 28 + .../bridge/ReadableMapKeySeyIterator.java | 22 + .../react/bridge/ReadableNativeArray.java | 47 + .../react/bridge/ReadableNativeMap.java | 75 ++ .../facebook/react/bridge/ReadableType.java | 26 + .../facebook/react/bridge/SoftAssertions.java | 41 + .../facebook/react/bridge/UiThreadUtil.java | 56 + .../bridge/UnexpectedNativeTypeException.java | 25 + .../bridge/WebsocketJavaScriptExecutor.java | 183 +++ .../facebook/react/bridge/WritableArray.java | 24 + .../facebook/react/bridge/WritableMap.java | 26 + .../react/bridge/WritableNativeArray.java | 60 + .../react/bridge/WritableNativeMap.java | 68 + .../com/facebook/react/bridge/package_js.py | 14 + .../queue/CatalystQueueConfiguration.java | 90 ++ .../queue/CatalystQueueConfigurationSpec.java | 79 ++ .../bridge/queue/MessageQueueThread.java | 144 +++ .../queue/MessageQueueThreadHandler.java | 36 + .../bridge/queue/MessageQueueThreadSpec.java | 48 + .../react/bridge/queue/NativeRunnable.java | 29 + .../queue/QueueThreadExceptionHandler.java | 19 + .../com/facebook/react/common/LongArray.java | 78 ++ .../com/facebook/react/common/MapBuilder.java | 154 +++ .../facebook/react/common/ReactConstants.java | 15 + .../com/facebook/react/common/SetBuilder.java | 26 + .../facebook/react/common/ShakeDetector.java | 121 ++ .../facebook/react/common/SystemClock.java | 25 + .../common/annotations/VisibleForTesting.java | 17 + .../common/futures/SimpleSettableFuture.java | 62 + .../react/devsupport/AndroidManifest.xml | 10 + .../devsupport/DebugOverlayController.java | 54 + .../devsupport/DebugServerException.java | 74 ++ .../react/devsupport/DevInternalSettings.java | 70 ++ .../react/devsupport/DevOptionHandler.java | 25 + .../react/devsupport/DevServerHelper.java | 307 +++++ .../react/devsupport/DevSettingsActivity.java | 29 + .../react/devsupport/DevSupportManager.java | 629 ++++++++++ .../devsupport/ExceptionFormatterHelper.java | 75 ++ .../facebook/react/devsupport/FpsView.java | 102 ++ .../ReactInstanceDevCommandsHandler.java | 34 + .../react/devsupport/RedBoxDialog.java | 84 ++ .../modules/common/ModuleDataCleaner.java | 50 + .../core/DefaultHardwareBackBtnHandler.java | 25 + .../core/DeviceEventManagerModule.java | 67 + .../modules/core/ExceptionsManagerModule.java | 78 ++ .../react/modules/core/JSTimersExecution.java | 18 + .../modules/core/JavascriptException.java | 21 + .../facebook/react/modules/core/Timing.java | 204 +++ .../modules/debug/AnimationsDebugModule.java | 120 ++ .../modules/debug/DeveloperSettings.java | 26 + .../DidJSUpdateUiDuringFrameDetector.java | 175 +++ .../modules/debug/FpsDebugFrameCallback.java | 196 +++ .../react/modules/debug/SourceCodeModule.java | 54 + .../react/modules/fresco/FrescoModule.java | 76 ++ .../modules/network/NetworkingModule.java | 289 +++++ .../modules/network/OkHttpClientProvider.java | 36 + .../modules/network/RequestBodyUtil.java | 115 ++ .../storage/AsyncLocalStorageUtil.java | 147 +++ .../storage/AsyncStorageErrorUtil.java | 47 + .../modules/storage/AsyncStorageModule.java | 369 ++++++ .../storage/CatalystSQLiteOpenHelper.java | 53 + .../modules/systeminfo/AndroidInfoModule.java | 37 + .../react/modules/toast/ToastModule.java | 52 + .../react/shell/MainReactPackage.java | 73 ++ .../touch/CatalystInterceptingViewGroup.java | 34 + .../react/touch/JSResponderHandler.java | 77 ++ .../touch/OnInterceptTouchEventListener.java | 30 + .../react/uimanager/AccessibilityHelper.java | 103 ++ .../react/uimanager/AndroidManifest.xml | 6 + .../facebook/react/uimanager/AppRegistry.java | 20 + .../uimanager/BaseCSSPropertyApplicator.java | 146 +++ .../uimanager/BaseViewPropertyApplicator.java | 175 +++ .../react/uimanager/CSSColorUtil.java | 155 +++ .../uimanager/CatalystStylesDiffMap.java | 89 ++ .../react/uimanager/DisplayMetricsHolder.java | 29 + .../GuardedChoreographerFrameCallback.java | 43 + .../IllegalViewOperationException.java | 22 + .../uimanager/MeasureSpecAssertions.java | 29 + .../uimanager/NativeViewHierarchyManager.java | 607 +++++++++ .../NativeViewHierarchyOptimizer.java | 428 +++++++ .../uimanager/NoSuchNativeViewException.java | 21 + .../react/uimanager/OnLayoutEvent.java | 51 + .../facebook/react/uimanager/PixelUtil.java | 60 + .../react/uimanager/PointerEvents.java | 38 + .../react/uimanager/ReactChoreographer.java | 121 ++ .../react/uimanager/ReactCompoundView.java | 28 + .../ReactInvalidPropertyException.java | 18 + .../facebook/react/uimanager/ReactNative.java | 19 + .../uimanager/ReactPointerEventsView.java | 24 + .../react/uimanager/ReactShadowNode.java | 374 ++++++ .../facebook/react/uimanager/RootView.java | 24 + .../react/uimanager/RootViewManager.java | 30 + .../react/uimanager/RootViewUtil.java | 34 + .../react/uimanager/ShadowNodeRegistry.java | 72 ++ .../react/uimanager/SimpleViewManager.java | 36 + .../uimanager/SizeMonitoringFrameLayout.java | 56 + .../react/uimanager/ThemedReactContext.java | 50 + .../react/uimanager/TouchTargetHelper.java | 146 +++ .../react/uimanager/UIManagerModule.java | 837 +++++++++++++ .../uimanager/UIManagerModuleConstants.java | 157 +++ .../UIManagerModuleConstantsHelper.java | 100 ++ .../com/facebook/react/uimanager/UIProp.java | 52 + .../react/uimanager/UIViewOperationQueue.java | 631 ++++++++++ .../facebook/react/uimanager/ViewAtIndex.java | 33 + .../react/uimanager/ViewDefaults.java | 20 + .../react/uimanager/ViewGroupManager.java | 65 + .../facebook/react/uimanager/ViewManager.java | 216 ++++ .../react/uimanager/ViewManagerRegistry.java | 38 + .../facebook/react/uimanager/ViewProps.java | 112 ++ .../debug/DebugComponentOwnershipModule.java | 100 ++ .../NotThreadSafeUiManagerDebugListener.java | 32 + .../react/uimanager/events/Event.java | 83 ++ .../uimanager/events/EventDispatcher.java | 302 +++++ .../uimanager/events/NativeGestureUtil.java | 33 + .../uimanager/events/RCTEventEmitter.java | 24 + .../react/uimanager/events/TouchEvent.java | 98 ++ .../events/TouchEventCoalescingKeyHelper.java | 86 ++ .../uimanager/events/TouchEventType.java | 30 + .../react/uimanager/events/TouchesHelper.java | 99 ++ .../react/views/drawer/ReactDrawerLayout.java | 73 ++ .../drawer/ReactDrawerLayoutManager.java | 182 +++ .../drawer/events/DrawerClosedEvent.java | 40 + .../drawer/events/DrawerOpenedEvent.java | 40 + .../views/drawer/events/DrawerSlideEvent.java | 56 + .../events/DrawerStateChangedEvent.java | 53 + .../react/views/image/ImageResizeMode.java | 51 + .../react/views/image/ReactImageManager.java | 88 ++ .../react/views/image/ReactImageView.java | 259 ++++ .../progressbar/ProgressBarShadowNode.java | 86 ++ .../ReactProgressBarViewManager.java | 93 ++ .../views/scroll/OnScrollDispatchHelper.java | 44 + .../scroll/ReactHorizontalScrollView.java | 63 + .../ReactHorizontalScrollViewManager.java | 54 + .../react/views/scroll/ReactScrollView.java | 123 ++ .../scroll/ReactScrollViewCommandHelper.java | 68 + .../views/scroll/ReactScrollViewHelper.java | 41 + .../views/scroll/ReactScrollViewManager.java | 98 ++ .../react/views/scroll/ScrollEvent.java | 88 ++ .../react/views/switchview/ReactSwitch.java | 45 + .../views/switchview/ReactSwitchEvent.java | 57 + .../views/switchview/ReactSwitchManager.java | 119 ++ .../react/views/text/CustomStyleSpan.java | 114 ++ .../views/text/DefaultStyleValuesUtil.java | 65 + .../react/views/text/ReactRawTextManager.java | 48 + .../react/views/text/ReactTagSpan.java | 27 + .../react/views/text/ReactTextShadowNode.java | 394 ++++++ .../react/views/text/ReactTextView.java | 70 ++ .../views/text/ReactTextViewManager.java | 103 ++ .../text/ReactVirtualTextViewManager.java | 27 + .../react/views/textinput/ReactEditText.java | 275 +++++ .../textinput/ReactTextChangedEvent.java | 66 + .../textinput/ReactTextInputBlurEvent.java | 50 + .../ReactTextInputEndEditingEvent.java | 56 + .../views/textinput/ReactTextInputEvent.java | 72 ++ .../textinput/ReactTextInputFocusEvent.java | 50 + .../textinput/ReactTextInputManager.java | 445 +++++++ .../textinput/ReactTextInputShadowNode.java | 147 +++ .../ReactTextInputSubmitEditingEvent.java | 56 + .../views/textinput/ReactTextUpdate.java | 35 + .../views/toolbar/ReactToolbarManager.java | 234 ++++ .../toolbar/events/ToolbarClickEvent.java | 51 + .../facebook/react/views/view/ColorUtil.java | 55 + .../views/view/ReactClippingViewGroup.java | 63 + .../view/ReactClippingViewGroupHelper.java | 74 ++ .../react/views/view/ReactDrawableHelper.java | 94 ++ .../view/ReactViewBackgroundDrawable.java | 301 +++++ .../react/views/view/ReactViewGroup.java | 487 ++++++++ .../react/views/view/ReactViewManager.java | 222 ++++ .../com/facebook/soloader/ApkSoSource.java | 171 +++ .../facebook/soloader/DirectorySoSource.java | 76 ++ .../java/com/facebook/soloader/Elf32_Dyn.java | 14 + .../com/facebook/soloader/Elf32_Ehdr.java | 26 + .../com/facebook/soloader/Elf32_Phdr.java | 20 + .../com/facebook/soloader/Elf32_Shdr.java | 22 + .../java/com/facebook/soloader/Elf64_Dyn.java | 14 + .../com/facebook/soloader/Elf64_Ehdr.java | 26 + .../com/facebook/soloader/Elf64_Phdr.java | 20 + .../com/facebook/soloader/Elf64_Shdr.java | 22 + .../com/facebook/soloader/ExoSoSource.java | 177 +++ .../com/facebook/soloader/FileLocker.java | 48 + .../java/com/facebook/soloader/MinElf.java | 282 +++++ .../com/facebook/soloader/NativeLibrary.java | 93 ++ .../com/facebook/soloader/NoopSoSource.java | 28 + .../java/com/facebook/soloader/SoLoader.java | 237 ++++ .../java/com/facebook/soloader/SoSource.java | 57 + .../java/com/facebook/soloader/SysUtil.java | 205 +++ .../java/com/facebook/soloader/genstructs.sh | 35 + .../java/com/facebook/soloader/soloader.pro | 6 + .../java/com/facebook/systrace/Systrace.java | 31 + .../facebook/systrace/SystraceMessage.java | 69 ++ ReactAndroid/src/main/jni/Application.mk | 14 + .../src/main/jni/first-party/fb/Android.mk | 30 + .../src/main/jni/first-party/fb/Countable.h | 47 + .../main/jni/first-party/fb/ProgramLocation.h | 50 + .../src/main/jni/first-party/fb/RefPtr.h | 274 ++++ .../jni/first-party/fb/StaticInitialized.h | 40 + .../src/main/jni/first-party/fb/ThreadLocal.h | 118 ++ .../src/main/jni/first-party/fb/assert.cpp | 41 + .../jni/first-party/fb/include/fb/assert.h | 34 + .../main/jni/first-party/fb/include/fb/log.h | 361 ++++++ .../src/main/jni/first-party/fb/log.cpp | 100 ++ .../src/main/jni/first-party/fb/noncopyable.h | 21 + .../src/main/jni/first-party/fb/nonmovable.h | 21 + .../src/main/jni/first-party/jni/ALog.h | 83 ++ .../src/main/jni/first-party/jni/Android.mk | 35 + .../main/jni/first-party/jni/Countable.cpp | 69 ++ .../src/main/jni/first-party/jni/Countable.h | 33 + .../src/main/jni/first-party/jni/Doxyfile | 18 + .../main/jni/first-party/jni/Environment.cpp | 89 ++ .../main/jni/first-party/jni/Environment.h | 61 + .../jni/first-party/jni/GlobalReference.h | 91 ++ .../main/jni/first-party/jni/LocalReference.h | 37 + .../main/jni/first-party/jni/LocalString.cpp | 242 ++++ .../main/jni/first-party/jni/LocalString.h | 61 + .../src/main/jni/first-party/jni/OnLoad.cpp | 23 + .../main/jni/first-party/jni/Registration.h | 27 + .../jni/first-party/jni/WeakReference.cpp | 43 + .../main/jni/first-party/jni/WeakReference.h | 53 + .../src/main/jni/first-party/jni/fbjni.cpp | 203 +++ .../src/main/jni/first-party/jni/fbjni.h | 23 + .../main/jni/first-party/jni/fbjni/Common.h | 68 + .../first-party/jni/fbjni/CoreClasses-inl.h | 451 +++++++ .../jni/first-party/jni/fbjni/CoreClasses.h | 488 ++++++++ .../jni/first-party/jni/fbjni/Exceptions.cpp | 399 ++++++ .../jni/first-party/jni/fbjni/Exceptions.h | 130 ++ .../main/jni/first-party/jni/fbjni/Hybrid.cpp | 76 ++ .../main/jni/first-party/jni/fbjni/Hybrid.h | 251 ++++ .../main/jni/first-party/jni/fbjni/Meta-inl.h | 342 +++++ .../src/main/jni/first-party/jni/fbjni/Meta.h | 302 +++++ .../jni/fbjni/ReferenceAllocators-inl.h | 123 ++ .../jni/fbjni/ReferenceAllocators.h | 60 + .../first-party/jni/fbjni/References-inl.h | 370 ++++++ .../jni/first-party/jni/fbjni/References.cpp | 41 + .../jni/first-party/jni/fbjni/References.h | 506 ++++++++ .../first-party/jni/fbjni/Registration-inl.h | 206 +++ .../jni/first-party/jni/fbjni/Registration.h | 87 ++ .../jni/first-party/jni/fbjni/TypeTraits.h | 149 +++ .../main/jni/first-party/jni/jni_helpers.cpp | 195 +++ .../main/jni/first-party/jni/jni_helpers.h | 137 ++ ReactAndroid/src/main/jni/react/Android.mk | 32 + ReactAndroid/src/main/jni/react/Bridge.cpp | 109 ++ ReactAndroid/src/main/jni/react/Bridge.h | 50 + ReactAndroid/src/main/jni/react/Executor.h | 47 + .../src/main/jni/react/JSCExecutor.cpp | 154 +++ ReactAndroid/src/main/jni/react/JSCExecutor.h | 41 + .../src/main/jni/react/JSCHelpers.cpp | 22 + ReactAndroid/src/main/jni/react/JSCHelpers.h | 16 + .../src/main/jni/react/JSCLegacyProfiler.cpp | 63 + .../src/main/jni/react/JSCLegacyProfiler.h | 15 + .../src/main/jni/react/JSCPerfLogging.cpp | 212 ++++ .../src/main/jni/react/JSCPerfLogging.h | 11 + .../src/main/jni/react/JSCTracing.cpp | 327 +++++ ReactAndroid/src/main/jni/react/JSCTracing.h | 11 + .../src/main/jni/react/MethodCall.cpp | 61 + ReactAndroid/src/main/jni/react/MethodCall.h | 27 + ReactAndroid/src/main/jni/react/Value.cpp | 53 + ReactAndroid/src/main/jni/react/Value.h | 138 +++ .../src/main/jni/react/jni/Android.mk | 30 + .../src/main/jni/react/jni/JSLoader.cpp | 60 + .../src/main/jni/react/jni/JSLoader.h | 21 + .../src/main/jni/react/jni/NativeArray.cpp | 41 + .../src/main/jni/react/jni/NativeArray.h | 36 + .../src/main/jni/react/jni/OnLoad.cpp | 731 +++++++++++ .../src/main/jni/react/jni/ProxyExecutor.cpp | 64 + .../src/main/jni/react/jni/ProxyExecutor.h | 47 + .../src/main/jni/react/perftests/OnLoad.cpp | 65 + .../src/main/jni/react/test/.gitignore | 2 + .../src/main/jni/react/test/Android.mk | 27 + .../src/main/jni/react/test/jni/Android.mk | 1 + .../main/jni/react/test/jni/Application.mk | 6 + .../src/main/jni/react/test/jscexecutor.cpp | 223 ++++ .../src/main/jni/react/test/jsclogging.cpp | 44 + .../src/main/jni/react/test/methodcall.cpp | 115 ++ ReactAndroid/src/main/jni/react/test/run | 19 + .../src/main/jni/react/test/value.cpp | 38 + .../src/main/jni/third-party/boost/Android.mk | 11 + .../third-party/double-conversion/Android.mk | 23 + .../src/main/jni/third-party/folly/Android.mk | 41 + .../src/main/jni/third-party/glog/Android.mk | 32 + .../src/main/jni/third-party/glog/config.h | 179 +++ .../src/main/jni/third-party/jsc/Android.mk | 6 + .../devsupport/anim/catalyst_push_up_in.xml | 13 + .../devsupport/anim/catalyst_push_up_out.xml | 13 + .../main/res/devsupport/layout/fps_view.xml | 18 + .../res/devsupport/layout/redbox_view.xml | 54 + .../main/res/devsupport/values-cs/strings.xml | 16 + .../main/res/devsupport/values-da/strings.xml | 16 + .../main/res/devsupport/values-de/strings.xml | 16 + .../main/res/devsupport/values-el/strings.xml | 16 + .../res/devsupport/values-en-rGB/strings.xml | 16 + .../res/devsupport/values-es-rES/strings.xml | 16 + .../main/res/devsupport/values-es/strings.xml | 16 + .../res/devsupport/values-fb-rLL/strings.xml | 16 + .../main/res/devsupport/values-fb/strings.xml | 16 + .../main/res/devsupport/values-fi/strings.xml | 16 + .../main/res/devsupport/values-fr/strings.xml | 16 + .../main/res/devsupport/values-hu/strings.xml | 16 + .../main/res/devsupport/values-in/strings.xml | 16 + .../main/res/devsupport/values-it/strings.xml | 16 + .../main/res/devsupport/values-ja/strings.xml | 16 + .../main/res/devsupport/values-ko/strings.xml | 16 + .../main/res/devsupport/values-nb/strings.xml | 16 + .../main/res/devsupport/values-nl/strings.xml | 16 + .../main/res/devsupport/values-pl/strings.xml | 16 + .../res/devsupport/values-pt-rPT/strings.xml | 16 + .../main/res/devsupport/values-pt/strings.xml | 16 + .../main/res/devsupport/values-ro/strings.xml | 16 + .../main/res/devsupport/values-ru/strings.xml | 16 + .../main/res/devsupport/values-sv/strings.xml | 16 + .../main/res/devsupport/values-th/strings.xml | 16 + .../main/res/devsupport/values-tr/strings.xml | 16 + .../main/res/devsupport/values-vi/strings.xml | 16 + .../res/devsupport/values-zh-rCN/strings.xml | 16 + .../res/devsupport/values-zh-rHK/strings.xml | 16 + .../res/devsupport/values-zh-rTW/strings.xml | 16 + .../src/main/res/devsupport/values/colors.xml | 4 + .../main/res/devsupport/values/strings.xml | 16 + .../src/main/res/devsupport/values/styles.xml | 16 + .../main/res/devsupport/xml/preferences.xml | 40 + .../src/main/res/shell/values/styles.xml | 13 + build.gradle | 22 + docs/Accessibility.md | 113 +- docs/Animations.md | 2 +- docs/Debugging.md | 24 +- docs/DevelopmentSetupAndroid.md | 37 + docs/GettingStarted.md | 54 +- docs/Image.md | 22 +- docs/JavaScriptEnvironment.md | 2 +- docs/KnownIssues.md | 64 + docs/NativeComponentsAndroid.md | 179 +++ docs/NativeComponentsIOS.md | 2 +- docs/NativeModulesAndroid.md | 247 ++++ docs/NativeModulesIOS.md | 2 +- docs/NavigatorComparison.md | 2 +- docs/RunningOnDeviceAndroid.md | 38 + ...nningOnDevice.md => RunningOnDeviceIOS.md} | 2 +- docs/Testing.md | 12 +- docs/Tutorial.md | 32 +- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 52141 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 +++ gradlew.bat | 90 ++ local-cli/__tests__/generator-android-test.js | 77 ++ local-cli/__tests__/generator-test.js | 24 +- local-cli/cli.js | 23 +- local-cli/generate-android.js | 24 + local-cli/generator-android/index.js | 64 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../bin/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 52266 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + .../generator-android/templates/bin/gradlew | 164 +++ .../templates/bin/gradlew.bat | 90 ++ .../templates/package/MainActivity.java | 69 ++ .../templates/src/app/build.gradle | 29 + .../templates/src/app/proguard-rules.pro | 17 + .../src/app/src/main/AndroidManifest.xml | 22 + .../src/app/src/main/res/values/strings.xml | 3 + .../src/app/src/main/res/values/styles.xml | 8 + .../templates/src/build.gradle | 20 + .../templates/src/gradle.properties | 20 + .../templates/src/settings.gradle | 3 + local-cli/generator-ios/index.js | 2 +- local-cli/generator/index.js | 43 +- .../generator/templates/index.android.js | 52 + local-cli/init.js | 5 +- local-cli/run-android.js | 73 ++ local-cli/run-packager.js | 18 + package.json | 2 +- react-native-cli/package.json | 3 + react-native-gradle/.gitignore | 5 + .../.idea/codeStyleSettings.xml | 110 ++ react-native-gradle/README.md | 68 + react-native-gradle/build.gradle | 18 + react-native-gradle/settings.gradle | 1 + .../facebook/react/AbstractPackageJsTask.java | 232 ++++ .../facebook/react/PackageDebugJsTask.java | 20 + .../facebook/react/PackageReleaseJsTask.java | 20 + .../com/facebook/react/PackagerParams.java | 96 ++ .../facebook/react/ReactGradleExtension.java | 95 ++ .../com/facebook/react/ReactGradlePlugin.java | 39 + .../com.facebook.react.properties | 1 + .../facebook/react/PackageJSTasksTest.java | 85 ++ .../facebook/react/ReactGradlePluginTest.java | 26 + settings.gradle | 3 + website/publish-android.sh | 36 + website/server/extractDocs.js | 40 +- website/src/react-native/img/AndroidSDK1.png | Bin 0 -> 389672 bytes website/src/react-native/img/AndroidSDK2.png | Bin 0 -> 374852 bytes website/src/react-native/img/CreateAVD.png | Bin 0 -> 86956 bytes .../src/react-native/img/TutorialFinal2.png | Bin 0 -> 42030 bytes .../src/react-native/img/TutorialMock2.png | Bin 0 -> 8606 bytes .../img/TutorialSingleFetched2.png | Bin 0 -> 9399 bytes .../react-native/img/TutorialStyledMock2.png | Bin 0 -> 8797 bytes website/src/react-native/index.js | 148 ++- 571 files changed, 44550 insertions(+), 116 deletions(-) create mode 100644 Examples/Movies/MoviesApp.android.js create mode 100644 Examples/Movies/SearchBar.android.js create mode 100644 Examples/Movies/android/app/build.gradle create mode 100644 Examples/Movies/android/app/proguard-rules.pro create mode 100644 Examples/Movies/android/app/src/main/AndroidManifest.xml create mode 100644 Examples/Movies/android/app/src/main/java/com/facebook/react/movies/MoviesActivity.java create mode 100644 Examples/Movies/android/app/src/main/res/drawable-hdpi/android_back_white.png create mode 100755 Examples/Movies/android/app/src/main/res/drawable-hdpi/android_search_white.png create mode 100644 Examples/Movies/android/app/src/main/res/drawable-mdpi/android_back_white.png create mode 100755 Examples/Movies/android/app/src/main/res/drawable-mdpi/android_search_white.png create mode 100644 Examples/Movies/android/app/src/main/res/drawable-xhdpi/android_back_white.png create mode 100755 Examples/Movies/android/app/src/main/res/drawable-xhdpi/android_search_white.png create mode 100644 Examples/Movies/android/app/src/main/res/drawable-xxhdpi/android_back_white.png create mode 100755 Examples/Movies/android/app/src/main/res/drawable-xxhdpi/android_search_white.png create mode 100644 Examples/Movies/android/app/src/main/res/drawable/rotten_tomatoes_icon.png create mode 100644 Examples/Movies/android/app/src/main/res/layout/activity_main.xml create mode 100644 Examples/Movies/android/app/src/main/res/values/strings.xml create mode 100644 Examples/Movies/android/app/src/main/res/values/styles.xml create mode 100644 Examples/SampleApp/android/app/build.gradle create mode 100644 Examples/SampleApp/android/app/proguard-rules.pro create mode 100644 Examples/SampleApp/android/app/src/main/AndroidManifest.xml create mode 100644 Examples/SampleApp/android/app/src/main/java/com/facebook/react/sample/MainActivity.java create mode 100644 Examples/SampleApp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 Examples/SampleApp/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 Examples/SampleApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 Examples/SampleApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 Examples/SampleApp/android/app/src/main/res/values/strings.xml create mode 100644 Examples/SampleApp/android/app/src/main/res/values/styles.xml create mode 100644 Examples/SampleApp/index.android.js create mode 100644 Examples/UIExplorer/AccessibilityAndroidExample.android.js create mode 100644 Examples/UIExplorer/ProgressBarAndroidExample.android.js create mode 100644 Examples/UIExplorer/SwitchAndroidExample.android.js create mode 100644 Examples/UIExplorer/TextExample.android.js create mode 100644 Examples/UIExplorer/TextInputExample.android.js create mode 100644 Examples/UIExplorer/ToastAndroidExample.android.js create mode 100644 Examples/UIExplorer/ToolbarAndroidExample.android.js create mode 100644 Examples/UIExplorer/UIExplorerList.android.js create mode 100644 Examples/UIExplorer/XHRExample.android.js create mode 100644 Examples/UIExplorer/android/app/build.gradle create mode 100644 Examples/UIExplorer/android/app/proguard-rules.pro create mode 100644 Examples/UIExplorer/android/app/src/main/AndroidManifest.xml create mode 100644 Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/ic_create_black_48dp.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/ic_menu_black_24dp.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/ic_settings_black_48dp.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/launcher_icon.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/uie_comment_highlighted.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/uie_comment_normal.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/uie_thumb_normal.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/drawable/uie_thumb_selected.png create mode 100644 Examples/UIExplorer/android/app/src/main/res/layout/activity_main.xml create mode 100644 Examples/UIExplorer/android/app/src/main/res/values/strings.xml create mode 100644 Examples/UIExplorer/android/app/src/main/res/values/styles.xml create mode 100644 Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.android.js create mode 100644 Libraries/Components/DatePicker/DatePickerIOS.android.js create mode 100644 Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js create mode 100644 Libraries/Components/Navigation/NavigatorIOS.android.js create mode 100644 Libraries/Components/Navigator/NavigatorBreadcrumbNavigationBarStyles.android.js create mode 100644 Libraries/Components/Navigator/NavigatorNavigationBarStyles.android.js create mode 100644 Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js create mode 100644 Libraries/Components/SliderIOS/SliderIOS.android.js create mode 100644 Libraries/Components/StatusBar/StatusBarIOS.android.js create mode 100644 Libraries/Components/SwitchAndroid/SwitchAndroid.android.js create mode 100644 Libraries/Components/ToastAndroid/ToastAndroid.android.js create mode 100644 Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js create mode 100644 Libraries/Components/Touchable/TouchableNativeFeedback.android.js create mode 100644 Libraries/Image/Image.android.js create mode 100644 Libraries/Network/RCTNetworking.android.js create mode 100644 Libraries/Network/XMLHttpRequest.android.js create mode 100644 Libraries/ReactIOS/renderApplication.android.js create mode 100644 Libraries/Storage/AsyncStorage.android.js create mode 100644 Libraries/Utilities/BackAndroid.android.js create mode 100644 Libraries/Utilities/Platform.android.js create mode 100644 Libraries/vendor/react/vendor/core/ExecutionEnvironment.android.js create mode 100644 ReactAndroid/DevExperience.md create mode 100644 ReactAndroid/README.md create mode 100644 ReactAndroid/build.gradle create mode 100644 ReactAndroid/gradle.properties create mode 100644 ReactAndroid/libs/infer-annotations-1.5.jar create mode 100644 ReactAndroid/release.gradle create mode 100644 ReactAndroid/src/main/AndroidManifest.xml create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSAlign.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSConstants.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSDirection.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSFlexDirection.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSJustify.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayout.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayoutContext.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSPositionType.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSStyle.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSWrap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CachedCSSLayout.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/FloatUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/MeasureOutput.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/README create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/Spacing.java create mode 100755 ReactAndroid/src/main/java/com/facebook/csslayout/syncFromGithub.sh create mode 100644 ReactAndroid/src/main/java/com/facebook/jni/Countable.java create mode 100644 ReactAndroid/src/main/java/com/facebook/jni/CppException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/jni/CppSystemErrorException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/jni/HybridData.java create mode 100644 ReactAndroid/src/main/java/com/facebook/jni/Prerequisites.java create mode 100644 ReactAndroid/src/main/java/com/facebook/jni/UnknownCppException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/proguard/annotations/DoNotStrip.java create mode 100644 ReactAndroid/src/main/java/com/facebook/proguard/annotations/KeepGettersAndSetters.java create mode 100644 ReactAndroid/src/main/java/com/facebook/proguard/annotations/proguard_annotations.pro create mode 100644 ReactAndroid/src/main/java/com/facebook/react/CompositeReactPackage.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/LifecycleState.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/ReactPackage.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/AbstractFloatPairPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/AbstractSingleFloatProperyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/Animation.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/AnimationListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/AnimationPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/AnimationRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/ImmediateAnimation.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/NoopAnimationPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/OpacityAnimationPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/PositionAnimationPairPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/RotationAnimationPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/ScaleXAnimationPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/ScaleXYAnimationPairPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animation/ScaleYAnimationPropertyUpdater.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/AssertionException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/Callback.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/CallbackImpl.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/GuardedAsyncTask.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/InvalidIteratorException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationCausedNativeException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationIllegalArgumentException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JSDebuggerWebSocketClient.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistration.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModuleRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JavaScriptModulesConfig.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/JsonGeneratorHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/LifecycleEventListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArgumentsParseException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleCallExceptionHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NoSuchKeyException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/NotThreadSafeBridgeIdleDebugListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ObjectAlreadyConsumedException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/OnBatchCompleteListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReactApplicationContext.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReactCallback.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContextBaseJavaModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReactMethod.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableArray.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMapKeySeyIterator.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableType.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/SoftAssertions.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/UiThreadUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/UnexpectedNativeTypeException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/WebsocketJavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/WritableArray.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/package_js.py create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfiguration.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/CatalystQueueConfigurationSpec.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadSpec.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/NativeRunnable.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/QueueThreadExceptionHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/LongArray.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/MapBuilder.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/ReactConstants.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/SetBuilder.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/ShakeDetector.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/SystemClock.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/annotations/VisibleForTesting.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/AndroidManifest.xml create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugOverlayController.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugServerException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DevOptionHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSettingsActivity.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/ExceptionFormatterHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/FpsView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevCommandsHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/common/ModuleDataCleaner.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/DefaultHardwareBackBtnHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/JavascriptException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/debug/AnimationsDebugModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/debug/DeveloperSettings.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/debug/FpsDebugFrameCallback.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageErrorUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/storage/CatalystSQLiteOpenHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/toast/ToastModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/touch/CatalystInterceptingViewGroup.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/touch/JSResponderHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/touch/OnInterceptTouchEventListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/AndroidManifest.xml create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/AppRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseCSSPropertyApplicator.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewPropertyApplicator.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/CSSColorUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/CatalystStylesDiffMap.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/GuardedChoreographerFrameCallback.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/MeasureSpecAssertions.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/NoSuchNativeViewException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/OnLayoutEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/PointerEvents.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactCompoundView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactInvalidPropertyException.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactNative.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactPointerEventsView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ShadowNodeRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/SimpleViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/SizeMonitoringFrameLayout.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIProp.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewAtIndex.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/DebugComponentOwnershipModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/NotThreadSafeUiManagerDebugListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/NativeGestureUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTEventEmitter.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventType.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerClosedEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerOpenedEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerSlideEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerStateChangedEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarShadowNode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/OnScrollDispatchHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/DefaultStyleValuesUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTagSpan.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEndEditingEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputSubmitEditingEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextUpdate.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/toolbar/events/ToolbarClickEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroup.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroupHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/ApkSoSource.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/DirectorySoSource.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Dyn.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Ehdr.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Phdr.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Shdr.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Dyn.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Ehdr.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Phdr.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Shdr.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/ExoSoSource.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/FileLocker.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/MinElf.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/NativeLibrary.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/NoopSoSource.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/SoLoader.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/SoSource.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/SysUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/genstructs.sh create mode 100644 ReactAndroid/src/main/java/com/facebook/soloader/soloader.pro create mode 100644 ReactAndroid/src/main/java/com/facebook/systrace/Systrace.java create mode 100644 ReactAndroid/src/main/java/com/facebook/systrace/SystraceMessage.java create mode 100644 ReactAndroid/src/main/jni/Application.mk create mode 100644 ReactAndroid/src/main/jni/first-party/fb/Android.mk create mode 100644 ReactAndroid/src/main/jni/first-party/fb/Countable.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/ProgramLocation.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/RefPtr.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/StaticInitialized.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/ThreadLocal.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/assert.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/fb/include/fb/assert.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/include/fb/log.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/log.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/fb/noncopyable.h create mode 100644 ReactAndroid/src/main/jni/first-party/fb/nonmovable.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/ALog.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Android.mk create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Countable.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Countable.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Doxyfile create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Environment.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Environment.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/GlobalReference.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/LocalReference.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/LocalString.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/LocalString.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/OnLoad.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/Registration.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/WeakReference.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/WeakReference.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Common.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses-inl.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta-inl.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators-inl.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/References-inl.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/References.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/References.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration-inl.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/fbjni/TypeTraits.h create mode 100644 ReactAndroid/src/main/jni/first-party/jni/jni_helpers.cpp create mode 100644 ReactAndroid/src/main/jni/first-party/jni/jni_helpers.h create mode 100644 ReactAndroid/src/main/jni/react/Android.mk create mode 100644 ReactAndroid/src/main/jni/react/Bridge.cpp create mode 100644 ReactAndroid/src/main/jni/react/Bridge.h create mode 100644 ReactAndroid/src/main/jni/react/Executor.h create mode 100644 ReactAndroid/src/main/jni/react/JSCExecutor.cpp create mode 100644 ReactAndroid/src/main/jni/react/JSCExecutor.h create mode 100644 ReactAndroid/src/main/jni/react/JSCHelpers.cpp create mode 100644 ReactAndroid/src/main/jni/react/JSCHelpers.h create mode 100644 ReactAndroid/src/main/jni/react/JSCLegacyProfiler.cpp create mode 100644 ReactAndroid/src/main/jni/react/JSCLegacyProfiler.h create mode 100644 ReactAndroid/src/main/jni/react/JSCPerfLogging.cpp create mode 100644 ReactAndroid/src/main/jni/react/JSCPerfLogging.h create mode 100644 ReactAndroid/src/main/jni/react/JSCTracing.cpp create mode 100644 ReactAndroid/src/main/jni/react/JSCTracing.h create mode 100644 ReactAndroid/src/main/jni/react/MethodCall.cpp create mode 100644 ReactAndroid/src/main/jni/react/MethodCall.h create mode 100644 ReactAndroid/src/main/jni/react/Value.cpp create mode 100644 ReactAndroid/src/main/jni/react/Value.h create mode 100644 ReactAndroid/src/main/jni/react/jni/Android.mk create mode 100644 ReactAndroid/src/main/jni/react/jni/JSLoader.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/JSLoader.h create mode 100644 ReactAndroid/src/main/jni/react/jni/NativeArray.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/NativeArray.h create mode 100644 ReactAndroid/src/main/jni/react/jni/OnLoad.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/ProxyExecutor.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/ProxyExecutor.h create mode 100644 ReactAndroid/src/main/jni/react/perftests/OnLoad.cpp create mode 100644 ReactAndroid/src/main/jni/react/test/.gitignore create mode 100644 ReactAndroid/src/main/jni/react/test/Android.mk create mode 100644 ReactAndroid/src/main/jni/react/test/jni/Android.mk create mode 100644 ReactAndroid/src/main/jni/react/test/jni/Application.mk create mode 100644 ReactAndroid/src/main/jni/react/test/jscexecutor.cpp create mode 100644 ReactAndroid/src/main/jni/react/test/jsclogging.cpp create mode 100644 ReactAndroid/src/main/jni/react/test/methodcall.cpp create mode 100755 ReactAndroid/src/main/jni/react/test/run create mode 100644 ReactAndroid/src/main/jni/react/test/value.cpp create mode 100644 ReactAndroid/src/main/jni/third-party/boost/Android.mk create mode 100644 ReactAndroid/src/main/jni/third-party/double-conversion/Android.mk create mode 100644 ReactAndroid/src/main/jni/third-party/folly/Android.mk create mode 100644 ReactAndroid/src/main/jni/third-party/glog/Android.mk create mode 100644 ReactAndroid/src/main/jni/third-party/glog/config.h create mode 100644 ReactAndroid/src/main/jni/third-party/jsc/Android.mk create mode 100644 ReactAndroid/src/main/res/devsupport/anim/catalyst_push_up_in.xml create mode 100644 ReactAndroid/src/main/res/devsupport/anim/catalyst_push_up_out.xml create mode 100644 ReactAndroid/src/main/res/devsupport/layout/fps_view.xml create mode 100644 ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-cs/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-da/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-de/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-el/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-en-rGB/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-es-rES/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-es/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-fb-rLL/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-fb/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-fi/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-fr/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-hu/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-in/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-it/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-ja/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-ko/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-nb/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-nl/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-pl/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-pt-rPT/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-pt/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-ro/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-ru/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-sv/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-th/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-tr/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-vi/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-zh-rCN/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-zh-rHK/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values-zh-rTW/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values/colors.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values/strings.xml create mode 100644 ReactAndroid/src/main/res/devsupport/values/styles.xml create mode 100644 ReactAndroid/src/main/res/devsupport/xml/preferences.xml create mode 100644 ReactAndroid/src/main/res/shell/values/styles.xml create mode 100644 build.gradle create mode 100644 docs/DevelopmentSetupAndroid.md create mode 100644 docs/KnownIssues.md create mode 100644 docs/NativeComponentsAndroid.md create mode 100644 docs/NativeModulesAndroid.md create mode 100644 docs/RunningOnDeviceAndroid.md rename docs/{RunningOnDevice.md => RunningOnDeviceIOS.md} (98%) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 local-cli/__tests__/generator-android-test.js create mode 100644 local-cli/generate-android.js create mode 100644 local-cli/generator-android/index.js create mode 100644 local-cli/generator-android/templates/bin/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 local-cli/generator-android/templates/bin/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 local-cli/generator-android/templates/bin/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 local-cli/generator-android/templates/bin/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 local-cli/generator-android/templates/bin/gradle/wrapper/gradle-wrapper.jar create mode 100644 local-cli/generator-android/templates/bin/gradle/wrapper/gradle-wrapper.properties create mode 100755 local-cli/generator-android/templates/bin/gradlew create mode 100644 local-cli/generator-android/templates/bin/gradlew.bat create mode 100644 local-cli/generator-android/templates/package/MainActivity.java create mode 100644 local-cli/generator-android/templates/src/app/build.gradle create mode 100644 local-cli/generator-android/templates/src/app/proguard-rules.pro create mode 100644 local-cli/generator-android/templates/src/app/src/main/AndroidManifest.xml create mode 100644 local-cli/generator-android/templates/src/app/src/main/res/values/strings.xml create mode 100644 local-cli/generator-android/templates/src/app/src/main/res/values/styles.xml create mode 100644 local-cli/generator-android/templates/src/build.gradle create mode 100644 local-cli/generator-android/templates/src/gradle.properties create mode 100644 local-cli/generator-android/templates/src/settings.gradle create mode 100644 local-cli/generator/templates/index.android.js create mode 100644 local-cli/run-android.js create mode 100644 local-cli/run-packager.js create mode 100644 react-native-gradle/.gitignore create mode 100644 react-native-gradle/.idea/codeStyleSettings.xml create mode 100644 react-native-gradle/README.md create mode 100644 react-native-gradle/build.gradle create mode 100644 react-native-gradle/settings.gradle create mode 100644 react-native-gradle/src/main/java/com/facebook/react/AbstractPackageJsTask.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/PackageDebugJsTask.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/PackageReleaseJsTask.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/PackagerParams.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/ReactGradleExtension.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/ReactGradlePlugin.java create mode 100644 react-native-gradle/src/main/resources/META-INF/gradle-plugins/com.facebook.react.properties create mode 100644 react-native-gradle/src/test/java/com/facebook/react/PackageJSTasksTest.java create mode 100644 react-native-gradle/src/test/java/com/facebook/react/ReactGradlePluginTest.java create mode 100644 settings.gradle create mode 100755 website/publish-android.sh create mode 100644 website/src/react-native/img/AndroidSDK1.png create mode 100644 website/src/react-native/img/AndroidSDK2.png create mode 100644 website/src/react-native/img/CreateAVD.png create mode 100644 website/src/react-native/img/TutorialFinal2.png create mode 100644 website/src/react-native/img/TutorialMock2.png create mode 100644 website/src/react-native/img/TutorialSingleFetched2.png create mode 100644 website/src/react-native/img/TutorialStyledMock2.png diff --git a/.gitignore b/.gitignore index 5326cd9ca..60dbe841d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,12 @@ project.xcworkspace # OS X .DS_Store +# Android/IJ +.idea +.gradle +local.properties +*.iml + # Node node_modules *.log diff --git a/Examples/Movies/Movies/AppDelegate.m b/Examples/Movies/Movies/AppDelegate.m index 4d322023f..680d5eba4 100644 --- a/Examples/Movies/Movies/AppDelegate.m +++ b/Examples/Movies/Movies/AppDelegate.m @@ -37,14 +37,14 @@ * on the same Wi-Fi network. */ - jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/Movies/MoviesApp.ios.bundle?platform=ios&dev=true"]; + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/Movies/MoviesApp.ios.includeRequire.runModule.bundle"]; /** * OPTION 2 * Load from pre-bundled file on disk. To re-generate the static bundle, `cd` * to your Xcode project folder in the terminal, and run * - * $ curl 'http://localhost:8081/Examples/Movies/MoviesApp.includeRequire.runModule.bundle' -o main.jsbundle + * $ curl 'http://localhost:8081/Examples/Movies/MoviesApp.ios.includeRequire.runModule.bundle' -o main.jsbundle * * then add the `main.jsbundle` file to your project and uncomment this line: */ diff --git a/Examples/Movies/MoviesApp.android.js b/Examples/Movies/MoviesApp.android.js new file mode 100644 index 000000000..5dfb57fd2 --- /dev/null +++ b/Examples/Movies/MoviesApp.android.js @@ -0,0 +1,94 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule MoviesApp + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + AppRegistry, + BackAndroid, + Navigator, + StyleSheet, + ToolbarAndroid, + View, +} = React; + +var MovieScreen = require('./MovieScreen'); +var SearchScreen = require('./SearchScreen'); + +var _navigator; +BackAndroid.addEventListener('hardwareBackPress', () => { + if (_navigator && _navigator.getCurrentRoutes().length > 1) { + _navigator.pop(); + return true; + } + return false; +}); + +var RouteMapper = function(route, navigationOperations, onComponentRef) { + _navigator = navigationOperations; + if (route.name === 'search') { + return ( + + ); + } else if (route.name === 'movie') { + return ( + + + + + ); + } +}; + +var MoviesApp = React.createClass({ + render: function() { + var initialRoute = {name: 'search'}; + return ( + Navigator.SceneConfigs.FadeAndroid} + renderScene={RouteMapper} + /> + ); + } +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: 'white', + }, + toolbar: { + backgroundColor: '#a9a9a9', + height: 56, + }, +}); + +AppRegistry.registerComponent('MoviesApp', () => MoviesApp); + +module.exports = MoviesApp; diff --git a/Examples/Movies/SearchBar.android.js b/Examples/Movies/SearchBar.android.js new file mode 100644 index 000000000..2e12ff488 --- /dev/null +++ b/Examples/Movies/SearchBar.android.js @@ -0,0 +1,104 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule SearchBar + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Image, + Platform, + ProgressBarAndroid, + TextInput, + StyleSheet, + TouchableNativeFeedback, + View, +} = React; + +var IS_RIPPLE_EFFECT_SUPPORTED = Platform.Version >= 21; + +var SearchBar = React.createClass({ + render: function() { + var loadingView; + if (this.props.isLoading) { + loadingView = ( + + ); + } else { + loadingView = ; + } + var background = IS_RIPPLE_EFFECT_SUPPORTED ? + TouchableNativeFeedback.SelectableBackgroundBorderless() : + TouchableNativeFeedback.SelectableBackground(); + return ( + + this.refs.input && this.refs.input.focus()}> + + + + + + {loadingView} + + ); + } +}); + +var styles = StyleSheet.create({ + searchBar: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#a9a9a9', + height: 56, + }, + searchBarInput: { + flex: 1, + fontSize: 20, + fontWeight: 'bold', + color: 'white', + height: 50, + padding: 0, + backgroundColor: 'transparent' + }, + spinner: { + width: 30, + height: 30, + }, + icon: { + width: 24, + height: 24, + marginHorizontal: 8, + }, +}); + +module.exports = SearchBar; diff --git a/Examples/Movies/android/app/build.gradle b/Examples/Movies/android/app/build.gradle new file mode 100644 index 000000000..3a8c184e6 --- /dev/null +++ b/Examples/Movies/android/app/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 22 + buildToolsVersion "23.0.1" + + defaultConfig { + applicationId "com.facebook.react.movies" + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + ndk { + abiFilters "armeabi-v7a", "x86" + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:22.2.0' + + // Depend on pre-built React Native + compile 'com.facebook.react:react-native:0.11.+' + + // Depend on React Native source. + // This is useful for testing your changes when working on React Native. + // compile project(':ReactAndroid') +} diff --git a/Examples/Movies/android/app/proguard-rules.pro b/Examples/Movies/android/app/proguard-rules.pro new file mode 100644 index 000000000..a92fa177e --- /dev/null +++ b/Examples/Movies/android/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/Examples/Movies/android/app/src/main/AndroidManifest.xml b/Examples/Movies/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..2123d078f --- /dev/null +++ b/Examples/Movies/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/Examples/Movies/android/app/src/main/java/com/facebook/react/movies/MoviesActivity.java b/Examples/Movies/android/app/src/main/java/com/facebook/react/movies/MoviesActivity.java new file mode 100644 index 000000000..6499cea1f --- /dev/null +++ b/Examples/Movies/android/app/src/main/java/com/facebook/react/movies/MoviesActivity.java @@ -0,0 +1,89 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.facebook.react.movies; + +import android.app.Activity; +import android.os.Bundle; +import android.view.KeyEvent; + +import com.facebook.react.LifecycleState; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactRootView; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.shell.MainReactPackage; + +public class MoviesActivity extends Activity implements DefaultHardwareBackBtnHandler { + + private ReactInstanceManager mReactInstanceManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mReactInstanceManager = ReactInstanceManager.builder() + .setApplication(getApplication()) + .setBundleAssetName("MoviesApp.android.bundle") + .setJSMainModuleName("Examples/Movies/MoviesApp.android") + .addPackage(new MainReactPackage()) + .setUseDeveloperSupport(true) + .setInitialLifecycleState(LifecycleState.RESUMED) + .build(); + + ((ReactRootView) findViewById(R.id.react_root_view)) + .startReactApplication(mReactInstanceManager, "MoviesApp", null); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) { + mReactInstanceManager.showDevOptionsDialog(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onPause() { + super.onPause(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onPause(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onResume(this); + } + } + + @Override + public void onBackPressed() { + if (mReactInstanceManager != null) { + mReactInstanceManager.onBackPressed(); + } else { + super.onBackPressed(); + } + } + + @Override + public void invokeDefaultOnBackPressed() { + super.onBackPressed(); + } +} diff --git a/Examples/Movies/android/app/src/main/res/drawable-hdpi/android_back_white.png b/Examples/Movies/android/app/src/main/res/drawable-hdpi/android_back_white.png new file mode 100644 index 0000000000000000000000000000000000000000..a34f0dbb8fd520edb1d996294478110d3c085504 GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;V2Nu)NpOBzNqJ&XDnmhDdU8=| zafU;w=VzegbWaz@5Rc<;uX=Me81S%MXj(8`?2Xs?$Io-}j)w+p%w4vuspIUwi&N4b zJvcSx_rh{VxqU287#)^dut@|)E||Mg*IX#=(oB&lM~^5bruB6?-t>{!b+APtahl9C zF3!!1+QYTEI1kTzzA!nvdFj^A$>!H2cDgRLPhnfy%i;Ud**nOmCqP6zhvlf-+obMW kbF)sbS^O^k=KU74e(?n{ab}NOfKFuaboFyt=akR{0O5RHwEzGB literal 0 HcmV?d00001 diff --git a/Examples/Movies/android/app/src/main/res/drawable-hdpi/android_search_white.png b/Examples/Movies/android/app/src/main/res/drawable-hdpi/android_search_white.png new file mode 100755 index 0000000000000000000000000000000000000000..861b40db0d4ddb6d7b7b6d68d174072fb691008c GIT binary patch literal 575 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB*pj^6U4S$Y{B+)352QE?JR*yM zvQ&rUPlPeufHm>3#+V##5dyjv*0;-%hjjmI)MTyC0df$#L?bC7tn^ zn{tC#!w)8LW=uVF%U(h7NZg}oo?EYmdCCW|n}}tySkFG&6tzZ7Ni)V#mxrs)M(o_7 zLZig=wD<3#&P2=VJ-7XC`TgF^>ZKtoqb7^1OIE05I$heZWX+OYOL9G>=RLn@n5DqK zhG}}sYyJ%!U$#mwVb@^3?sD1g!?vlG|97VzaQ3uU@tyfl+w=9LRg=!C@Oo6Kzid5e zIsMDM&wJw6g)f(R@z2^oXwr1XvR{J-1!SYp|?6C~CW*;M& z76c31mokRrFLk~f`;b}OVYZqc!^{;&YkX(zkh=VPJ;V9^v!!NrKCETPk$xX@+M(`Y z|FR5OiM|)VQ)=7~2!}iDJupvmANQWTO-A#X!_V#!mM}i4@!`5p0b|YA|HkJV-4AFd zh~3~ia7Ad&UXOsu2MUiYcXzf{`^T9Q>-_Z3p|d3{ItA6gFNbN!=kRDSiarpqc>c|C z8&Ay!c8evA-yDuMoL?588N$AxE0&q7@zpyn)&*VPnbbm8`MqRgud`g}emFT07{v^p Lu6{1-oD!M(}6TtKSPDj(q%x-9Zwg>5Rc<;rx@}zIf%H}s}yibcP=+DRGW8r z^#R>4JkqvpALW8F(z6@SZkFs(e6^{2s@2(+)PHA;ZXSKpAHiUJfYE({pml=T6$|ET zdyY$;NMa5%+Q}elz*tncw6q~KM|bVdFRf|61cbt-Td`@_?h3LJmnyiF@bIGOrMXVb zQoPUPV)bGlfA+CHpZ@WLvH7bP)$DJMc%P5?T_+zE_(F3QJL`!B?`?0e&DLIU-uGSy z{{aiL7a!wGt_qgr>Ke(5J-T_|hq==wkNeh>R92qNNlg2Cy!`(YZ@c)5-ygG`lV9OA U_x_2kKz}lLy85}Sb4q9e0DP-{=l}o! literal 0 HcmV?d00001 diff --git a/Examples/Movies/android/app/src/main/res/drawable-xhdpi/android_back_white.png b/Examples/Movies/android/app/src/main/res/drawable-xhdpi/android_back_white.png new file mode 100644 index 0000000000000000000000000000000000000000..500892e105e98c5a84da959a6cf4eadc317582ee GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}t!4lVqlHmNblJdl&REC1Q^yH$_ z;tYpU&(A=~EuJopArXh)Ui9W-G8AZexN~ZS!NJ}c%H7!;V|X16Ub=@ac3L()^IyNG zS(9`jp@j(pZmp{DhwiC`&HNBCryC*{_Ks~F231r@tm7p|>*@R#tWV$vf|**%Orp6eAaXjpG& z5PiT=abDn`_xok}OV@npKA_=VuvTz`gw>xrEtz1G|xihP`RxLbau3P+%DdNO~jS0VAm|o;zELg|3@0?JaV~qCt;`-7J-o@sn= zed@>P&zu{Ui?3n!-@D;^SJgS6jO^9FZXEo-$oAIGm>{MrA+;|V9ICGr?OY=GVUb+< z)yDMD%6%eV=V)dH1Z=iroEN(HiNc0&6`wc@*1ytaXSntFocaNWtJZuBTaI67TprsP zm-6}^!e1{rR{;nxH*8kP$*}M$`YA3Yjr?)ZA=l*&%X1jy@4R$k~zgEG_fedy| zKCMbew!DaUo>IHyPuL_CSwYJ!rc>wYPQEhv#gw_rIgZ@tkrx#e{kr`N%5L>1~hVoEMWS}zasE7OO}g^nTmCv;K@nJPMe+z?9J$BIXlPu2^XV8+%D->wN_oS{l)WjiTfAb*PiEJlyiiz fYC$|<_?r2`s>aPtPE0w#U|{fc^>bP0l+XkKNSc6; literal 0 HcmV?d00001 diff --git a/Examples/Movies/android/app/src/main/res/drawable-xxhdpi/android_search_white.png b/Examples/Movies/android/app/src/main/res/drawable-xxhdpi/android_search_white.png new file mode 100755 index 0000000000000000000000000000000000000000..d9f75bc794e76f1e7057b35124cefe6c2fa9c5c8 GIT binary patch literal 930 zcmV;T16}-yP)mb5SFhNPR4CL}$ObYHF6EskADbz{t~4^dg< z45oOj_JAiqf6AEs(e-;N9=Ac?4w9N#NNC~(Fq|{?aP<2)7JNI1YoLyOwmJo#0_R$M z0w#(Rm#3Ud0AMZVKRaJ=uKAc;Yrb7{D|ja1u_|=i65UC0k6XD`+`zJ>(0yRYQO=<# zem9nVJ83U;1ZY|copYRf&XTuDYeVcGVAfJ(1IJqdT3eZhCI77J-Y=!RZpt#gVvIR< zSzr`@C1YKMkyg;8W&D-v0u%B~W{Q%7w1UPho>mq?GlA2u`Fvi)jv6TJ}I<$;mcUfGDJV&k*rxkQy8BaRyC<CLJnElPT9j5+D0q+HP3ck|u&1=M_dNRrfxuRAt?#VSj3^?lF@@C@h!E?4?a zibNIph1n_{Q+dMm)sPf5lJ^_MCa{EMg&axfxg=j-xgJSsNcySPk;O+zZ;dgHn0Pfw z?pr`9k3B| zCtd7Eteq~Bf}Bp;Bn3G(B1tvhbp!%|Kp+qZ1OgX}|BrB-B9PU+=Kufz07*qoM6N<$ Eg1Z%=^Z)<= literal 0 HcmV?d00001 diff --git a/Examples/Movies/android/app/src/main/res/drawable/rotten_tomatoes_icon.png b/Examples/Movies/android/app/src/main/res/drawable/rotten_tomatoes_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..395d7043130e5e1712052cf5722608b7f4315b4a GIT binary patch literal 58467 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 + + + + diff --git a/Examples/Movies/android/app/src/main/res/values/strings.xml b/Examples/Movies/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..7c4632e0d --- /dev/null +++ b/Examples/Movies/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + MoviesApp + diff --git a/Examples/Movies/android/app/src/main/res/values/styles.xml b/Examples/Movies/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..319eb0ca1 --- /dev/null +++ b/Examples/Movies/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/Examples/SampleApp/android/app/build.gradle b/Examples/SampleApp/android/app/build.gradle new file mode 100644 index 000000000..aafb4a48a --- /dev/null +++ b/Examples/SampleApp/android/app/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + applicationId "com.facebook.react.sample" + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + ndk { + abiFilters "armeabi-v7a", "x86" + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:23.0.0' + + // Depend on pre-built React Native + compile 'com.facebook.react:react-native:0.11.+' + + // Depend on React Native source. + // This is useful for testing your changes when working on React Native. + // compile project(':ReactAndroid') +} diff --git a/Examples/SampleApp/android/app/proguard-rules.pro b/Examples/SampleApp/android/app/proguard-rules.pro new file mode 100644 index 000000000..a92fa177e --- /dev/null +++ b/Examples/SampleApp/android/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/Examples/SampleApp/android/app/src/main/AndroidManifest.xml b/Examples/SampleApp/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..bf3de9949 --- /dev/null +++ b/Examples/SampleApp/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/Examples/SampleApp/android/app/src/main/java/com/facebook/react/sample/MainActivity.java b/Examples/SampleApp/android/app/src/main/java/com/facebook/react/sample/MainActivity.java new file mode 100644 index 000000000..eb2e4f8b1 --- /dev/null +++ b/Examples/SampleApp/android/app/src/main/java/com/facebook/react/sample/MainActivity.java @@ -0,0 +1,88 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.facebook.react.sample; + +import android.app.Activity; +import android.os.Bundle; +import android.view.KeyEvent; + +import com.facebook.react.LifecycleState; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactRootView; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.shell.MainReactPackage; +import com.facebook.soloader.SoLoader; + +public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler { + + private ReactInstanceManager mReactInstanceManager; + private ReactRootView mReactRootView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + SoLoader.init(this, false); + mReactRootView = new ReactRootView(this); + + mReactInstanceManager = ReactInstanceManager.builder() + .setApplication(getApplication()) + .setBundleAssetName("index.android.bundle") + .setJSMainModuleName("Examples/SampleApp/index.android") + .addPackage(new MainReactPackage()) + .setUseDeveloperSupport(BuildConfig.DEBUG) + .setInitialLifecycleState(LifecycleState.RESUMED) + .build(); + + mReactRootView.startReactApplication( + mReactInstanceManager, + "SampleApp", + null); + + setContentView(mReactRootView); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) { + mReactInstanceManager.showDevOptionsDialog(); + return true; + } + return super.onKeyUp(keyCode, event); + + } + + @Override + public void invokeDefaultOnBackPressed() { + super.onBackPressed(); + } + + @Override + protected void onPause() { + super.onPause(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onPause(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onResume(this); + } + } +} diff --git a/Examples/SampleApp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Examples/SampleApp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cde69bcccec65160d92116f20ffce4fce0b5245c GIT binary patch literal 3418 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 literal 0 HcmV?d00001 diff --git a/Examples/SampleApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Examples/SampleApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa42f0e7b91d006d22352c9ff2f134e504e3c1d GIT binary patch literal 4842 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 literal 0 HcmV?d00001 diff --git a/Examples/SampleApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Examples/SampleApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..324e72cdd7480cb983fa1bcc7ce686e51ef87fe7 GIT binary patch literal 7718 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 literal 0 HcmV?d00001 diff --git a/Examples/SampleApp/android/app/src/main/res/values/strings.xml b/Examples/SampleApp/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..18f470881 --- /dev/null +++ b/Examples/SampleApp/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + SampleApp + diff --git a/Examples/SampleApp/android/app/src/main/res/values/styles.xml b/Examples/SampleApp/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..319eb0ca1 --- /dev/null +++ b/Examples/SampleApp/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/Examples/SampleApp/index.android.js b/Examples/SampleApp/index.android.js new file mode 100644 index 000000000..47371fa59 --- /dev/null +++ b/Examples/SampleApp/index.android.js @@ -0,0 +1,52 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + */ +'use strict'; + +var React = require('react-native'); +var { + AppRegistry, + StyleSheet, + Text, + View, +} = React; + +var SampleApp = React.createClass({ + render: function() { + return ( + + + Welcome to React Native! + + + To get started, edit index.android.js + + + Shake or press menu button for dev menu + + + ); + } +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#F5FCFF', + }, + welcome: { + fontSize: 20, + textAlign: 'center', + margin: 10, + }, + instructions: { + textAlign: 'center', + color: '#333333', + marginBottom: 5, + }, +}); + +AppRegistry.registerComponent('SampleApp', () => SampleApp); diff --git a/Examples/UIExplorer/AccessibilityAndroidExample.android.js b/Examples/UIExplorer/AccessibilityAndroidExample.android.js new file mode 100644 index 000000000..3df94c603 --- /dev/null +++ b/Examples/UIExplorer/AccessibilityAndroidExample.android.js @@ -0,0 +1,213 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + Text, + View, + ToastAndroid, + TouchableWithoutFeedback, +} = React; + +var UIExplorerBlock = require('./UIExplorerBlock'); +var UIExplorerPage = require('./UIExplorerPage'); + +var importantForAccessibilityValues = ['auto', 'yes', 'no', 'no-hide-descendants']; + +var AccessibilityAndroidExample = React.createClass({ + + statics: { + title: 'Accessibility', + description: 'Examples of using Accessibility API.', + }, + + getInitialState: function() { + return { + count: 0, + backgroundImportantForAcc: 0, + forgroundImportantForAcc: 0, + }; + }, + + _addOne: function() { + this.setState({ + count: ++this.state.count, + }); + }, + + _changeBackgroundImportantForAcc: function() { + this.setState({ + backgroundImportantForAcc: (this.state.backgroundImportantForAcc + 1) % 4, + }); + }, + + _changeForgroundImportantForAcc: function() { + this.setState({ + forgroundImportantForAcc: (this.state.forgroundImportantForAcc + 1) % 4, + }); + }, + + render: function() { + return ( + + + + + + This is + + + nontouchable normal view. + + + + + + + + This is + + + nontouchable accessible view without label. + + + + + + + + This is + + + nontouchable accessible view with label. + + + + + + ToastAndroid.show('Toasts work by default', ToastAndroid.SHORT)} + accessibilityComponentType="button"> + + Click me + Or not + + + + + + + + Click me + + + + Clicked {this.state.count} times + + + + + + + + + Hello + + + + + + + world + + + + + + + + Change importantForAccessibility for background layout. + + + + + + Background layout importantForAccessibility + + + {importantForAccessibilityValues[this.state.backgroundImportantForAcc]} + + + + + + Change importantForAccessibility for forground layout. + + + + + + Forground layout importantForAccessibility + + + {importantForAccessibilityValues[this.state.forgroundImportantForAcc]} + + + + + + ); + }, +}); + +var styles = StyleSheet.create({ + embedded: { + backgroundColor: 'yellow', + padding:10, + }, + container: { + flex: 1, + backgroundColor: 'white', + padding: 10, + height:150, + }, +}); + +module.exports = AccessibilityAndroidExample; diff --git a/Examples/UIExplorer/ProgressBarAndroidExample.android.js b/Examples/UIExplorer/ProgressBarAndroidExample.android.js new file mode 100644 index 000000000..040ed00fd --- /dev/null +++ b/Examples/UIExplorer/ProgressBarAndroidExample.android.js @@ -0,0 +1,62 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var ProgressBar = require('ProgressBarAndroid'); +var React = require('React'); +var UIExplorerBlock = require('UIExplorerBlock'); +var UIExplorerPage = require('UIExplorerPage'); + +var ProgressBarAndroidExample = React.createClass({ + + statics: { + title: '', + description: 'Visual indicator of progress of some operation. ' + + 'Shows either a cyclic animation or a horizontal bar.', + }, + + render: function() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); + }, +}); + +module.exports = ProgressBarAndroidExample; diff --git a/Examples/UIExplorer/ScrollViewSimpleExample.js b/Examples/UIExplorer/ScrollViewSimpleExample.js index c9bbe7407..af7d6863b 100644 --- a/Examples/UIExplorer/ScrollViewSimpleExample.js +++ b/Examples/UIExplorer/ScrollViewSimpleExample.js @@ -30,6 +30,7 @@ var ScrollViewSimpleExample = React.createClass({ title: '', description: 'Component that enables scrolling through child components.' }, + makeItems: function(nItems: number, styles): Array { var items = []; for (var i = 0; i < nItems; i++) { diff --git a/Examples/UIExplorer/SwitchAndroidExample.android.js b/Examples/UIExplorer/SwitchAndroidExample.android.js new file mode 100644 index 000000000..ef58c5a36 --- /dev/null +++ b/Examples/UIExplorer/SwitchAndroidExample.android.js @@ -0,0 +1,80 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +'use strict'; + +var React = require('React'); + +var SwitchAndroid = require('SwitchAndroid'); +var Text = require('Text'); +var UIExplorerBlock = require('UIExplorerBlock'); +var UIExplorerPage = require('UIExplorerPage'); + +var SwitchAndroidExample = React.createClass({ + statics: { + title: '', + description: 'Standard Android two-state toggle component' + }, + + getInitialState : function() { + return { + trueSwitchIsOn: true, + falseSwitchIsOn: false, + colorTrueSwitchIsOn: true, + colorFalseSwitchIsOn: false, + eventSwitchIsOn: false, + }; + }, + + render: function() { + return ( + + + this.setState({falseSwitchIsOn: value})} + style={{marginBottom: 10}} + value={this.state.falseSwitchIsOn} /> + this.setState({trueSwitchIsOn: value})} + value={this.state.trueSwitchIsOn} /> + + + + + + + this.setState({eventSwitchIsOn: value})} + style={{marginBottom: 10}} + value={this.state.eventSwitchIsOn} /> + this.setState({eventSwitchIsOn: value})} + style={{marginBottom: 10}} + value={this.state.eventSwitchIsOn} /> + {this.state.eventSwitchIsOn ? "On" : "Off"} + + + + + + + ); + } +}); + +module.exports = SwitchAndroidExample; diff --git a/Examples/UIExplorer/TextExample.android.js b/Examples/UIExplorer/TextExample.android.js new file mode 100644 index 000000000..4159d0c18 --- /dev/null +++ b/Examples/UIExplorer/TextExample.android.js @@ -0,0 +1,349 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + Text, + View, +} = React; +var UIExplorerBlock = require('./UIExplorerBlock'); +var UIExplorerPage = require('./UIExplorerPage'); + +var Entity = React.createClass({ + render: function() { + return ( + + {this.props.children} + + ); + } +}); + +var AttributeToggler = React.createClass({ + getInitialState: function() { + return {fontWeight: 'bold', fontSize: 15}; + }, + toggleWeight: function() { + this.setState({ + fontWeight: this.state.fontWeight === 'bold' ? 'normal' : 'bold' + }); + }, + increaseSize: function() { + this.setState({ + fontSize: this.state.fontSize + 1 + }); + }, + render: function() { + var curStyle = {fontWeight: this.state.fontWeight, fontSize: this.state.fontSize}; + return ( + + + Tap the controls below to change attributes. + + + See how it will even work on this nested text + + + Toggle Weight + {' (with highlight onPress)'} + + + Increase Size (suppressHighlighting true) + + + ); + } +}); + +var TextExample = React.createClass({ + statics: { + title: '', + description: 'Base component for rendering styled text.', + }, + render: function() { + return ( + + + + The text should wrap if it goes on multiple lines. + See, this is going to the next line. + + + + + This text is indented by 10px padding on all sides. + + + + + Sans-Serif + + + Sans-Serif Bold + + + Serif + + + Serif Bold + + + Monospace + + + Monospace Bold (After 5.0) + + + + + + + Roboto Regular + + + Roboto Italic + + + Roboto Bold + + + Roboto Bold Italic + + + Roboto Light + + + Roboto Light Italic + + + Roboto Thin (After 4.2) + + + Roboto Thin Italic (After 4.2) + + + Roboto Condensed + + + Roboto Condensed Italic + + + Roboto Condensed Bold + + + Roboto Condensed Bold Italic + + + Roboto Medium (After 5.0) + + + Roboto Medium Italic (After 5.0) + + + + + + + Size 23 + + + Size 8 + + + + + Red color + + + Blue color + + + + + Move fast and be bold + + + Move fast and be bold + + + + + Move fast and be bold + + + Move fast and be bold + + + + + Move fast and be bold + + + + console.log('1st')}> + (Normal text, + console.log('2nd')}> + (and bold + console.log('3rd')}> + (and tiny bold italic blue + console.log('4th')}> + (and tiny normal blue) + + ) + + ) + + ) + + console.log('1st')}> + (Serif + console.log('2nd')}> + (Serif Bold Italic + console.log('3rd')}> + (Monospace Normal + console.log('4th')}> + (Sans-Serif Bold + console.log('5th')}> + (and Sans-Serif Normal) + + ) + + ) + + ) + + ) + + + Entity Name + + + + + auto (default) - english LTR + + + أحب اللغة العربية auto (default) - arabic RTL + + + left left left left left left left left left left left left left left left + + + center center center center center center center center center center center + + + right right right right right right right right right right right right right + + + + + + + 星际争霸是世界上最好的游戏。 + + + + + 星际争霸是世界上最好的游戏。 + + + + + 星际争霸是世界上最好的游戏。 + + + + + 星际争霸是世界上最好的游戏。星际争霸是世界上最好的游戏。星际争霸是世界上最好的游戏。星际争霸是世界上最好的游戏。 + + + + + + + A {'generated'} {' '} {'string'} and some     spaces + + + + + Holisticly formulate inexpensive ideas before best-of-breed benefits. Continually expedite magnetic potentialities rather than client-focused interfaces. + + + + + + + + + + + Red background, + + {' '}blue background, + + {' '}inherited blue background, + + {' '}nested green background. + + + + + + + + + + + + Default containerBackgroundColor (inherited) + backgroundColor wash + + + {"containerBackgroundColor: 'transparent' + backgroundColor wash"} + + + + + Maximum of one line no matter now much I write here. If I keep writing it{"'"}ll just truncate after one line + + + Maximum of two lines no matter now much I write here. If I keep writing it{"'"}ll just truncate after two lines + + + No maximum lines specified no matter now much I write here. If I keep writing it{"'"}ll just keep going and going + + + + ); + } +}); + +var styles = StyleSheet.create({ + backgroundColorText: { + left: 5, + backgroundColor: 'rgba(100, 100, 100, 0.3)' + }, +}); + +module.exports = TextExample; diff --git a/Examples/UIExplorer/TextInputExample.android.js b/Examples/UIExplorer/TextInputExample.android.js new file mode 100644 index 000000000..9659e9180 --- /dev/null +++ b/Examples/UIExplorer/TextInputExample.android.js @@ -0,0 +1,316 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Text, + TextInput, + View, + StyleSheet, +} = React; + +var TextEventsExample = React.createClass({ + getInitialState: function() { + return { + curText: '', + prevText: '', + prev2Text: '', + }; + }, + + updateText: function(text) { + this.setState((state) => { + return { + curText: text, + prevText: state.curText, + prev2Text: state.prevText, + }; + }); + }, + + render: function() { + return ( + + this.updateText('onFocus')} + onBlur={() => this.updateText('onBlur')} + onChange={(event) => this.updateText( + 'onChange text: ' + event.nativeEvent.text + )} + onEndEditing={(event) => this.updateText( + 'onEndEditing text: ' + event.nativeEvent.text + )} + onSubmitEditing={(event) => this.updateText( + 'onSubmitEditing text: ' + event.nativeEvent.text + )} + style={styles.singleLine} + /> + + {this.state.curText}{'\n'} + (prev: {this.state.prevText}){'\n'} + (prev2: {this.state.prev2Text}) + + + ); + } +}); + +class RewriteExample extends React.Component { + constructor(props) { + super(props); + this.state = {text: ''}; + } + render() { + return ( + { + text = text.replace(/ /g, '_'); + this.setState({text}); + }} + style={styles.singleLine} + value={this.state.text} + /> + ); + } +} + +var styles = StyleSheet.create({ + multiline: { + height: 60, + fontSize: 16, + padding: 4, + marginBottom: 10, + }, + eventLabel: { + margin: 3, + fontSize: 12, + }, + singleLine: { + fontSize: 16, + padding: 4, + }, + singleLineWithHeightTextInput: { + height: 30, + }, +}); + +exports.title = ''; +exports.description = 'Single and multi-line text inputs.'; +exports.examples = [ + { + title: 'Auto-focus', + render: function() { + return ; + } + }, + { + title: "Live Re-Write ( -> '_')", + render: function() { + return ; + } + }, + { + title: 'Auto-capitalize', + render: function() { + var autoCapitalizeTypes = [ + 'none', + 'sentences', + 'words', + 'characters', + ]; + var examples = autoCapitalizeTypes.map((type) => { + return ( + + ); + }); + return {examples}; + } + }, + { + title: 'Auto-correct', + render: function() { + return ( + + + + + ); + } + }, + { + title: 'Keyboard types', + render: function() { + var keyboardTypes = [ + 'default', + 'email-address', + 'numeric', + ]; + var examples = keyboardTypes.map((type) => { + return ( + + ); + }); + return {examples}; + } + }, + { + title: 'Event handling', + render: function(): ReactElement { return ; }, + }, + { + title: 'Colors and text inputs', + render: function() { + return ( + + + + + + + + + ); + } + }, + { + title: 'Text input, themes and heights', + render: function() { + return ( + + ); + } + }, + { + title: 'Passwords', + render: function() { + return ( + + ); + } + }, + { + title: 'Editable', + render: function() { + return ( + + ); + } + }, + { + title: 'Multiline', + render: function() { + return ( + + + + + multiline with children, aligned bottom-right + + + ); + } + }, + { + title: 'Fixed number of lines', + platform: 'android', + render: function() { + return ( + + + + + ); + } + }, +]; diff --git a/Examples/UIExplorer/ToastAndroidExample.android.js b/Examples/UIExplorer/ToastAndroidExample.android.js new file mode 100644 index 000000000..0e9964766 --- /dev/null +++ b/Examples/UIExplorer/ToastAndroidExample.android.js @@ -0,0 +1,68 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + Text, + ToastAndroid, + TouchableWithoutFeedback +} = React; + +var UIExplorerBlock = require('UIExplorerBlock'); +var UIExplorerPage = require('UIExplorerPage'); + +var ToastExample = React.createClass({ + + statics: { + title: 'Toast Example', + description: 'Toast Example', + }, + + getInitialState: function() { + return {}; + }, + + render: function() { + return ( + + + + ToastAndroid.show('This is a toast with short duration', ToastAndroid.SHORT)}> + Click me. + + + + + ToastAndroid.show('This is a toast with long duration', ToastAndroid.LONG)}> + Click me too. + + + + ); + }, +}); + +var styles = StyleSheet.create({ + text: { + color: 'black', + }, +}); + +module.exports = ToastExample; diff --git a/Examples/UIExplorer/ToolbarAndroidExample.android.js b/Examples/UIExplorer/ToolbarAndroidExample.android.js new file mode 100644 index 000000000..359ba087e --- /dev/null +++ b/Examples/UIExplorer/ToolbarAndroidExample.android.js @@ -0,0 +1,119 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + Text, + View, +} = React; +var UIExplorerBlock = require('./UIExplorerBlock'); +var UIExplorerPage = require('./UIExplorerPage'); + +var SwitchAndroid = require('SwitchAndroid'); +var ToolbarAndroid = require('ToolbarAndroid'); + +var ToolbarAndroidExample = React.createClass({ + statics: { + title: '', + description: 'Examples of using the Android toolbar.' + }, + getInitialState: function() { + return { + actionText: 'Example app with toolbar component', + toolbarSwitch: false, + colorProps: { + titleColor: '#3b5998', + subtitleColor: '#6a7180', + }, + }; + }, + render: function() { + return ( + + + this.setState({actionText: 'Icon clicked'})} + style={styles.toolbar} + subtitle={this.state.actionText} + title="Toolbar" /> + {this.state.actionText} + + + + + this.setState({'toolbarSwitch': value})} /> + {'\'Tis but a switch'} + + + + + + + + + + + this.setState({colorProps: {}})} + title="Wow, such toolbar" + style={styles.toolbar} + subtitle="Much native" + {...this.state.colorProps} /> + + Touch the icon to reset the custom colors to the default (theme-provided) ones. + + + + ); + }, + _onActionSelected: function(position) { + this.setState({ + actionText: 'Selected ' + toolbarActions[position].title, + }); + }, +}); + +var toolbarActions = [ + {title: 'Create', icon: require('image!ic_create_black_48dp'), show: 'always'}, + {title: 'Filter'}, + {title: 'Settings', icon: require('image!ic_settings_black_48dp'), show: 'always'}, +]; + +var styles = StyleSheet.create({ + toolbar: { + backgroundColor: '#e9eaed', + height: 56, + }, +}); + +module.exports = ToolbarAndroidExample; diff --git a/Examples/UIExplorer/UIExplorerApp.android.js b/Examples/UIExplorer/UIExplorerApp.android.js index 154796cef..c9bd2418f 100644 --- a/Examples/UIExplorer/UIExplorerApp.android.js +++ b/Examples/UIExplorer/UIExplorerApp.android.js @@ -18,34 +18,42 @@ var React = require('react-native'); var { + AppRegistry, + BackAndroid, Dimensions, + DrawerLayoutAndroid, StyleSheet, + ToolbarAndroid, View, } = React; -var UIExplorerList = require('./UIExplorerList'); - -// TODO: these should be exposed by the 'react-native' module. -var DrawerLayoutAndroid = require('DrawerLayoutAndroid'); -var ToolbarAndroid = require('ToolbarAndroid'); +var UIExplorerList = require('./UIExplorerList.android'); var DRAWER_WIDTH_LEFT = 56; var UIExplorerApp = React.createClass({ - getInitialState: function() { return { - example: { - title: 'UIExplorer', - component: this._renderHome(), - }, + example: this._getUIExplorerHome(), }; }, + _getUIExplorerHome: function() { + return { + title: 'UIExplorer', + component: this._renderHome(), + }; + }, + + componentWillMount: function() { + BackAndroid.addEventListener('hardwareBackPress', this._handleBackButtonPress); + }, + render: function() { return ( { this.drawer = drawer; }} renderNavigationView={this._renderNavigationView}> {this._renderNavigation()} @@ -64,14 +72,11 @@ var UIExplorerApp = React.createClass({ onSelectExample: function(example) { this.drawer.closeDrawer(); - if (example.title === 'UIExplorer') { - example.component = this._renderHome(); + if (example.title === this._getUIExplorerHome().title) { + example = this._getUIExplorerHome(); } this.setState({ - example: { - title: example.title, - component: example.component, - }, + example: example, }); }, @@ -105,16 +110,16 @@ var UIExplorerApp = React.createClass({ ); }, + _handleBackButtonPress: function() { + if (this.state.example.title !== this._getUIExplorerHome().title) { + this.onSelectExample(this._getUIExplorerHome()); + return true; + } + return false; + }, }); var styles = StyleSheet.create({ - messageText: { - fontSize: 17, - fontWeight: '500', - padding: 15, - marginTop: 50, - marginLeft: 15, - }, container: { flex: 1, }, @@ -124,4 +129,6 @@ var styles = StyleSheet.create({ }, }); +AppRegistry.registerComponent('UIExplorerApp', () => UIExplorerApp); + module.exports = UIExplorerApp; diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js new file mode 100644 index 000000000..d102d3de8 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -0,0 +1,99 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + View, +} = React; +var UIExplorerListBase = require('./UIExplorerListBase'); + +var COMPONENTS = [ + require('./ImageExample'), + require('./ProgressBarAndroidExample'), + require('./ScrollViewSimpleExample'), + require('./SwitchAndroidExample'), + require('./TextExample.android'), + require('./TextInputExample.android'), + require('./ToolbarAndroidExample'), + require('./TouchableExample'), + require('./ViewExample'), +]; + +var APIS = [ + require('./AccessibilityAndroidExample.android'), + require('./BorderExample'), + require('./LayoutEventsExample'), + require('./LayoutExample'), + require('./PanResponderExample'), + require('./PointerEventsExample'), + require('./TimerExample'), + require('./ToastAndroidExample.android'), + require('./XHRExample'), +]; + +type Props = { + onSelectExample: Function, + isInDrawer: bool, +}; + +class UIExplorerList extends React.Component { + props: Props; + + render() { + return ( + + ); + } + + renderAdditionalView(renderRow, renderTextInput): React.Component { + if (this.props.isInDrawer) { + var homePage = renderRow({ + title: 'UIExplorer', + description: 'List of examples', + }, -1); + return ( + + {homePage} + + ); + } + return renderTextInput(styles.searchTextInput); + } + + onPressRow(example: any) { + var Component = UIExplorerListBase.makeRenderable(example); + this.props.onSelectExample({ + title: Component.title, + component: Component, + }); + } +} + +var styles = StyleSheet.create({ + searchTextInput: { + padding: 2, + }, +}); + +module.exports = UIExplorerList; diff --git a/Examples/UIExplorer/XHRExample.android.js b/Examples/UIExplorer/XHRExample.android.js new file mode 100644 index 000000000..b0069794e --- /dev/null +++ b/Examples/UIExplorer/XHRExample.android.js @@ -0,0 +1,326 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + PixelRatio, + StyleSheet, + Text, + TextInput, + TouchableHighlight, + View, +} = React; + +// TODO t7093728 This is a simlified XHRExample.ios.js. +// Once we have Camera roll, Toast, Intent (for opening URLs) +// we should make this consistent with iOS. + +class Downloader extends React.Component { + + xhr: XMLHttpRequest; + cancelled: boolean; + + constructor(props) { + super(props); + this.cancelled = false; + this.state = { + status: '', + contentSize: 1, + downloaded: 0, + }; + } + + download() { + this.xhr && this.xhr.abort(); + + var xhr = this.xhr || new XMLHttpRequest(); + xhr.onreadystatechange = () => { + if (xhr.readyState === xhr.HEADERS_RECEIVED) { + var contentSize = parseInt(xhr.getResponseHeader('Content-Length'), 10); + this.setState({ + contentSize: contentSize, + downloaded: 0, + }); + } else if (xhr.readyState === xhr.LOADING) { + this.setState({ + downloaded: xhr.responseText.length, + }); + console.log(xhr.responseText.length); + } else if (xhr.readyState === xhr.DONE) { + if (this.cancelled) { + this.cancelled = false; + return; + } + if (xhr.status === 200) { + this.setState({ + status: 'Download complete!', + }); + } else if (xhr.status !== 0) { + this.setState({ + status: 'Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText, + }); + } else { + this.setState({ + status: 'Error: ' + xhr.responseText, + }); + } + } + }; + xhr.open('GET', 'http://www.gutenberg.org/cache/epub/100/pg100.txt'); + xhr.send(); + this.xhr = xhr; + + this.setState({status: 'Downloading...'}); + } + + componentWillUnmount() { + this.cancelled = true; + this.xhr && this.xhr.abort(); + } + + render() { + var button = this.state.status === 'Downloading...' ? ( + + + ... + + + ) : ( + + + Download 5MB Text File + + + ); + + return ( + + {button} + {this.state.status} + + ); + } +} + +class FormUploader extends React.Component { + + _isMounted: boolean; + _addTextParam: () => void; + _upload: () => void; + + constructor(props) { + super(props); + this.state = { + isUploading: false, + uploadProgress: null, + textParams: [], + }; + this._isMounted = true; + this._addTextParam = this._addTextParam.bind(this); + this._upload = this._upload.bind(this); + } + + _addTextParam() { + var textParams = this.state.textParams; + textParams.push({name: '', value: ''}); + this.setState({textParams}); + } + + componentWillUnmount() { + this._isMounted = false; + } + + _onTextParamNameChange(index, text) { + var textParams = this.state.textParams; + textParams[index].name = text; + this.setState({textParams}); + } + + _onTextParamValueChange(index, text) { + var textParams = this.state.textParams; + textParams[index].value = text; + this.setState({textParams}); + } + + _upload() { + var xhr = new XMLHttpRequest(); + xhr.open('POST', 'http://posttestserver.com/post.php'); + xhr.onload = () => { + this.setState({isUploading: false}); + if (xhr.status !== 200) { + console.log( + 'Upload failed', + 'Expected HTTP 200 OK response, got ' + xhr.status + ); + return; + } + if (!xhr.responseText) { + console.log( + 'Upload failed', + 'No response payload.' + ); + return; + } + var index = xhr.responseText.indexOf('http://www.posttestserver.com/'); + if (index === -1) { + console.log( + 'Upload failed', + 'Invalid response payload.' + ); + return; + } + var url = xhr.responseText.slice(index).split('\n')[0]; + console.log('Upload successful: ' + url); + }; + var formdata = new FormData(); + this.state.textParams.forEach( + (param) => formdata.append(param.name, param.value) + ); + if (xhr.upload) { + xhr.upload.onprogress = (event) => { + console.log('upload onprogress', event); + if (event.lengthComputable) { + this.setState({uploadProgress: event.loaded / event.total}); + } + }; + } + xhr.send(formdata); + this.setState({isUploading: true}); + } + + render() { + var textItems = this.state.textParams.map((item, index) => ( + + + = + + + )); + var uploadButtonLabel = this.state.isUploading ? 'Uploading...' : 'Upload'; + var uploadProgress = this.state.uploadProgress; + if (uploadProgress !== null) { + uploadButtonLabel += ' ' + Math.round(uploadProgress * 100) + '%'; + } + var uploadButton = ( + + {uploadButtonLabel} + + ); + if (!this.state.isUploading) { + uploadButton = ( + + {uploadButton} + + ); + } + return ( + + {textItems} + + + Add a text param + + + + {uploadButton} + + + ); + } +} + + +exports.framework = 'React'; +exports.title = 'XMLHttpRequest'; +exports.description = 'XMLHttpRequest'; +exports.examples = [{ + title: 'File Download', + render() { + return ; + } +}, { + title: 'multipart/form-data Upload', + render() { + return ; + } +}]; + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + backgroundColor: '#eeeeee', + padding: 8, + }, + paramRow: { + flexDirection: 'row', + paddingVertical: 8, + alignItems: 'center', + borderBottomWidth: 1 / PixelRatio.get(), + borderBottomColor: 'grey', + }, + textButton: { + color: 'blue', + }, + addTextParamButton: { + marginTop: 8, + }, + textInput: { + flex: 1, + borderRadius: 3, + borderColor: 'grey', + borderWidth: 1, + height: 30, + paddingLeft: 8, + }, + equalSign: { + paddingHorizontal: 4, + }, + uploadButton: { + marginTop: 16, + }, + uploadButtonBox: { + flex: 1, + paddingVertical: 12, + alignItems: 'center', + backgroundColor: 'blue', + borderRadius: 4, + }, + uploadButtonLabel: { + color: 'white', + fontSize: 16, + fontWeight: '500', + }, +}); diff --git a/Examples/UIExplorer/android/app/build.gradle b/Examples/UIExplorer/android/app/build.gradle new file mode 100644 index 000000000..e5fa61d8c --- /dev/null +++ b/Examples/UIExplorer/android/app/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + applicationId "com.facebook.react.uiapp" + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + ndk { + abiFilters "armeabi-v7a", "x86" + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:23.0.0' + + // Depend on pre-built React Native + compile 'com.facebook.react:react-native:0.11.+' + + // Depend on React Native source. + // This is useful for testing your changes when working on React Native. + // compile project(':ReactAndroid') +} diff --git a/Examples/UIExplorer/android/app/proguard-rules.pro b/Examples/UIExplorer/android/app/proguard-rules.pro new file mode 100644 index 000000000..a92fa177e --- /dev/null +++ b/Examples/UIExplorer/android/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml b/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..b69776f9a --- /dev/null +++ b/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java b/Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java new file mode 100644 index 000000000..04c8e258c --- /dev/null +++ b/Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java @@ -0,0 +1,89 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.facebook.react.uiapp; + +import android.app.Activity; +import android.os.Bundle; +import android.view.KeyEvent; + +import com.facebook.react.LifecycleState; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactRootView; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.shell.MainReactPackage; + +public class UIExplorerActivity extends Activity implements DefaultHardwareBackBtnHandler { + + private ReactInstanceManager mReactInstanceManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mReactInstanceManager = ReactInstanceManager.builder() + .setApplication(getApplication()) + .setBundleAssetName("UIExplorerApp.android.bundle") + .setJSMainModuleName("Examples/UIExplorer/UIExplorerApp.android") + .addPackage(new MainReactPackage()) + .setUseDeveloperSupport(true) + .setInitialLifecycleState(LifecycleState.RESUMED) + .build(); + + ((ReactRootView) findViewById(R.id.react_root_view)) + .startReactApplication(mReactInstanceManager, "UIExplorerApp", null); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) { + mReactInstanceManager.showDevOptionsDialog(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onPause() { + super.onPause(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onPause(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onResume(this); + } + } + + @Override + public void onBackPressed() { + if (mReactInstanceManager != null) { + mReactInstanceManager.onBackPressed(); + } else { + super.onBackPressed(); + } + } + + @Override + public void invokeDefaultOnBackPressed() { + super.onBackPressed(); + } +} diff --git a/Examples/UIExplorer/android/app/src/main/res/drawable/ic_create_black_48dp.png b/Examples/UIExplorer/android/app/src/main/res/drawable/ic_create_black_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..d3c4ccef2f5b4ff6ac5096459cef49f30fdc0812 GIT binary patch literal 406 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcg6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g&? zz`)4o>EaktaqI1s-MohkBwQcfZggpuXmriE(J;%iLe%k$tN(Ep1w&?0ArTLe6P_~W zKi3zn6aV4&BqFKD@R&sM-~6?!&)ab30lWW-wkZZh{uaj+&vyys74G{H`?;wleF=B)q=L@U`@7ZGEc(0du8^5v z{Omt1xmS)G=_~5k>nobL)I zeLB$2FK<`#86zx41X%4cpkd{J%TJX>IJF?hQAxvXHYbqTr( S^A7^GF?hQAxvX=P)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00d!4L_t(|+U;9eY)w%VU2WA&h-eKpHvJ>1ImX5mL4=SX zHAGF3_#`nmln{RK;X@=y1Rph~l&_){5o#u_QZcr~JQJNK!5-NRH2?BRAUleqRJ32Fj2387e@I%EyU=Q-;O0e6diOLNMjoq zrqjS2wt@LN4QyZ=*rd}yHrv1~agZ{o-(0JS{qN5+-4nDAQHHI+ zQJMm=NXHu|`Js4?$;32<7%QS~xJhmX_IM2a%vn5#X`hSyel+i^>O8EMatzKZkY9z1 zw(sqkD&&VE!$y8nJW*qy9L?kg)~k^pij7kFUGPPffv-rG891m)ekhJebQt0mdU$g8LXiXeWuAeaD6<^k7#bVdn;_Scy^QR$3-o*|`Mln=0b&Em+PUFx57p?v6XmLAHyzcd~b$ZZ0KF3*@l& z{jOr>6xMs;2%5WA#W7fA{JHb2m`xi+C6@cug1lfDYLOM|5(Dd5|5{s*3_}gHVjXN? zz2~2S&xWBA{!2UftzYtEjZ3Y*6>kIM8VC$bWag?UqL$kkrI0`;;~IwauiXq$VD?sl4a~$Ns;JW=A1>*m&+U^p3d6j>;?Ri zvM-)mmfg8jESkUllh#E&bDCapXnYh?vFQEflsK@T=NTw+YCcs`v6%9YjdPb{W9vH4 zz&syXi}_Tp45nGzfT9g04NGYbySElIxbNO8&O_DlSy`G&1ESN)Y<2R5=f$0jH9+l# z#fkk|RR%2XOf-qOcu{v@d24058Us^odvfPg$j`U02l^``er^D$tz2ep4!5uW&3 zD*c7FQE!8O!vTz>DK?RL;od8rV>~g9qT9URqPA8N;D}`rJHkq`RgFaKcapyst^cS( zGV#!ten*Y|S!sAS=3p#kN|8-kyTx7*r70YTaX22%SxaPf*uQi5YgAfip`*t$!;HP7 znwfo?XCAPQ&V~xzmai1EL3h|CD{}oh%U&me0X(0Q56(_X? z{-1!dbQ+k=HZVh{fgx-I{dF4P{}r({l;SzF$Eg+*7+3PkDIp9HAV7cs0er#_=WN)d Tv8ld300000NkvXXu0mjfLCH4e literal 0 HcmV?d00001 diff --git a/Examples/UIExplorer/android/app/src/main/res/drawable/launcher_icon.png b/Examples/UIExplorer/android/app/src/main/res/drawable/launcher_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a4aa3bd6d9acdc977bc945565d7722d84e0797c8 GIT binary patch literal 9578 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

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

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

    MTGfXx-r*jlFez~maRtD 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 literal 0 HcmV?d00001 diff --git a/ReactAndroid/release.gradle b/ReactAndroid/release.gradle new file mode 100644 index 000000000..9b8cd35a8 --- /dev/null +++ b/ReactAndroid/release.gradle @@ -0,0 +1,128 @@ +// Copyright 2015-present Facebook. All Rights Reserved. + +apply plugin: 'maven' +apply plugin: 'signing' + +// Gradle tasks for publishing to maven +// 1) To install in local maven repo use :installArchives task +// 2) To upload artifact to maven central use: :uploadArchives (you'd need to have the permission to do that) + +def isReleaseBuild() { + return VERSION_NAME.contains('SNAPSHOT') == false +} + +def getRepositoryUrl() { + return hasProperty('repositoryUrl') ? property('repositoryUrl') : 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' +} + +def getRepositoryUsername() { + return hasProperty('repositoryUsername') ? property('repositoryUsername') : '' +} + +def getRepositoryPassword() { + return hasProperty('repositoryPassword') ? property('repositoryPassword') : '' +} + +def configureReactNativePom(def pom) { + pom.project { + name POM_NAME + artifactId POM_ARTIFACT_ID + packaging POM_PACKAGING + description 'A framework for building native apps with React' + url 'https://github.com/facebook/react-native' + + scm { + url 'https://github.com/facebook/react-native.git' + connection 'scm:git:https://github.com/facebook/react-native.git' + developerConnection 'scm:git:git@github.com:facebook/react-native.git' + } + + licenses { + license { + name 'BSD License' + url 'https://github.com/facebook/react-native/blob/master/LICENSE' + distribution 'repo' + } + } + + developers { + developer { + id 'facebook' + name 'Facebook' + } + } + } +} + +afterEvaluate { project -> + + task androidJavadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += files(android.bootClasspath) + classpath += files(project.getConfigurations().getByName('compile').asList()) + include '**/*.java' + exclude '**/ReactBuildConfig.java' + } + + task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) { + classifier = 'javadoc' + from androidJavadoc.destinationDir + } + + task androidSourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.srcDirs + include '**/*.java' + } + + android.libraryVariants.all { variant -> + def name = variant.name.capitalize() + task "jar${name}"(type: Jar, dependsOn: variant.javaCompile) { + from variant.javaCompile.destinationDir + } + } + + artifacts { + archives androidSourcesJar + // TODO Make Javadoc generation work with Java 1.8, currently only works with 1.7 + // archives androidJavadocJar + } + + version = VERSION_NAME + group = GROUP + + signing { + required { isReleaseBuild() && gradle.taskGraph.hasTask('uploadArchives') } + sign configurations.archives + } + + uploadArchives { + configuration = configurations.archives + repositories.mavenDeployer { + beforeDeployment { + MavenDeployment deployment -> signing.signPom(deployment) + } + + repository(url: getRepositoryUrl()) { + authentication( + userName: getRepositoryUsername(), + password: getRepositoryPassword()) + + } + + configureReactNativePom pom + } + } + + task installArchives(type: Upload) { + configuration = configurations.archives + repositories.mavenDeployer { + beforeDeployment { + MavenDeployment deployment -> signing.signPom(deployment) + } + + repository url: "file://${System.properties['user.home']}/.m2/repository" + configureReactNativePom pom + } + } +} diff --git a/ReactAndroid/src/main/AndroidManifest.xml b/ReactAndroid/src/main/AndroidManifest.xml new file mode 100644 index 000000000..900169cc7 --- /dev/null +++ b/ReactAndroid/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSAlign.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSAlign.java new file mode 100644 index 000000000..7ca88e146 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSAlign.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<0a1e3b1f834f027e7a5bc5303f945b0e>> + +package com.facebook.csslayout; + +public enum CSSAlign { + AUTO, + FLEX_START, + CENTER, + FLEX_END, + STRETCH, +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSConstants.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSConstants.java new file mode 100644 index 000000000..f0441fc41 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSConstants.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<755069c4747cc9fc5624d70e5130e3d1>> + +package com.facebook.csslayout; + +public class CSSConstants { + + public static final float UNDEFINED = Float.NaN; + + public static boolean isUndefined(float value) { + return Float.compare(value, UNDEFINED) == 0; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSDirection.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSDirection.java new file mode 100644 index 000000000..361a6f264 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSDirection.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<5dc7f205706089599859188712b3bd8a>> + +package com.facebook.csslayout; + +public enum CSSDirection { + INHERIT, + LTR, + RTL, +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSFlexDirection.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSFlexDirection.java new file mode 100644 index 000000000..4a6a492e2 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSFlexDirection.java @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<6183a87290f3acd1caef7b6301bbf3a7>> + +package com.facebook.csslayout; + +public enum CSSFlexDirection { + COLUMN, + COLUMN_REVERSE, + ROW, + ROW_REVERSE +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSJustify.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSJustify.java new file mode 100644 index 000000000..bdfd6aa5a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSJustify.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<619fbefba1cfee797bbc7dd18e22f50c>> + +package com.facebook.csslayout; + +public enum CSSJustify { + FLEX_START, + CENTER, + FLEX_END, + SPACE_BETWEEN, + SPACE_AROUND, +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayout.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayout.java new file mode 100644 index 000000000..5d594868d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayout.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<153b6759d2dd8fe8cf6d58a422450b96>> + +package com.facebook.csslayout; + +/** + * Where the output of {@link LayoutEngine#layoutNode(CSSNode, float)} will go in the CSSNode. + */ +public class CSSLayout { + + public float top; + public float left; + public float right; + public float bottom; + public float width = CSSConstants.UNDEFINED; + public float height = CSSConstants.UNDEFINED; + public CSSDirection direction = CSSDirection.LTR; + + /** + * This should always get called before calling {@link LayoutEngine#layoutNode(CSSNode, float)} + */ + public void resetResult() { + left = 0; + top = 0; + right = 0; + bottom = 0; + width = CSSConstants.UNDEFINED; + height = CSSConstants.UNDEFINED; + direction = CSSDirection.LTR; + } + + public void copy(CSSLayout layout) { + left = layout.left; + top = layout.top; + right = layout.right; + bottom = layout.bottom; + width = layout.width; + height = layout.height; + direction = layout.direction; + } + + @Override + public String toString() { + return "layout: {" + + "left: " + left + ", " + + "top: " + top + ", " + + "width: " + width + ", " + + "height: " + height + + "direction: " + direction + + "}"; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayoutContext.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayoutContext.java new file mode 100644 index 000000000..574c0bdf5 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayoutContext.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<9d48f3d4330e7b6cba0fff7d8f1e8b0c>> + +package com.facebook.csslayout; + +/** + * A context for holding values local to a given instance of layout computation. + * + * This is necessary for making layout thread-safe. A separate instance should + * be used when {@link CSSNode#calculateLayout} is called concurrently on + * different node hierarchies. + */ +public class CSSLayoutContext { + /*package*/ final MeasureOutput measureOutput = new MeasureOutput(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java new file mode 100644 index 000000000..795295608 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java @@ -0,0 +1,396 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<> + +package com.facebook.csslayout; + +import javax.annotation.Nullable; + +import java.util.ArrayList; + +import com.facebook.infer.annotation.Assertions; + +/** + * A CSS Node. It has a style object you can manipulate at {@link #style}. After calling + * {@link #calculateLayout()}, {@link #layout} will be filled with the results of the layout. + */ +public class CSSNode { + + private static enum LayoutState { + /** + * Some property of this node or its children has changes and the current values in + * {@link #layout} are not valid. + */ + DIRTY, + + /** + * This node has a new layout relative to the last time {@link #markLayoutSeen()} was called. + */ + HAS_NEW_LAYOUT, + + /** + * {@link #layout} is valid for the node's properties and this layout has been marked as + * having been seen. + */ + UP_TO_DATE, + } + + public static interface MeasureFunction { + + /** + * Should measure the given node and put the result in the given MeasureOutput. + * + * NB: measure is NOT guaranteed to be threadsafe/re-entrant safe! + */ + public void measure(CSSNode node, float width, MeasureOutput measureOutput); + } + + // VisibleForTesting + /*package*/ final CSSStyle style = new CSSStyle(); + /*package*/ final CSSLayout layout = new CSSLayout(); + /*package*/ final CachedCSSLayout lastLayout = new CachedCSSLayout(); + + public int lineIndex = 0; + + private @Nullable ArrayList mChildren; + private @Nullable CSSNode mParent; + private @Nullable MeasureFunction mMeasureFunction = null; + private LayoutState mLayoutState = LayoutState.DIRTY; + + public int getChildCount() { + return mChildren == null ? 0 : mChildren.size(); + } + + public CSSNode getChildAt(int i) { + Assertions.assertNotNull(mChildren); + return mChildren.get(i); + } + + public void addChildAt(CSSNode child, int i) { + if (child.mParent != null) { + throw new IllegalStateException("Child already has a parent, it must be removed first."); + } + if (mChildren == null) { + // 4 is kinda arbitrary, but the default of 10 seems really high for an average View. + mChildren = new ArrayList<>(4); + } + + mChildren.add(i, child); + child.mParent = this; + dirty(); + } + + public CSSNode removeChildAt(int i) { + Assertions.assertNotNull(mChildren); + CSSNode removed = mChildren.remove(i); + removed.mParent = null; + dirty(); + return removed; + } + + public @Nullable CSSNode getParent() { + return mParent; + } + + /** + * @return the index of the given child, or -1 if the child doesn't exist in this node. + */ + public int indexOf(CSSNode child) { + Assertions.assertNotNull(mChildren); + return mChildren.indexOf(child); + } + + public void setMeasureFunction(MeasureFunction measureFunction) { + if (!valuesEqual(mMeasureFunction, measureFunction)) { + mMeasureFunction = measureFunction; + dirty(); + } + } + + public boolean isMeasureDefined() { + return mMeasureFunction != null; + } + + /*package*/ MeasureOutput measure(MeasureOutput measureOutput, float width) { + if (!isMeasureDefined()) { + throw new RuntimeException("Measure function isn't defined!"); + } + measureOutput.height = CSSConstants.UNDEFINED; + measureOutput.width = CSSConstants.UNDEFINED; + Assertions.assertNotNull(mMeasureFunction).measure(this, width, measureOutput); + return measureOutput; + } + + /** + * Performs the actual layout and saves the results in {@link #layout} + */ + public void calculateLayout(CSSLayoutContext layoutContext) { + layout.resetResult(); + LayoutEngine.layoutNode(layoutContext, this, CSSConstants.UNDEFINED, null); + } + + /** + * See {@link LayoutState#DIRTY}. + */ + protected boolean isDirty() { + return mLayoutState == LayoutState.DIRTY; + } + + /** + * See {@link LayoutState#HAS_NEW_LAYOUT}. + */ + public boolean hasNewLayout() { + return mLayoutState == LayoutState.HAS_NEW_LAYOUT; + } + + protected void dirty() { + if (mLayoutState == LayoutState.DIRTY) { + return; + } else if (mLayoutState == LayoutState.HAS_NEW_LAYOUT) { + throw new IllegalStateException("Previous layout was ignored! markLayoutSeen() never called"); + } + + mLayoutState = LayoutState.DIRTY; + + if (mParent != null) { + mParent.dirty(); + } + } + + /*package*/ void markHasNewLayout() { + mLayoutState = LayoutState.HAS_NEW_LAYOUT; + } + + /** + * Tells the node that the current values in {@link #layout} have been seen. Subsequent calls + * to {@link #hasNewLayout()} will return false until this node is laid out with new parameters. + * You must call this each time the layout is generated if the node has a new layout. + */ + public void markLayoutSeen() { + if (!hasNewLayout()) { + throw new IllegalStateException("Expected node to have a new layout to be seen!"); + } + + mLayoutState = LayoutState.UP_TO_DATE; + } + + private void toStringWithIndentation(StringBuilder result, int level) { + // Spaces and tabs are dropped by IntelliJ logcat integration, so rely on __ instead. + StringBuilder indentation = new StringBuilder(); + for (int i = 0; i < level; ++i) { + indentation.append("__"); + } + + result.append(indentation.toString()); + result.append(layout.toString()); + + if (getChildCount() == 0) { + return; + } + + result.append(", children: [\n"); + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).toStringWithIndentation(result, level + 1); + result.append("\n"); + } + result.append(indentation + "]"); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + this.toStringWithIndentation(sb, 0); + return sb.toString(); + } + + protected boolean valuesEqual(float f1, float f2) { + return FloatUtil.floatsEqual(f1, f2); + } + + protected boolean valuesEqual(@Nullable T o1, @Nullable T o2) { + if (o1 == null) { + return o2 == null; + } + return o1.equals(o2); + } + + public void setDirection(CSSDirection direction) { + if (!valuesEqual(style.direction, direction)) { + style.direction = direction; + dirty(); + } + } + + public void setFlexDirection(CSSFlexDirection flexDirection) { + if (!valuesEqual(style.flexDirection, flexDirection)) { + style.flexDirection = flexDirection; + dirty(); + } + } + + public void setJustifyContent(CSSJustify justifyContent) { + if (!valuesEqual(style.justifyContent, justifyContent)) { + style.justifyContent = justifyContent; + dirty(); + } + } + + public void setAlignItems(CSSAlign alignItems) { + if (!valuesEqual(style.alignItems, alignItems)) { + style.alignItems = alignItems; + dirty(); + } + } + + public void setAlignSelf(CSSAlign alignSelf) { + if (!valuesEqual(style.alignSelf, alignSelf)) { + style.alignSelf = alignSelf; + dirty(); + } + } + + public void setPositionType(CSSPositionType positionType) { + if (!valuesEqual(style.positionType, positionType)) { + style.positionType = positionType; + dirty(); + } + } + + public void setWrap(CSSWrap flexWrap) { + if (!valuesEqual(style.flexWrap, flexWrap)) { + style.flexWrap = flexWrap; + dirty(); + } + } + + public void setFlex(float flex) { + if (!valuesEqual(style.flex, flex)) { + style.flex = flex; + dirty(); + } + } + + public void setMargin(int spacingType, float margin) { + if (style.margin.set(spacingType, margin)) { + dirty(); + } + } + + public void setPadding(int spacingType, float padding) { + if (style.padding.set(spacingType, padding)) { + dirty(); + } + } + + public void setBorder(int spacingType, float border) { + if (style.border.set(spacingType, border)) { + dirty(); + } + } + + public void setPositionTop(float positionTop) { + if (!valuesEqual(style.positionTop, positionTop)) { + style.positionTop = positionTop; + dirty(); + } + } + + public void setPositionBottom(float positionBottom) { + if (!valuesEqual(style.positionBottom, positionBottom)) { + style.positionBottom = positionBottom; + dirty(); + } + } + + public void setPositionLeft(float positionLeft) { + if (!valuesEqual(style.positionLeft, positionLeft)) { + style.positionLeft = positionLeft; + dirty(); + } + } + + public void setPositionRight(float positionRight) { + if (!valuesEqual(style.positionRight, positionRight)) { + style.positionRight = positionRight; + dirty(); + } + } + + public void setStyleWidth(float width) { + if (!valuesEqual(style.width, width)) { + style.width = width; + dirty(); + } + } + + public void setStyleHeight(float height) { + if (!valuesEqual(style.height, height)) { + style.height = height; + dirty(); + } + } + + public float getLayoutX() { + return layout.left; + } + + public float getLayoutY() { + return layout.top; + } + + public float getLayoutWidth() { + return layout.width; + } + + public float getLayoutHeight() { + return layout.height; + } + + public CSSDirection getLayoutDirection() { + return layout.direction; + } + + /** + * Get this node's padding, as defined by style + default padding. + */ + public Spacing getStylePadding() { + return style.padding; + } + + /** + * Get this node's width, as defined in the style. + */ + public float getStyleWidth() { + return style.width; + } + + /** + * Get this node's height, as defined in the style. + */ + public float getStyleHeight() { + return style.height; + } + + /** + * Get this node's direction, as defined in the style. + */ + public CSSDirection getStyleDirection() { + return style.direction; + } + + /** + * Set a default padding (left/top/right/bottom) for this node. + */ + public void setDefaultPadding(int spacingType, float padding) { + if (style.padding.setDefault(spacingType, padding)) { + dirty(); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSPositionType.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSPositionType.java new file mode 100644 index 000000000..96fba2f5d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSPositionType.java @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<> + +package com.facebook.csslayout; + +public enum CSSPositionType { + RELATIVE, + ABSOLUTE, +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSStyle.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSStyle.java new file mode 100644 index 000000000..a25afaf67 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSStyle.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<> + +package com.facebook.csslayout; + +/** + * The CSS style definition for a {@link CSSNode}. + */ +public class CSSStyle { + + public CSSDirection direction = CSSDirection.INHERIT; + public CSSFlexDirection flexDirection = CSSFlexDirection.COLUMN; + public CSSJustify justifyContent = CSSJustify.FLEX_START; + public CSSAlign alignContent = CSSAlign.FLEX_START; + public CSSAlign alignItems = CSSAlign.STRETCH; + public CSSAlign alignSelf = CSSAlign.AUTO; + public CSSPositionType positionType = CSSPositionType.RELATIVE; + public CSSWrap flexWrap = CSSWrap.NOWRAP; + public float flex; + + public Spacing margin = new Spacing(); + public Spacing padding = new Spacing(); + public Spacing border = new Spacing(); + + public float positionTop = CSSConstants.UNDEFINED; + public float positionBottom = CSSConstants.UNDEFINED; + public float positionLeft = CSSConstants.UNDEFINED; + public float positionRight = CSSConstants.UNDEFINED; + + public float width = CSSConstants.UNDEFINED; + public float height = CSSConstants.UNDEFINED; + + public float minWidth = CSSConstants.UNDEFINED; + public float minHeight = CSSConstants.UNDEFINED; + + public float maxWidth = CSSConstants.UNDEFINED; + public float maxHeight = CSSConstants.UNDEFINED; +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSWrap.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSWrap.java new file mode 100644 index 000000000..476d907c7 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSWrap.java @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<21dab9bd1acf5892ad09370b69b7dd71>> + +package com.facebook.csslayout; + +public enum CSSWrap { + NOWRAP, + WRAP, +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CachedCSSLayout.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CachedCSSLayout.java new file mode 100644 index 000000000..97ef4886f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CachedCSSLayout.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<8276834951a75286a0b6d4a980bc43ce>> + +package com.facebook.csslayout; + +/** + * CSSLayout with additional information about the conditions under which it was generated. + * {@link #requestedWidth} and {@link #requestedHeight} are the width and height the parent set on + * this node before calling layout visited us. + */ +public class CachedCSSLayout extends CSSLayout { + + public float requestedWidth = CSSConstants.UNDEFINED; + public float requestedHeight = CSSConstants.UNDEFINED; + public float parentMaxWidth = CSSConstants.UNDEFINED; +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/FloatUtil.java b/ReactAndroid/src/main/java/com/facebook/csslayout/FloatUtil.java new file mode 100644 index 000000000..420a679ca --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/FloatUtil.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<> + +package com.facebook.csslayout; + +public class FloatUtil { + + private static final float EPSILON = .00001f; + + public static boolean floatsEqual(float f1, float f2) { + if (Float.isNaN(f1) || Float.isNaN(f2)) { + return Float.isNaN(f1) && Float.isNaN(f2); + } + return Math.abs(f2 - f1) < EPSILON; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java b/ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java new file mode 100644 index 000000000..a3646c840 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java @@ -0,0 +1,1100 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<4795d7e8efc1dbbaadfe117105c8991a>> + +package com.facebook.csslayout; + +/** + * Calculates layouts based on CSS style. See {@link #layoutNode(CSSNode, float)}. + */ +public class LayoutEngine { + + private static enum PositionIndex { + TOP, + LEFT, + BOTTOM, + RIGHT, + START, + END, + } + + private static enum DimensionIndex { + WIDTH, + HEIGHT, + } + + private static void setLayoutPosition(CSSNode node, PositionIndex position, float value) { + switch (position) { + case TOP: + node.layout.top = value; + break; + case LEFT: + node.layout.left = value; + break; + case RIGHT: + node.layout.right = value; + break; + case BOTTOM: + node.layout.bottom = value; + break; + default: + throw new RuntimeException("Didn't get TOP, LEFT, RIGHT, or BOTTOM!"); + } + } + + private static float getLayoutPosition(CSSNode node, PositionIndex position) { + switch (position) { + case TOP: + return node.layout.top; + case LEFT: + return node.layout.left; + case RIGHT: + return node.layout.right; + case BOTTOM: + return node.layout.bottom; + default: + throw new RuntimeException("Didn't get TOP, LEFT, RIGHT, or BOTTOM!"); + } + } + + private static void setLayoutDimension(CSSNode node, DimensionIndex dimension, float value) { + switch (dimension) { + case WIDTH: + node.layout.width = value; + break; + case HEIGHT: + node.layout.height = value; + break; + default: + throw new RuntimeException("Someone added a third dimension..."); + } + } + + private static float getLayoutDimension(CSSNode node, DimensionIndex dimension) { + switch (dimension) { + case WIDTH: + return node.layout.width; + case HEIGHT: + return node.layout.height; + default: + throw new RuntimeException("Someone added a third dimension..."); + } + } + + private static void setLayoutDirection(CSSNode node, CSSDirection direction) { + node.layout.direction = direction; + } + + private static float getStylePosition(CSSNode node, PositionIndex position) { + switch (position) { + case TOP: + return node.style.positionTop; + case BOTTOM: + return node.style.positionBottom; + case LEFT: + return node.style.positionLeft; + case RIGHT: + return node.style.positionRight; + default: + throw new RuntimeException("Someone added a new cardinal direction..."); + } + } + + private static float getStyleDimension(CSSNode node, DimensionIndex dimension) { + switch (dimension) { + case WIDTH: + return node.style.width; + case HEIGHT: + return node.style.height; + default: + throw new RuntimeException("Someone added a third dimension..."); + } + } + + private static PositionIndex getLeading(CSSFlexDirection axis) { + switch (axis) { + case COLUMN: + return PositionIndex.TOP; + case COLUMN_REVERSE: + return PositionIndex.BOTTOM; + case ROW: + return PositionIndex.LEFT; + case ROW_REVERSE: + return PositionIndex.RIGHT; + default: + throw new RuntimeException("Didn't get TOP, LEFT, RIGHT, or BOTTOM!"); + } + } + + private static PositionIndex getTrailing(CSSFlexDirection axis) { + switch (axis) { + case COLUMN: + return PositionIndex.BOTTOM; + case COLUMN_REVERSE: + return PositionIndex.TOP; + case ROW: + return PositionIndex.RIGHT; + case ROW_REVERSE: + return PositionIndex.LEFT; + default: + throw new RuntimeException("Didn't get COLUMN, COLUMN_REVERSE, ROW, or ROW_REVERSE!"); + } + } + + private static PositionIndex getPos(CSSFlexDirection axis) { + switch (axis) { + case COLUMN: + return PositionIndex.TOP; + case COLUMN_REVERSE: + return PositionIndex.BOTTOM; + case ROW: + return PositionIndex.LEFT; + case ROW_REVERSE: + return PositionIndex.RIGHT; + default: + throw new RuntimeException("Didn't get COLUMN, COLUMN_REVERSE, ROW, or ROW_REVERSE!"); + } + } + + private static DimensionIndex getDim(CSSFlexDirection axis) { + switch (axis) { + case COLUMN: + case COLUMN_REVERSE: + return DimensionIndex.HEIGHT; + case ROW: + case ROW_REVERSE: + return DimensionIndex.WIDTH; + default: + throw new RuntimeException("Didn't get COLUMN, COLUMN_REVERSE, ROW, or ROW_REVERSE!"); + } + } + + private static boolean isDimDefined(CSSNode node, CSSFlexDirection axis) { + float value = getStyleDimension(node, getDim(axis)); + return !CSSConstants.isUndefined(value) && value > 0.0; + } + + private static boolean isPosDefined(CSSNode node, PositionIndex position) { + return !CSSConstants.isUndefined(getStylePosition(node, position)); + } + + private static float getPosition(CSSNode node, PositionIndex position) { + float result = getStylePosition(node, position); + return CSSConstants.isUndefined(result) ? 0 : result; + } + + private static float getMargin(CSSNode node, PositionIndex position) { + switch (position) { + case TOP: + return node.style.margin.get(Spacing.TOP); + case BOTTOM: + return node.style.margin.get(Spacing.BOTTOM); + case LEFT: + return node.style.margin.get(Spacing.LEFT); + case RIGHT: + return node.style.margin.get(Spacing.RIGHT); + case START: + return node.style.margin.get(Spacing.START); + case END: + return node.style.margin.get(Spacing.END); + default: + throw new RuntimeException("Someone added a new cardinal direction..."); + } + } + + private static float getLeadingMargin(CSSNode node, CSSFlexDirection axis) { + if (isRowDirection(axis)) { + float leadingMargin = node.style.margin.getRaw(Spacing.START); + if (!CSSConstants.isUndefined(leadingMargin)) { + return leadingMargin; + } + } + + return getMargin(node, getLeading(axis)); + } + + private static float getTrailingMargin(CSSNode node, CSSFlexDirection axis) { + if (isRowDirection(axis)) { + float trailingMargin = node.style.margin.getRaw(Spacing.END); + if (!CSSConstants.isUndefined(trailingMargin)) { + return trailingMargin; + } + } + + return getMargin(node, getTrailing(axis)); + } + + private static float getPadding(CSSNode node, PositionIndex position) { + switch (position) { + case TOP: + return node.style.padding.get(Spacing.TOP); + case BOTTOM: + return node.style.padding.get(Spacing.BOTTOM); + case LEFT: + return node.style.padding.get(Spacing.LEFT); + case RIGHT: + return node.style.padding.get(Spacing.RIGHT); + case START: + return node.style.padding.get(Spacing.START); + case END: + return node.style.padding.get(Spacing.END); + default: + throw new RuntimeException("Someone added a new cardinal direction..."); + } + } + + private static float getLeadingPadding(CSSNode node, CSSFlexDirection axis) { + if (isRowDirection(axis)) { + float leadingPadding = node.style.padding.getRaw(Spacing.START); + if (!CSSConstants.isUndefined(leadingPadding)) { + return leadingPadding; + } + } + + return getPadding(node, getLeading(axis)); + } + + private static float getTrailingPadding(CSSNode node, CSSFlexDirection axis) { + if (isRowDirection(axis)) { + float trailingPadding = node.style.padding.getRaw(Spacing.END); + if (!CSSConstants.isUndefined(trailingPadding)) { + return trailingPadding; + } + } + + return getPadding(node, getTrailing(axis)); + } + + private static float getBorder(CSSNode node, PositionIndex position) { + switch (position) { + case TOP: + return node.style.border.get(Spacing.TOP); + case BOTTOM: + return node.style.border.get(Spacing.BOTTOM); + case LEFT: + return node.style.border.get(Spacing.LEFT); + case RIGHT: + return node.style.border.get(Spacing.RIGHT); + case START: + return node.style.border.get(Spacing.START); + case END: + return node.style.border.get(Spacing.END); + default: + throw new RuntimeException("Someone added a new cardinal direction..."); + } + } + + private static float getLeadingBorder(CSSNode node, CSSFlexDirection axis) { + if (isRowDirection(axis)) { + float leadingBorder = node.style.border.getRaw(Spacing.START); + if (!CSSConstants.isUndefined(leadingBorder)) { + return leadingBorder; + } + } + + return getBorder(node, getLeading(axis)); + } + + private static float getTrailingBorder(CSSNode node, CSSFlexDirection axis) { + if (isRowDirection(axis)) { + float trailingBorder = node.style.border.getRaw(Spacing.END); + if (!CSSConstants.isUndefined(trailingBorder)) { + return trailingBorder; + } + } + + return getBorder(node, getTrailing(axis)); + } + + private static float getLeadingPaddingAndBorder(CSSNode node, CSSFlexDirection axis) { + return getLeadingPadding(node, axis) + getLeadingBorder(node, axis); + } + + private static float getTrailingPaddingAndBorder(CSSNode node, CSSFlexDirection axis) { + return getTrailingPadding(node, axis) + getTrailingBorder(node, axis); + } + + private static float getBorderAxis(CSSNode node, CSSFlexDirection axis) { + return getLeadingBorder(node, axis) + getTrailingBorder(node, axis); + } + + private static float getMarginAxis(CSSNode node, CSSFlexDirection axis) { + return getLeadingMargin(node, axis) + getTrailingMargin(node, axis); + } + + private static float getPaddingAndBorderAxis(CSSNode node, CSSFlexDirection axis) { + return getLeadingPaddingAndBorder(node, axis) + getTrailingPaddingAndBorder(node, axis); + } + + private static float boundAxis(CSSNode node, CSSFlexDirection axis, float value) { + float min = CSSConstants.UNDEFINED; + float max = CSSConstants.UNDEFINED; + + if (isColumnDirection(axis)) { + min = node.style.minHeight; + max = node.style.maxHeight; + } else if (isRowDirection(axis)) { + min = node.style.minWidth; + max = node.style.maxWidth; + } + + float boundValue = value; + + if (!CSSConstants.isUndefined(max) && max >= 0.0 && boundValue > max) { + boundValue = max; + } + if (!CSSConstants.isUndefined(min) && min >= 0.0 && boundValue < min) { + boundValue = min; + } + + return boundValue; + } + + private static void setDimensionFromStyle(CSSNode node, CSSFlexDirection axis) { + // The parent already computed us a width or height. We just skip it + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(axis)))) { + return; + } + // We only run if there's a width or height defined + if (!isDimDefined(node, axis)) { + return; + } + + // The dimensions can never be smaller than the padding and border + float maxLayoutDimension = Math.max( + boundAxis(node, axis, getStyleDimension(node, getDim(axis))), + getPaddingAndBorderAxis(node, axis)); + setLayoutDimension(node, getDim(axis), maxLayoutDimension); + } + + private static void setTrailingPosition( + CSSNode node, + CSSNode child, + CSSFlexDirection axis) { + setLayoutPosition( + child, + getTrailing(axis), + getLayoutDimension(node, getDim(axis)) - + getLayoutDimension(child, getDim(axis)) - + getLayoutPosition(child, getPos(axis))); + } + + private static float getRelativePosition(CSSNode node, CSSFlexDirection axis) { + float lead = getStylePosition(node, getLeading(axis)); + if (!CSSConstants.isUndefined(lead)) { + return lead; + } + return -getPosition(node, getTrailing(axis)); + } + + private static float getFlex(CSSNode node) { + return node.style.flex; + } + + private static boolean isRowDirection(CSSFlexDirection flexDirection) { + return flexDirection == CSSFlexDirection.ROW || + flexDirection == CSSFlexDirection.ROW_REVERSE; + } + + private static boolean isColumnDirection(CSSFlexDirection flexDirection) { + return flexDirection == CSSFlexDirection.COLUMN || + flexDirection == CSSFlexDirection.COLUMN_REVERSE; + } + + private static CSSFlexDirection resolveAxis( + CSSFlexDirection axis, + CSSDirection direction) { + if (direction == CSSDirection.RTL) { + if (axis == CSSFlexDirection.ROW) { + return CSSFlexDirection.ROW_REVERSE; + } else if (axis == CSSFlexDirection.ROW_REVERSE) { + return CSSFlexDirection.ROW; + } + } + + return axis; + } + + private static CSSDirection resolveDirection(CSSNode node, CSSDirection parentDirection) { + CSSDirection direction = node.style.direction; + if (direction == CSSDirection.INHERIT) { + direction = (parentDirection == null ? CSSDirection.LTR : parentDirection); + } + + return direction; + } + + private static CSSFlexDirection getFlexDirection(CSSNode node) { + return node.style.flexDirection; + } + + private static CSSFlexDirection getCrossFlexDirection( + CSSFlexDirection flexDirection, + CSSDirection direction) { + if (isColumnDirection(flexDirection)) { + return resolveAxis(CSSFlexDirection.ROW, direction); + } else { + return CSSFlexDirection.COLUMN; + } + } + + private static CSSPositionType getPositionType(CSSNode node) { + return node.style.positionType; + } + + private static CSSAlign getAlignItem(CSSNode node, CSSNode child) { + if (child.style.alignSelf != CSSAlign.AUTO) { + return child.style.alignSelf; + } + return node.style.alignItems; + } + + private static CSSAlign getAlignContent(CSSNode node) { + return node.style.alignContent; + } + + private static CSSJustify getJustifyContent(CSSNode node) { + return node.style.justifyContent; + } + + private static boolean isFlexWrap(CSSNode node) { + return node.style.flexWrap == CSSWrap.WRAP; + } + + private static boolean isFlex(CSSNode node) { + return getPositionType(node) == CSSPositionType.RELATIVE && getFlex(node) > 0; + } + + private static boolean isMeasureDefined(CSSNode node) { + return node.isMeasureDefined(); + } + + private static float getDimWithMargin(CSSNode node, CSSFlexDirection axis) { + return getLayoutDimension(node, getDim(axis)) + + getLeadingMargin(node, axis) + + getTrailingMargin(node, axis); + } + + private static boolean needsRelayout(CSSNode node, float parentMaxWidth) { + return node.isDirty() || + !FloatUtil.floatsEqual(node.lastLayout.requestedHeight, node.layout.height) || + !FloatUtil.floatsEqual(node.lastLayout.requestedWidth, node.layout.width) || + !FloatUtil.floatsEqual(node.lastLayout.parentMaxWidth, parentMaxWidth); + } + + /*package*/ static void layoutNode( + CSSLayoutContext layoutContext, + CSSNode node, + float parentMaxWidth, + CSSDirection parentDirection) { + if (needsRelayout(node, parentMaxWidth)) { + node.lastLayout.requestedWidth = node.layout.width; + node.lastLayout.requestedHeight = node.layout.height; + node.lastLayout.parentMaxWidth = parentMaxWidth; + + layoutNodeImpl(layoutContext, node, parentMaxWidth, parentDirection); + node.lastLayout.copy(node.layout); + } else { + node.layout.copy(node.lastLayout); + } + + node.markHasNewLayout(); + } + + private static void layoutNodeImpl( + CSSLayoutContext layoutContext, + CSSNode node, + float parentMaxWidth, + CSSDirection parentDirection) { + for (int i = 0; i < node.getChildCount(); i++) { + node.getChildAt(i).layout.resetResult(); + } + + /** START_GENERATED **/ + + CSSDirection direction = resolveDirection(node, parentDirection); + CSSFlexDirection mainAxis = resolveAxis(getFlexDirection(node), direction); + CSSFlexDirection crossAxis = getCrossFlexDirection(mainAxis, direction); + CSSFlexDirection resolvedRowAxis = resolveAxis(CSSFlexDirection.ROW, direction); + + // Handle width and height style attributes + setDimensionFromStyle(node, mainAxis); + setDimensionFromStyle(node, crossAxis); + + // Set the resolved resolution in the node's layout + setLayoutDirection(node, direction); + + // The position is set by the parent, but we need to complete it with a + // delta composed of the margin and left/top/right/bottom + setLayoutPosition(node, getLeading(mainAxis), getLayoutPosition(node, getLeading(mainAxis)) + getLeadingMargin(node, mainAxis) + + getRelativePosition(node, mainAxis)); + setLayoutPosition(node, getTrailing(mainAxis), getLayoutPosition(node, getTrailing(mainAxis)) + getTrailingMargin(node, mainAxis) + + getRelativePosition(node, mainAxis)); + setLayoutPosition(node, getLeading(crossAxis), getLayoutPosition(node, getLeading(crossAxis)) + getLeadingMargin(node, crossAxis) + + getRelativePosition(node, crossAxis)); + setLayoutPosition(node, getTrailing(crossAxis), getLayoutPosition(node, getTrailing(crossAxis)) + getTrailingMargin(node, crossAxis) + + getRelativePosition(node, crossAxis)); + + if (isMeasureDefined(node)) { + float width = CSSConstants.UNDEFINED; + if (isDimDefined(node, resolvedRowAxis)) { + width = node.style.width; + } else if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(resolvedRowAxis)))) { + width = getLayoutDimension(node, getDim(resolvedRowAxis)); + } else { + width = parentMaxWidth - + getMarginAxis(node, resolvedRowAxis); + } + width -= getPaddingAndBorderAxis(node, resolvedRowAxis); + + // We only need to give a dimension for the text if we haven't got any + // for it computed yet. It can either be from the style attribute or because + // the element is flexible. + boolean isRowUndefined = !isDimDefined(node, resolvedRowAxis) && + CSSConstants.isUndefined(getLayoutDimension(node, getDim(resolvedRowAxis))); + boolean isColumnUndefined = !isDimDefined(node, CSSFlexDirection.COLUMN) && + CSSConstants.isUndefined(getLayoutDimension(node, getDim(CSSFlexDirection.COLUMN))); + + // Let's not measure the text if we already know both dimensions + if (isRowUndefined || isColumnUndefined) { + MeasureOutput measureDim = node.measure( + layoutContext.measureOutput, + width + ); + if (isRowUndefined) { + node.layout.width = measureDim.width + + getPaddingAndBorderAxis(node, resolvedRowAxis); + } + if (isColumnUndefined) { + node.layout.height = measureDim.height + + getPaddingAndBorderAxis(node, CSSFlexDirection.COLUMN); + } + } + if (node.getChildCount() == 0) { + return; + } + } + + int i; + int ii; + CSSNode child; + CSSFlexDirection axis; + + // Pre-fill some dimensions straight from the parent + for (i = 0; i < node.getChildCount(); ++i) { + child = node.getChildAt(i); + // Pre-fill cross axis dimensions when the child is using stretch before + // we call the recursive layout pass + if (getAlignItem(node, child) == CSSAlign.STRETCH && + getPositionType(child) == CSSPositionType.RELATIVE && + !CSSConstants.isUndefined(getLayoutDimension(node, getDim(crossAxis))) && + !isDimDefined(child, crossAxis)) { + setLayoutDimension(child, getDim(crossAxis), Math.max( + boundAxis(child, crossAxis, getLayoutDimension(node, getDim(crossAxis)) - + getPaddingAndBorderAxis(node, crossAxis) - + getMarginAxis(child, crossAxis)), + // You never want to go smaller than padding + getPaddingAndBorderAxis(child, crossAxis) + )); + } else if (getPositionType(child) == CSSPositionType.ABSOLUTE) { + // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both + // left and right or top and bottom). + for (ii = 0; ii < 2; ii++) { + axis = (ii != 0) ? CSSFlexDirection.ROW : CSSFlexDirection.COLUMN; + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(axis))) && + !isDimDefined(child, axis) && + isPosDefined(child, getLeading(axis)) && + isPosDefined(child, getTrailing(axis))) { + setLayoutDimension(child, getDim(axis), Math.max( + boundAxis(child, axis, getLayoutDimension(node, getDim(axis)) - + getPaddingAndBorderAxis(node, axis) - + getMarginAxis(child, axis) - + getPosition(child, getLeading(axis)) - + getPosition(child, getTrailing(axis))), + // You never want to go smaller than padding + getPaddingAndBorderAxis(child, axis) + )); + } + } + } + } + + float definedMainDim = CSSConstants.UNDEFINED; + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(mainAxis)))) { + definedMainDim = getLayoutDimension(node, getDim(mainAxis)) - + getPaddingAndBorderAxis(node, mainAxis); + } + + // We want to execute the next two loops one per line with flex-wrap + int startLine = 0; + int endLine = 0; + // int nextOffset = 0; + int alreadyComputedNextLayout = 0; + // We aggregate the total dimensions of the container in those two variables + float linesCrossDim = 0; + float linesMainDim = 0; + int linesCount = 0; + while (endLine < node.getChildCount()) { + // Layout non flexible children and count children by type + + // mainContentDim is accumulation of the dimensions and margin of all the + // non flexible children. This will be used in order to either set the + // dimensions of the node if none already exist, or to compute the + // remaining space left for the flexible children. + float mainContentDim = 0; + + // There are three kind of children, non flexible, flexible and absolute. + // We need to know how many there are in order to distribute the space. + int flexibleChildrenCount = 0; + float totalFlexible = 0; + int nonFlexibleChildrenCount = 0; + + float maxWidth; + for (i = startLine; i < node.getChildCount(); ++i) { + child = node.getChildAt(i); + float nextContentDim = 0; + + // It only makes sense to consider a child flexible if we have a computed + // dimension for the node. + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(mainAxis))) && isFlex(child)) { + flexibleChildrenCount++; + totalFlexible = totalFlexible + getFlex(child); + + // Even if we don't know its exact size yet, we already know the padding, + // border and margin. We'll use this partial information, which represents + // the smallest possible size for the child, to compute the remaining + // available space. + nextContentDim = getPaddingAndBorderAxis(child, mainAxis) + + getMarginAxis(child, mainAxis); + + } else { + maxWidth = CSSConstants.UNDEFINED; + if (!isRowDirection(mainAxis)) { + maxWidth = parentMaxWidth - + getMarginAxis(node, resolvedRowAxis) - + getPaddingAndBorderAxis(node, resolvedRowAxis); + + if (isDimDefined(node, resolvedRowAxis)) { + maxWidth = getLayoutDimension(node, getDim(resolvedRowAxis)) - + getPaddingAndBorderAxis(node, resolvedRowAxis); + } + } + + // This is the main recursive call. We layout non flexible children. + if (alreadyComputedNextLayout == 0) { + layoutNode(layoutContext, child, maxWidth, direction); + } + + // Absolute positioned elements do not take part of the layout, so we + // don't use them to compute mainContentDim + if (getPositionType(child) == CSSPositionType.RELATIVE) { + nonFlexibleChildrenCount++; + // At this point we know the final size and margin of the element. + nextContentDim = getDimWithMargin(child, mainAxis); + } + } + + // The element we are about to add would make us go to the next line + if (isFlexWrap(node) && + !CSSConstants.isUndefined(getLayoutDimension(node, getDim(mainAxis))) && + mainContentDim + nextContentDim > definedMainDim && + // If there's only one element, then it's bigger than the content + // and needs its own line + i != startLine) { + nonFlexibleChildrenCount--; + alreadyComputedNextLayout = 1; + break; + } + alreadyComputedNextLayout = 0; + mainContentDim = mainContentDim + nextContentDim; + endLine = i + 1; + } + + // Layout flexible children and allocate empty space + + // In order to position the elements in the main axis, we have two + // controls. The space between the beginning and the first element + // and the space between each two elements. + float leadingMainDim = 0; + float betweenMainDim = 0; + + // The remaining available space that needs to be allocated + float remainingMainDim = 0; + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(mainAxis)))) { + remainingMainDim = definedMainDim - mainContentDim; + } else { + remainingMainDim = Math.max(mainContentDim, 0) - mainContentDim; + } + + // If there are flexible children in the mix, they are going to fill the + // remaining space + if (flexibleChildrenCount != 0) { + float flexibleMainDim = remainingMainDim / totalFlexible; + float baseMainDim; + float boundMainDim; + + // Iterate over every child in the axis. If the flex share of remaining + // space doesn't meet min/max bounds, remove this child from flex + // calculations. + for (i = startLine; i < endLine; ++i) { + child = node.getChildAt(i); + if (isFlex(child)) { + baseMainDim = flexibleMainDim * getFlex(child) + + getPaddingAndBorderAxis(child, mainAxis); + boundMainDim = boundAxis(child, mainAxis, baseMainDim); + + if (baseMainDim != boundMainDim) { + remainingMainDim -= boundMainDim; + totalFlexible -= getFlex(child); + } + } + } + flexibleMainDim = remainingMainDim / totalFlexible; + + // The non flexible children can overflow the container, in this case + // we should just assume that there is no space available. + if (flexibleMainDim < 0) { + flexibleMainDim = 0; + } + // We iterate over the full array and only apply the action on flexible + // children. This is faster than actually allocating a new array that + // contains only flexible children. + for (i = startLine; i < endLine; ++i) { + child = node.getChildAt(i); + if (isFlex(child)) { + // At this point we know the final size of the element in the main + // dimension + setLayoutDimension(child, getDim(mainAxis), boundAxis(child, mainAxis, + flexibleMainDim * getFlex(child) + getPaddingAndBorderAxis(child, mainAxis) + )); + + maxWidth = CSSConstants.UNDEFINED; + if (isDimDefined(node, resolvedRowAxis)) { + maxWidth = getLayoutDimension(node, getDim(resolvedRowAxis)) - + getPaddingAndBorderAxis(node, resolvedRowAxis); + } else if (!isRowDirection(mainAxis)) { + maxWidth = parentMaxWidth - + getMarginAxis(node, resolvedRowAxis) - + getPaddingAndBorderAxis(node, resolvedRowAxis); + } + + // And we recursively call the layout algorithm for this child + layoutNode(layoutContext, child, maxWidth, direction); + } + } + + // We use justifyContent to figure out how to allocate the remaining + // space available + } else { + CSSJustify justifyContent = getJustifyContent(node); + if (justifyContent == CSSJustify.CENTER) { + leadingMainDim = remainingMainDim / 2; + } else if (justifyContent == CSSJustify.FLEX_END) { + leadingMainDim = remainingMainDim; + } else if (justifyContent == CSSJustify.SPACE_BETWEEN) { + remainingMainDim = Math.max(remainingMainDim, 0); + if (flexibleChildrenCount + nonFlexibleChildrenCount - 1 != 0) { + betweenMainDim = remainingMainDim / + (flexibleChildrenCount + nonFlexibleChildrenCount - 1); + } else { + betweenMainDim = 0; + } + } else if (justifyContent == CSSJustify.SPACE_AROUND) { + // Space on the edges is half of the space between elements + betweenMainDim = remainingMainDim / + (flexibleChildrenCount + nonFlexibleChildrenCount); + leadingMainDim = betweenMainDim / 2; + } + } + + // Position elements in the main axis and compute dimensions + + // At this point, all the children have their dimensions set. We need to + // find their position. In order to do that, we accumulate data in + // variables that are also useful to compute the total dimensions of the + // container! + float crossDim = 0; + float mainDim = leadingMainDim + + getLeadingPaddingAndBorder(node, mainAxis); + + for (i = startLine; i < endLine; ++i) { + child = node.getChildAt(i); + child.lineIndex = linesCount; + + if (getPositionType(child) == CSSPositionType.ABSOLUTE && + isPosDefined(child, getLeading(mainAxis))) { + // In case the child is position absolute and has left/top being + // defined, we override the position to whatever the user said + // (and margin/border). + setLayoutPosition(child, getPos(mainAxis), getPosition(child, getLeading(mainAxis)) + + getLeadingBorder(node, mainAxis) + + getLeadingMargin(child, mainAxis)); + } else { + // If the child is position absolute (without top/left) or relative, + // we put it at the current accumulated offset. + setLayoutPosition(child, getPos(mainAxis), getLayoutPosition(child, getPos(mainAxis)) + mainDim); + + // Define the trailing position accordingly. + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(mainAxis)))) { + setTrailingPosition(node, child, mainAxis); + } + } + + // Now that we placed the element, we need to update the variables + // We only need to do that for relative elements. Absolute elements + // do not take part in that phase. + if (getPositionType(child) == CSSPositionType.RELATIVE) { + // The main dimension is the sum of all the elements dimension plus + // the spacing. + mainDim = mainDim + betweenMainDim + getDimWithMargin(child, mainAxis); + // The cross dimension is the max of the elements dimension since there + // can only be one element in that cross dimension. + crossDim = Math.max(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis))); + } + } + + float containerCrossAxis = getLayoutDimension(node, getDim(crossAxis)); + if (CSSConstants.isUndefined(getLayoutDimension(node, getDim(crossAxis)))) { + containerCrossAxis = Math.max( + // For the cross dim, we add both sides at the end because the value + // is aggregate via a max function. Intermediate negative values + // can mess this computation otherwise + boundAxis(node, crossAxis, crossDim + getPaddingAndBorderAxis(node, crossAxis)), + getPaddingAndBorderAxis(node, crossAxis) + ); + } + + // Position elements in the cross axis + for (i = startLine; i < endLine; ++i) { + child = node.getChildAt(i); + + if (getPositionType(child) == CSSPositionType.ABSOLUTE && + isPosDefined(child, getLeading(crossAxis))) { + // In case the child is absolutely positionned and has a + // top/left/bottom/right being set, we override all the previously + // computed positions to set it correctly. + setLayoutPosition(child, getPos(crossAxis), getPosition(child, getLeading(crossAxis)) + + getLeadingBorder(node, crossAxis) + + getLeadingMargin(child, crossAxis)); + + } else { + float leadingCrossDim = getLeadingPaddingAndBorder(node, crossAxis); + + // For a relative children, we're either using alignItems (parent) or + // alignSelf (child) in order to determine the position in the cross axis + if (getPositionType(child) == CSSPositionType.RELATIVE) { + CSSAlign alignItem = getAlignItem(node, child); + if (alignItem == CSSAlign.STRETCH) { + // You can only stretch if the dimension has not already been set + // previously. + if (!isDimDefined(child, crossAxis)) { + setLayoutDimension(child, getDim(crossAxis), Math.max( + boundAxis(child, crossAxis, containerCrossAxis - + getPaddingAndBorderAxis(node, crossAxis) - + getMarginAxis(child, crossAxis)), + // You never want to go smaller than padding + getPaddingAndBorderAxis(child, crossAxis) + )); + } + } else if (alignItem != CSSAlign.FLEX_START) { + // The remaining space between the parent dimensions+padding and child + // dimensions+margin. + float remainingCrossDim = containerCrossAxis - + getPaddingAndBorderAxis(node, crossAxis) - + getDimWithMargin(child, crossAxis); + + if (alignItem == CSSAlign.CENTER) { + leadingCrossDim = leadingCrossDim + remainingCrossDim / 2; + } else { // CSSAlign.FLEX_END + leadingCrossDim = leadingCrossDim + remainingCrossDim; + } + } + } + + // And we apply the position + setLayoutPosition(child, getPos(crossAxis), getLayoutPosition(child, getPos(crossAxis)) + linesCrossDim + leadingCrossDim); + + // Define the trailing position accordingly. + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(crossAxis)))) { + setTrailingPosition(node, child, crossAxis); + } + } + } + + linesCrossDim = linesCrossDim + crossDim; + linesMainDim = Math.max(linesMainDim, mainDim); + linesCount = linesCount + 1; + startLine = endLine; + } + + // + // + // Note(prenaux): More than one line, we need to layout the crossAxis + // according to alignContent. + // + // Note that we could probably remove and handle the one line case + // here too, but for the moment this is safer since it won't interfere with + // previously working code. + // + // See specs: + // http://www.w3.org/TR/2012/CR-css3-flexbox-20120918/#layout-algorithm + // section 9.4 + // + if (linesCount > 1 && + !CSSConstants.isUndefined(getLayoutDimension(node, getDim(crossAxis)))) { + float nodeCrossAxisInnerSize = getLayoutDimension(node, getDim(crossAxis)) - + getPaddingAndBorderAxis(node, crossAxis); + float remainingAlignContentDim = nodeCrossAxisInnerSize - linesCrossDim; + + float crossDimLead = 0; + float currentLead = getLeadingPaddingAndBorder(node, crossAxis); + + CSSAlign alignContent = getAlignContent(node); + if (alignContent == CSSAlign.FLEX_END) { + currentLead = currentLead + remainingAlignContentDim; + } else if (alignContent == CSSAlign.CENTER) { + currentLead = currentLead + remainingAlignContentDim / 2; + } else if (alignContent == CSSAlign.STRETCH) { + if (nodeCrossAxisInnerSize > linesCrossDim) { + crossDimLead = (remainingAlignContentDim / linesCount); + } + } + + int endIndex = 0; + for (i = 0; i < linesCount; ++i) { + int startIndex = endIndex; + + // compute the line's height and find the endIndex + float lineHeight = 0; + for (ii = startIndex; ii < node.getChildCount(); ++ii) { + child = node.getChildAt(ii); + if (getPositionType(child) != CSSPositionType.RELATIVE) { + continue; + } + if (child.lineIndex != i) { + break; + } + if (!CSSConstants.isUndefined(getLayoutDimension(child, getDim(crossAxis)))) { + lineHeight = Math.max( + lineHeight, + getLayoutDimension(child, getDim(crossAxis)) + getMarginAxis(child, crossAxis) + ); + } + } + endIndex = ii; + lineHeight = lineHeight + crossDimLead; + + for (ii = startIndex; ii < endIndex; ++ii) { + child = node.getChildAt(ii); + if (getPositionType(child) != CSSPositionType.RELATIVE) { + continue; + } + + CSSAlign alignContentAlignItem = getAlignItem(node, child); + if (alignContentAlignItem == CSSAlign.FLEX_START) { + setLayoutPosition(child, getPos(crossAxis), currentLead + getLeadingMargin(child, crossAxis)); + } else if (alignContentAlignItem == CSSAlign.FLEX_END) { + setLayoutPosition(child, getPos(crossAxis), currentLead + lineHeight - getTrailingMargin(child, crossAxis) - getLayoutDimension(child, getDim(crossAxis))); + } else if (alignContentAlignItem == CSSAlign.CENTER) { + float childHeight = getLayoutDimension(child, getDim(crossAxis)); + setLayoutPosition(child, getPos(crossAxis), currentLead + (lineHeight - childHeight) / 2); + } else if (alignContentAlignItem == CSSAlign.STRETCH) { + setLayoutPosition(child, getPos(crossAxis), currentLead + getLeadingMargin(child, crossAxis)); + // TODO(prenaux): Correctly set the height of items with undefined + // (auto) crossAxis dimension. + } + } + + currentLead = currentLead + lineHeight; + } + } + + boolean needsMainTrailingPos = false; + boolean needsCrossTrailingPos = false; + + // If the user didn't specify a width or height, and it has not been set + // by the container, then we set it via the children. + if (CSSConstants.isUndefined(getLayoutDimension(node, getDim(mainAxis)))) { + setLayoutDimension(node, getDim(mainAxis), Math.max( + // We're missing the last padding at this point to get the final + // dimension + boundAxis(node, mainAxis, linesMainDim + getTrailingPaddingAndBorder(node, mainAxis)), + // We can never assign a width smaller than the padding and borders + getPaddingAndBorderAxis(node, mainAxis) + )); + + needsMainTrailingPos = true; + } + + if (CSSConstants.isUndefined(getLayoutDimension(node, getDim(crossAxis)))) { + setLayoutDimension(node, getDim(crossAxis), Math.max( + // For the cross dim, we add both sides at the end because the value + // is aggregate via a max function. Intermediate negative values + // can mess this computation otherwise + boundAxis(node, crossAxis, linesCrossDim + getPaddingAndBorderAxis(node, crossAxis)), + getPaddingAndBorderAxis(node, crossAxis) + )); + + needsCrossTrailingPos = true; + } + + // Set trailing position if necessary + if (needsMainTrailingPos || needsCrossTrailingPos) { + for (i = 0; i < node.getChildCount(); ++i) { + child = node.getChildAt(i); + + if (needsMainTrailingPos) { + setTrailingPosition(node, child, mainAxis); + } + + if (needsCrossTrailingPos) { + setTrailingPosition(node, child, crossAxis); + } + } + } + + // Calculate dimensions for absolutely positioned elements + for (i = 0; i < node.getChildCount(); ++i) { + child = node.getChildAt(i); + if (getPositionType(child) == CSSPositionType.ABSOLUTE) { + // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both + // left and right or top and bottom). + for (ii = 0; ii < 2; ii++) { + axis = (ii != 0) ? CSSFlexDirection.ROW : CSSFlexDirection.COLUMN; + if (!CSSConstants.isUndefined(getLayoutDimension(node, getDim(axis))) && + !isDimDefined(child, axis) && + isPosDefined(child, getLeading(axis)) && + isPosDefined(child, getTrailing(axis))) { + setLayoutDimension(child, getDim(axis), Math.max( + boundAxis(child, axis, getLayoutDimension(node, getDim(axis)) - + getBorderAxis(node, axis) - + getMarginAxis(child, axis) - + getPosition(child, getLeading(axis)) - + getPosition(child, getTrailing(axis)) + ), + // You never want to go smaller than padding + getPaddingAndBorderAxis(child, axis) + )); + } + } + for (ii = 0; ii < 2; ii++) { + axis = (ii != 0) ? CSSFlexDirection.ROW : CSSFlexDirection.COLUMN; + if (isPosDefined(child, getTrailing(axis)) && + !isPosDefined(child, getLeading(axis))) { + setLayoutPosition(child, getLeading(axis), getLayoutDimension(node, getDim(axis)) - + getLayoutDimension(child, getDim(axis)) - + getPosition(child, getTrailing(axis))); + } + } + } + } + } + /** END_GENERATED **/ +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/MeasureOutput.java b/ReactAndroid/src/main/java/com/facebook/csslayout/MeasureOutput.java new file mode 100644 index 000000000..8c0190db5 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/MeasureOutput.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<177254872216bd05e2c0667dc29ef032>> + +package com.facebook.csslayout; + +/** + * POJO to hold the output of the measure function. + */ +public class MeasureOutput { + + public float width; + public float height; +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/README b/ReactAndroid/src/main/java/com/facebook/csslayout/README new file mode 100644 index 000000000..65821d007 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/README @@ -0,0 +1,12 @@ +The source of truth for css-layout is: https://github.com/facebook/css-layout + +The code here should be kept in sync with GitHub. +HEAD at the time this code was synced: https://github.com/facebook/css-layout/commit/7104f7c8eb6c160a8d10badc08f9b981bb65e19c + +There is generated code in: + - README (this file) + - java/com/facebook/csslayout (this folder) + - javatests/com/facebook/csslayout + +The code was generated by running 'make' in the css-layout folder and copied to React Native. + diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook b/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook new file mode 100644 index 000000000..bc1327833 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook @@ -0,0 +1,14 @@ +The source of truth for css-layout is: https://github.com/facebook/css-layout + +The code here should be kept in sync with GitHub. +HEAD at the time this code was synced: https://github.com/facebook/css-layout/commit/7104f7c8eb6c160a8d10badc08f9b981bb65e19c + +There is generated code in: + - README.facebook (this file) + - java/com/facebook/csslayout (this folder) + - javatests/com/facebook/csslayout + +The code was generated by running 'make' in the css-layout folder and running: + + ./syncFromGithub.sh + diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/Spacing.java b/ReactAndroid/src/main/java/com/facebook/csslayout/Spacing.java new file mode 100644 index 000000000..2f3672ecd --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/Spacing.java @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<6853e87a84818f1abb6573aee7d6bd55>> + +package com.facebook.csslayout; + +import javax.annotation.Nullable; + +/** + * Class representing CSS spacing (padding, margin, and borders). This is mostly necessary to + * properly implement interactions and updates for properties like margin, marginLeft, and + * marginHorizontal. + */ +public class Spacing { + + /** + * Spacing type that represents the left direction. E.g. {@code marginLeft}. + */ + public static final int LEFT = 0; + /** + * Spacing type that represents the top direction. E.g. {@code marginTop}. + */ + public static final int TOP = 1; + /** + * Spacing type that represents the right direction. E.g. {@code marginRight}. + */ + public static final int RIGHT = 2; + /** + * Spacing type that represents the bottom direction. E.g. {@code marginBottom}. + */ + public static final int BOTTOM = 3; + /** + * Spacing type that represents vertical direction (top and bottom). E.g. {@code marginVertical}. + */ + public static final int VERTICAL = 4; + /** + * Spacing type that represents horizontal direction (left and right). E.g. + * {@code marginHorizontal}. + */ + public static final int HORIZONTAL = 5; + /** + * Spacing type that represents start direction e.g. left in left-to-right, right in right-to-left. + */ + public static final int START = 6; + /** + * Spacing type that represents end direction e.g. right in left-to-right, left in right-to-left. + */ + public static final int END = 7; + /** + * Spacing type that represents all directions (left, top, right, bottom). E.g. {@code margin}. + */ + public static final int ALL = 8; + + private final float[] mSpacing = newFullSpacingArray(); + @Nullable private float[] mDefaultSpacing = null; + + /** + * Set a spacing value. + * + * @param spacingType one of {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, {@link #BOTTOM}, + * {@link #VERTICAL}, {@link #HORIZONTAL}, {@link #ALL} + * @param value the value for this direction + * @return {@code true} if the spacing has changed, or {@code false} if the same value was already + * set + */ + public boolean set(int spacingType, float value) { + if (!FloatUtil.floatsEqual(mSpacing[spacingType], value)) { + mSpacing[spacingType] = value; + return true; + } + return false; + } + + /** + * Set a default spacing value. This is used as a fallback when no spacing has been set for a + * particular direction. + * + * @param spacingType one of {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, {@link #BOTTOM} + * @param value the default value for this direction + * @return + */ + public boolean setDefault(int spacingType, float value) { + if (mDefaultSpacing == null) { + mDefaultSpacing = newSpacingResultArray(); + } + if (!FloatUtil.floatsEqual(mDefaultSpacing[spacingType], value)) { + mDefaultSpacing[spacingType] = value; + return true; + } + return false; + } + + /** + * Get the spacing for a direction. This takes into account any default values that have been set. + * + * @param spacingType one of {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, {@link #BOTTOM} + */ + public float get(int spacingType) { + int secondType = spacingType == TOP || spacingType == BOTTOM ? VERTICAL : HORIZONTAL; + float defaultValue = spacingType == START || spacingType == END ? CSSConstants.UNDEFINED : 0; + return + !CSSConstants.isUndefined(mSpacing[spacingType]) + ? mSpacing[spacingType] + : !CSSConstants.isUndefined(mSpacing[secondType]) + ? mSpacing[secondType] + : !CSSConstants.isUndefined(mSpacing[ALL]) + ? mSpacing[ALL] + : mDefaultSpacing != null + ? mDefaultSpacing[spacingType] + : defaultValue; + } + + /** + * Get the raw value (that was set using {@link #set(int, float)}), without taking into account + * any default values. + * + * @param spacingType one of {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, {@link #BOTTOM}, + * {@link #VERTICAL}, {@link #HORIZONTAL}, {@link #ALL} + */ + public float getRaw(int spacingType) { + return mSpacing[spacingType]; + } + + private static float[] newFullSpacingArray() { + return new float[] { + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + }; + } + + private static float[] newSpacingResultArray() { + return newSpacingResultArray(0); + } + + private static float[] newSpacingResultArray(float defaultValue) { + return new float[] { + defaultValue, + defaultValue, + defaultValue, + defaultValue, + defaultValue, + defaultValue, + CSSConstants.UNDEFINED, + CSSConstants.UNDEFINED, + defaultValue, + }; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/syncFromGithub.sh b/ReactAndroid/src/main/java/com/facebook/csslayout/syncFromGithub.sh new file mode 100755 index 000000000..130f0c3e8 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/syncFromGithub.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +function usage { + echo "usage: syncFromGithub.sh "; +} + +function patchfile { + # Add React Native copyright + printf "/**\n" >> /tmp/csslayoutsync.tmp + printf " * Copyright (c) 2014-present, Facebook, Inc.\n" >> /tmp/csslayoutsync.tmp + printf " * All rights reserved.\n" >> /tmp/csslayoutsync.tmp + printf " * This source code is licensed under the BSD-style license found in the\n" >> /tmp/csslayoutsync.tmp + printf " * LICENSE file in the root directory of this source tree. An additional grant\n" >> /tmp/csslayoutsync.tmp + printf " * of patent rights can be found in the PATENTS file in the same directory.\n" >> /tmp/csslayoutsync.tmp + printf " */\n\n" >> /tmp/csslayoutsync.tmp + printf "// NOTE: this file is auto-copied from https://github.com/facebook/css-layout\n" >> /tmp/csslayoutsync.tmp + # The following is split over four lines so Phabricator doesn't think this file is generated + printf "// @g" >> /tmp/csslayoutsync.tmp + printf "enerated <> /tmp/csslayoutsync.tmp + printf "ignedSource::*O*zOeWoEQle#+L" >> /tmp/csslayoutsync.tmp + printf "!plEphiEmie@IsG>>\n\n" >> /tmp/csslayoutsync.tmp + tail -n +9 $1 >> /tmp/csslayoutsync.tmp + mv /tmp/csslayoutsync.tmp $1 + $ROOT/scripts/signedsource.py sign $1 +} + +if [ -z $1 ]; then + usage + exit 1 +fi + +if [ -z $2 ]; then + usage + exit 1 +fi + +GITHUB=$1 +ROOT=$2 + +set -e # exit if any command fails + +echo "Making github project..." +pushd $GITHUB +COMMIT_ID=$(git rev-parse HEAD) +make +popd + +SRC=$GITHUB/src/java/src/com/facebook/csslayout +TESTS=$GITHUB/src/java/tests/com/facebook/csslayout +FBA_SRC=. +FBA_TESTS=$ROOT/javatests/com/facebook/csslayout + +echo "Copying src files over..." +cp $SRC/*.java $FBA_SRC +echo "Copying test files over..." +cp $TESTS/*.java $FBA_TESTS + +echo "Patching files..." +for sourcefile in $FBA_SRC/*.java; do + patchfile $sourcefile +done +for testfile in $FBA_TESTS/*.java; do + patchfile $testfile +done + +echo "Writing README.facebook" + +echo "The source of truth for css-layout is: https://github.com/facebook/css-layout + +The code here should be kept in sync with GitHub. +HEAD at the time this code was synced: https://github.com/facebook/css-layout/commit/$COMMIT_ID + +There is generated code in: + - README.facebook (this file) + - java/com/facebook/csslayout (this folder) + - javatests/com/facebook/csslayout + +The code was generated by running 'make' in the css-layout folder and running: + + ./syncFromGithub.sh +" > $FBA_SRC/README.facebook + +echo "Writing README" + +echo "The source of truth for css-layout is: https://github.com/facebook/css-layout + +The code here should be kept in sync with GitHub. +HEAD at the time this code was synced: https://github.com/facebook/css-layout/commit/$COMMIT_ID + +There is generated code in: + - README (this file) + - java/com/facebook/csslayout (this folder) + - javatests/com/facebook/csslayout + +The code was generated by running 'make' in the css-layout folder and copied to React Native. +" > $FBA_SRC/README + +echo "Done." +echo "Please run buck test //javatests/com/facebook/csslayout" diff --git a/ReactAndroid/src/main/java/com/facebook/jni/Countable.java b/ReactAndroid/src/main/java/com/facebook/jni/Countable.java new file mode 100644 index 000000000..75892f48c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/jni/Countable.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.jni; + +import com.facebook.proguard.annotations.DoNotStrip; + +/** + * A Java Object that has native memory allocated corresponding to this instance. + * + * NB: THREAD SAFETY (this comment also exists at Countable.cpp) + * + * {@link #dispose} deletes the corresponding native object on whatever thread the method is called + * on. In the common case when this is called by Countable#finalize(), this will be called on the + * system finalizer thread. If you manually call dispose on the Java object, the native object + * will be deleted synchronously on that thread. + */ +@DoNotStrip +public class Countable { + // Private C++ instance + @DoNotStrip + private long mInstance = 0; + + public Countable() { + Prerequisites.ensure(); + } + + public native void dispose(); + + protected void finalize() throws Throwable { + dispose(); + super.finalize(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/jni/CppException.java b/ReactAndroid/src/main/java/com/facebook/jni/CppException.java new file mode 100644 index 000000000..3006da53a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/jni/CppException.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.jni; + +import com.facebook.proguard.annotations.DoNotStrip; + +@DoNotStrip +public class CppException extends RuntimeException { + @DoNotStrip + public CppException(String message) { + super(message); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/jni/CppSystemErrorException.java b/ReactAndroid/src/main/java/com/facebook/jni/CppSystemErrorException.java new file mode 100644 index 000000000..18f754bf4 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/jni/CppSystemErrorException.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.jni; + +import com.facebook.proguard.annotations.DoNotStrip; + +@DoNotStrip +public class CppSystemErrorException extends CppException { + int errorCode; + + @DoNotStrip + public CppSystemErrorException(String message, int errorCode) { + super(message); + this.errorCode = errorCode; + } + + public int getErrorCode() { + return errorCode; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/jni/HybridData.java b/ReactAndroid/src/main/java/com/facebook/jni/HybridData.java new file mode 100644 index 000000000..fcb4ca336 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/jni/HybridData.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.jni; + +import com.facebook.proguard.annotations.DoNotStrip; + +/** + * This object holds a native C++ member for hybrid Java/C++ objects. + * + * NB: THREAD SAFETY + * + * {@link #dispose} deletes the corresponding native object on whatever thread + * the method is called on. In the common case when this is called by + * HybridData#finalize(), this will be called on the system finalizer + * thread. If you manually call resetNative() on the Java object, the C++ + * object will be deleted synchronously on that thread. + */ +@DoNotStrip +public class HybridData { + // Private C++ instance + @DoNotStrip + private long mNativePointer = 0; + + public HybridData() { + Prerequisites.ensure(); + } + + /** + * To explicitly delete the instance, call resetNative(). If the C++ + * instance is referenced after this is called, a NullPointerException will + * be thrown. resetNative() may be called multiple times safely. Because + * {@link #finalize} calls resetNative, the instance will not leak if this is + * not called, but timing of deletion and the thread the C++ dtor is called + * on will be at the whim of the Java GC. If you want to control the thread + * and timing of the destructor, you should call resetNative() explicitly. + */ + public native void resetNative(); + + protected void finalize() throws Throwable { + resetNative(); + super.finalize(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/jni/Prerequisites.java b/ReactAndroid/src/main/java/com/facebook/jni/Prerequisites.java new file mode 100644 index 000000000..b669e546b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/jni/Prerequisites.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.jni; + + +import com.facebook.soloader.SoLoader; + + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; + +public class Prerequisites { + private static final int EGL_OPENGL_ES2_BIT = 0x0004; + + public static void ensure() { + SoLoader.loadLibrary("fbjni"); + } + + // Code is simplified version of getDetectedVersion() + // from cts/tests/tests/graphics/src/android/opengl/cts/OpenGlEsVersionTest.java + static public boolean supportsOpenGL20() { + EGL10 egl = (EGL10) EGLContext.getEGL(); + EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + int[] numConfigs = new int[1]; + + if (egl.eglInitialize(display, null)) { + try { + if (egl.eglGetConfigs(display, null, 0, numConfigs)) { + EGLConfig[] configs = new EGLConfig[numConfigs[0]]; + if (egl.eglGetConfigs(display, configs, numConfigs[0], numConfigs)) { + int[] value = new int[1]; + for (int i = 0; i < numConfigs[0]; i++) { + if (egl.eglGetConfigAttrib(display, configs[i], + EGL10.EGL_RENDERABLE_TYPE, value)) { + if ((value[0] & EGL_OPENGL_ES2_BIT) == EGL_OPENGL_ES2_BIT) { + return true; + } + } + } + } + } + } finally { + egl.eglTerminate(display); + } + } + return false; + } +} + diff --git a/ReactAndroid/src/main/java/com/facebook/jni/UnknownCppException.java b/ReactAndroid/src/main/java/com/facebook/jni/UnknownCppException.java new file mode 100644 index 000000000..fa6e971f6 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/jni/UnknownCppException.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.jni; + +import com.facebook.proguard.annotations.DoNotStrip; + +@DoNotStrip +public class UnknownCppException extends CppException { + @DoNotStrip + public UnknownCppException() { + super("Unknown"); + } + + @DoNotStrip + public UnknownCppException(String message) { + super(message); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/proguard/annotations/DoNotStrip.java b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/DoNotStrip.java new file mode 100644 index 000000000..86a3f2c3f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/DoNotStrip.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.proguard.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * Add this annotation to a class, method, or field to instruct Proguard to not strip it out. + * + * This is useful for methods called via reflection that could appear as unused to Proguard. + */ +@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR }) +@Retention(CLASS) +public @interface DoNotStrip { +} diff --git a/ReactAndroid/src/main/java/com/facebook/proguard/annotations/KeepGettersAndSetters.java b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/KeepGettersAndSetters.java new file mode 100644 index 000000000..11f4f32b9 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/KeepGettersAndSetters.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.proguard.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * Add this annotation to a class, to keep all "void set*(***)" and get* methods. + * + *

    This is useful for classes that are controlled by animator-like classes that control + * various properties with reflection. + * + *

    NOTE: This is not needed for Views because their getters and setters + * are automatically kept by the default Android SDK ProGuard config. + */ +@Target({ElementType.TYPE}) +@Retention(CLASS) +public @interface KeepGettersAndSetters { +} diff --git a/ReactAndroid/src/main/java/com/facebook/proguard/annotations/proguard_annotations.pro b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/proguard_annotations.pro new file mode 100644 index 000000000..b1ef5f7ce --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/proguard/annotations/proguard_annotations.pro @@ -0,0 +1,15 @@ +# Keep our interfaces so they can be used by other ProGuard rules. +# See http://sourceforge.net/p/proguard/bugs/466/ +-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip +-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters + +# Do not strip any method/class that is annotated with @DoNotStrip +-keep @com.facebook.proguard.annotations.DoNotStrip class * +-keepclassmembers class * { + @com.facebook.proguard.annotations.DoNotStrip *; +} + +-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { + void set*(***); + *** get*(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/CompositeReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CompositeReactPackage.java new file mode 100644 index 000000000..8f7856913 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/CompositeReactPackage.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +/** + * {@code CompositeReactPackage} allows to create a single package composed of views and modules + * from several other packages. + */ +public class CompositeReactPackage implements ReactPackage { + + private final List mChildReactPackages = new ArrayList<>(); + + /** + * The order in which packages are passed matters. It may happen that a NativeModule or + * or a ViewManager exists in two or more ReactPackages. In that case the latter will win + * i.e. the latter will overwrite the former. This re-occurrence is detected by + * comparing a name of a module. + */ + public CompositeReactPackage(ReactPackage arg1, ReactPackage arg2, ReactPackage... args) { + mChildReactPackages.add(arg1); + mChildReactPackages.add(arg2); + + for (ReactPackage reactPackage: args) { + mChildReactPackages.add(reactPackage); + } + } + + /** + * {@inheritDoc} + */ + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + final Map moduleMap = new HashMap<>(); + for (ReactPackage reactPackage: mChildReactPackages) { + for (NativeModule nativeModule: reactPackage.createNativeModules(reactContext)) { + moduleMap.put(nativeModule.getName(), nativeModule); + } + } + return new ArrayList(moduleMap.values()); + } + + /** + * {@inheritDoc} + */ + @Override + public List> createJSModules() { + final Set> moduleSet = new HashSet<>(); + for (ReactPackage reactPackage: mChildReactPackages) { + for (Class jsModule: reactPackage.createJSModules()) { + moduleSet.add(jsModule); + } + } + return new ArrayList(moduleSet); + } + + /** + * {@inheritDoc} + */ + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + final Map viewManagerMap = new HashMap<>(); + for (ReactPackage reactPackage: mChildReactPackages) { + for (ViewManager viewManager: reactPackage.createViewManagers(reactContext)) { + viewManagerMap.put(viewManager.getName(), viewManager); + } + } + return new ArrayList(viewManagerMap.values()); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java new file mode 100644 index 000000000..df524cf91 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.facebook.catalyst.uimanager.debug.DebugComponentOwnershipModule; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.modules.core.ExceptionsManagerModule; +import com.facebook.react.modules.core.JSTimersExecution; +import com.facebook.react.modules.core.Timing; +import com.facebook.react.modules.debug.AnimationsDebugModule; +import com.facebook.react.modules.debug.SourceCodeModule; +import com.facebook.react.modules.systeminfo.AndroidInfoModule; +import com.facebook.react.uimanager.AppRegistry; +import com.facebook.react.uimanager.ReactNative; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Package defining core framework modules (e.g. UIManager). It should be used for modules that + * require special integration with other framework parts (e.g. with the list of packages to load + * view managers from). + */ +/* package */ class CoreModulesPackage implements ReactPackage { + + private final ReactInstanceManager mReactInstanceManager; + private final DefaultHardwareBackBtnHandler mHardwareBackBtnHandler; + + CoreModulesPackage( + ReactInstanceManager reactInstanceManager, + DefaultHardwareBackBtnHandler hardwareBackBtnHandler) { + mReactInstanceManager = reactInstanceManager; + mHardwareBackBtnHandler = hardwareBackBtnHandler; + } + + @Override + public List createNativeModules( + ReactApplicationContext catalystApplicationContext) { + return Arrays.asList( + new AnimationsDebugModule( + catalystApplicationContext, + mReactInstanceManager.getDevSupportManager().getDevSettings()), + new AndroidInfoModule(), + new DeviceEventManagerModule(catalystApplicationContext, mHardwareBackBtnHandler), + new ExceptionsManagerModule(mReactInstanceManager.getDevSupportManager()), + new Timing(catalystApplicationContext), + new SourceCodeModule( + mReactInstanceManager.getDevSupportManager().getSourceUrl(), + mReactInstanceManager.getDevSupportManager().getSourceMapUrl()), + new UIManagerModule( + catalystApplicationContext, + mReactInstanceManager.createAllViewManagers(catalystApplicationContext)), + new DebugComponentOwnershipModule(catalystApplicationContext)); + } + + @Override + public List> createJSModules() { + return Arrays.asList( + DeviceEventManagerModule.RCTDeviceEventEmitter.class, + JSTimersExecution.class, + RCTEventEmitter.class, + AppRegistry.class, + ReactNative.class, + DebugComponentOwnershipModule.RCTDebugComponentOwnership.class); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return new ArrayList<>(0); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/LifecycleState.java b/ReactAndroid/src/main/java/com/facebook/react/LifecycleState.java new file mode 100644 index 000000000..f8598e908 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/LifecycleState.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react; + +/** + * Lifecycle state for an Activity. The state right after pause and right before resume are the + * basically the same so this enum is in terms of the forward lifecycle progression (onResume, etc). + * Eventually, if necessary, it could contain something like: + * + * BEFORE_CREATE, + * CREATED, + * VIEW_CREATED, + * STARTED, + * RESUMED + */ +public enum LifecycleState { + + BEFORE_RESUME, + RESUMED, +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java new file mode 100644 index 000000000..3cd5cb735 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -0,0 +1,564 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react; + +import javax.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +import android.app.Application; +import android.content.Context; +import android.os.Bundle; +import android.view.View; + +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.JSBundleLoader; +import com.facebook.react.bridge.JSCJavaScriptExecutor; +import com.facebook.react.bridge.JavaScriptExecutor; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.JavaScriptModulesConfig; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.NativeModuleRegistry; +import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; +import com.facebook.react.bridge.ProxyJavaScriptExecutor; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.bridge.queue.CatalystQueueConfigurationSpec; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.devsupport.DevSupportManager; +import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.uimanager.AppRegistry; +import com.facebook.react.uimanager.ReactNative; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.soloader.SoLoader; + +/** + * This class is managing instances of {@link CatalystInstance}. It expose a way to configure + * catalyst instance using {@link ReactPackage} and keeps track of the lifecycle of that + * instance. It also sets up connection between the instance and developers support functionality + * of the framework. + * + * An instance of this manager is required to start JS application in {@link ReactRootView} (see + * {@link ReactRootView#startReactApplication} for more info). + * + * The lifecycle of the instance of {@link ReactInstanceManager} should be bound to the activity + * that owns the {@link ReactRootView} that is used to render react application using this + * instance manager (see {@link ReactRootView#startReactApplication}). It's required tp pass + * owning activity's lifecycle events to the instance manager (see {@link #onPause}, + * {@link #onDestroy} and {@link #onResume}). + * + * To instantiate an instance of this class use {@link #builder}. + */ +public class ReactInstanceManager { + + /* should only be accessed from main thread (UI thread) */ + private final List mAttachedRootViews = new ArrayList<>(); + private LifecycleState mLifecycleState; + + /* accessed from any thread */ + private final @Nullable String mBundleAssetName; /* name of JS bundle file in assets folder */ + private final @Nullable String mJSMainModuleName; /* path to JS bundle root on packager server */ + private final List mPackages; + private final DevSupportManager mDevSupportManager; + private final boolean mUseDeveloperSupport; + private final @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener; + private @Nullable volatile ReactContext mCurrentReactContext; + private final Context mApplicationContext; + private @Nullable DefaultHardwareBackBtnHandler mDefaultBackButtonImpl; + + private final ReactInstanceDevCommandsHandler mDevInterface = + new ReactInstanceDevCommandsHandler() { + + @Override + public void onReloadWithJSDebugger(ProxyJavaScriptExecutor proxyExecutor) { + ReactInstanceManager.this.onReloadWithJSDebugger(proxyExecutor); + } + + @Override + public void onJSBundleLoadedFromServer() { + ReactInstanceManager.this.onJSBundleLoadedFromServer(); + } + + @Override + public void toggleElementInspector() { + ReactInstanceManager.this.toggleElementInspector(); + } + }; + + private final DefaultHardwareBackBtnHandler mBackBtnHandler = + new DefaultHardwareBackBtnHandler() { + @Override + public void invokeDefaultOnBackPressed() { + ReactInstanceManager.this.invokeDefaultOnBackPressed(); + } + }; + + private ReactInstanceManager( + Context applicationContext, + @Nullable String bundleAssetName, + @Nullable String jsMainModuleName, + List packages, + boolean useDeveloperSupport, + @Nullable NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener, + LifecycleState initialLifecycleState) { + initializeSoLoaderIfNecessary(applicationContext); + + mApplicationContext = applicationContext; + mBundleAssetName = bundleAssetName; + mJSMainModuleName = jsMainModuleName; + mPackages = packages; + mUseDeveloperSupport = useDeveloperSupport; + // We need to instantiate DevSupportManager regardless to the useDeveloperSupport option, + // although will prevent dev support manager from displaying any options or dialogs by + // checking useDeveloperSupport option before calling setDevSupportEnabled on this manager + // TODO(6803830): Don't instantiate devsupport manager when useDeveloperSupport is false + mDevSupportManager = new DevSupportManager( + applicationContext, + mDevInterface, + mJSMainModuleName, + useDeveloperSupport); + mBridgeIdleDebugListener = bridgeIdleDebugListener; + mLifecycleState = initialLifecycleState; + } + + public DevSupportManager getDevSupportManager() { + return mDevSupportManager; + } + + /** + * Creates a builder that is capable of creating an instance of {@link ReactInstanceManager}. + */ + public static Builder builder() { + return new Builder(); + } + + private static void initializeSoLoaderIfNecessary(Context applicationContext) { + // Call SoLoader.initialize here, this is required for apps that does not use exopackage and + // does not use SoLoader for loading other native code except from the one used by React Native + // This way we don't need to require others to have additional initialization code and to + // subclass android.app.Application. + + // Method SoLoader.init is idempotent, so if you wish to use native exopackage, just call + // SoLoader.init with appropriate args before initializing ReactInstanceManager + SoLoader.init(applicationContext, /* native exopackage */ false); + } + + /** + * This method will give JS the opportunity to consume the back button event. If JS does not + * consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip + * to JS. + */ + public void onBackPressed() { + UiThreadUtil.assertOnUiThread(); + ReactContext reactContext = mCurrentReactContext; + if (mCurrentReactContext == null) { + // Invoke without round trip to JS. + FLog.w(ReactConstants.TAG, "Instance detached from instance manager"); + invokeDefaultOnBackPressed(); + } else { + DeviceEventManagerModule deviceEventManagerModule = + Assertions.assertNotNull(reactContext).getNativeModule(DeviceEventManagerModule.class); + deviceEventManagerModule.emitHardwareBackPressed(); + } + } + + private void invokeDefaultOnBackPressed() { + UiThreadUtil.assertOnUiThread(); + if (mDefaultBackButtonImpl != null) { + mDefaultBackButtonImpl.invokeDefaultOnBackPressed(); + } + } + + private void toggleElementInspector() { + if (mCurrentReactContext != null) { + mCurrentReactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("toggleElementInspector", null); + } + } + + public void onPause() { + UiThreadUtil.assertOnUiThread(); + + mLifecycleState = LifecycleState.BEFORE_RESUME; + + mDefaultBackButtonImpl = null; + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(false); + } + + if (mCurrentReactContext != null) { + mCurrentReactContext.onPause(); + } + } + + /** + * Use this method when the activity resumes to enable invoking the back button directly from JS. + * + * This method retains an instance to provided mDefaultBackButtonImpl. Thus it's + * important to pass from the activity instance that owns this particular instance of {@link + * ReactInstanceManager}, so that once this instance receive {@link #onDestroy} event it will + * clear the reference to that defaultBackButtonImpl. + * + * @param defaultBackButtonImpl a {@link DefaultHardwareBackBtnHandler} from an Activity that owns + * this instance of {@link ReactInstanceManager}. + */ + public void onResume(DefaultHardwareBackBtnHandler defaultBackButtonImpl) { + UiThreadUtil.assertOnUiThread(); + + mLifecycleState = LifecycleState.RESUMED; + + mDefaultBackButtonImpl = defaultBackButtonImpl; + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(true); + } + + if (mCurrentReactContext != null) { + mCurrentReactContext.onResume(); + } + } + + public void onDestroy() { + UiThreadUtil.assertOnUiThread(); + + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(false); + } + + if (mCurrentReactContext != null) { + mCurrentReactContext.onDestroy(); + } + } + + public void showDevOptionsDialog() { + UiThreadUtil.assertOnUiThread(); + mDevSupportManager.showDevOptionsDialog(); + } + + /** + * Attach given {@param rootView} to a catalyst instance manager and start JS application using + * JS module provided by {@link ReactRootView#getJSModuleName}. This view will then be tracked + * by this manager and in case of catalyst instance restart it will be re-attached. + */ + /* package */ void attachMeasuredRootView(ReactRootView rootView) { + UiThreadUtil.assertOnUiThread(); + mAttachedRootViews.add(rootView); + if (mCurrentReactContext == null) { + initializeReactContext(); + } else { + attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance()); + } + } + + /** + * Detach given {@param rootView} from current catalyst instance. It's safe to call this method + * multiple times on the same {@param rootView} - in that case view will be detached with the + * first call. + */ + /* package */ void detachRootView(ReactRootView rootView) { + UiThreadUtil.assertOnUiThread(); + if (mAttachedRootViews.remove(rootView)) { + if (mCurrentReactContext != null && mCurrentReactContext.hasActiveCatalystInstance()) { + detachViewFromInstance(rootView, mCurrentReactContext.getCatalystInstance()); + } + } + } + + /** + * Uses configured {@link ReactPackage} instances to create all view managers + */ + /* package */ List createAllViewManagers( + ReactApplicationContext catalystApplicationContext) { + List allViewManagers = new ArrayList<>(); + for (ReactPackage reactPackage : mPackages) { + allViewManagers.addAll(reactPackage.createViewManagers(catalystApplicationContext)); + } + return allViewManagers; + } + + @VisibleForTesting + public @Nullable ReactContext getCurrentReactContext() { + return mCurrentReactContext; + } + + private void onReloadWithJSDebugger(ProxyJavaScriptExecutor proxyExecutor) { + recreateReactContext( + proxyExecutor, + JSBundleLoader.createRemoteDebuggerBundleLoader( + mDevSupportManager.getJSBundleURLForRemoteDebugging())); + } + + private void onJSBundleLoadedFromServer() { + recreateReactContext( + new JSCJavaScriptExecutor(), + JSBundleLoader.createCachedBundleFromNetworkLoader( + mDevSupportManager.getSourceUrl(), + mDevSupportManager.getDownloadedJSBundleFile())); + } + + private void initializeReactContext() { + if (mUseDeveloperSupport) { + if (mDevSupportManager.hasUpToDateJSBundleInCache()) { + // If there is a up-to-date bundle downloaded from server, always use that + onJSBundleLoadedFromServer(); + return; + } else if (mBundleAssetName == null || + !mDevSupportManager.hasBundleInAssets(mBundleAssetName)) { + // Bundle not available in assets, fetch from the server + mDevSupportManager.handleReloadJS(); + return; + } + } + // Use JS file from assets + recreateReactContext( + new JSCJavaScriptExecutor(), + JSBundleLoader.createAssetLoader( + mApplicationContext.getAssets(), + mBundleAssetName)); + } + + private void recreateReactContext( + JavaScriptExecutor jsExecutor, + JSBundleLoader jsBundleLoader) { + UiThreadUtil.assertOnUiThread(); + if (mCurrentReactContext != null) { + tearDownReactContext(mCurrentReactContext); + } + mCurrentReactContext = createReactContext(jsExecutor, jsBundleLoader); + for (ReactRootView rootView : mAttachedRootViews) { + attachMeasuredRootViewToInstance( + rootView, + mCurrentReactContext.getCatalystInstance()); + } + } + + private void attachMeasuredRootViewToInstance( + ReactRootView rootView, + CatalystInstance catalystInstance) { + UiThreadUtil.assertOnUiThread(); + + // Reset view content as it's going to be populated by the application content from JS + rootView.removeAllViews(); + rootView.setId(View.NO_ID); + + UIManagerModule uiManagerModule = catalystInstance.getNativeModule(UIManagerModule.class); + int rootTag = uiManagerModule.addMeasuredRootView(rootView); + @Nullable Bundle launchOptions = rootView.getLaunchOptions(); + WritableMap initialProps = launchOptions != null + ? Arguments.fromBundle(launchOptions) + : Arguments.createMap(); + String jsAppModuleName = rootView.getJSModuleName(); + + WritableNativeMap appParams = new WritableNativeMap(); + appParams.putDouble("rootTag", rootTag); + appParams.putMap("initialProps", initialProps); + catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams); + } + + private void detachViewFromInstance( + ReactRootView rootView, + CatalystInstance catalystInstance) { + UiThreadUtil.assertOnUiThread(); + catalystInstance.getJSModule(ReactNative.class) + .unmountComponentAtNodeAndRemoveContainer(rootView.getId()); + } + + private void tearDownReactContext(ReactContext reactContext) { + UiThreadUtil.assertOnUiThread(); + if (mLifecycleState == LifecycleState.RESUMED) { + reactContext.onPause(); + } + for (ReactRootView rootView : mAttachedRootViews) { + detachViewFromInstance(rootView, reactContext.getCatalystInstance()); + } + reactContext.onDestroy(); + mDevSupportManager.onReactInstanceDestroyed(reactContext); + } + + /** + * @return instance of {@link ReactContext} configured a {@link CatalystInstance} set + */ + private ReactApplicationContext createReactContext( + JavaScriptExecutor jsExecutor, + JSBundleLoader jsBundleLoader) { + NativeModuleRegistry.Builder nativeRegistryBuilder = new NativeModuleRegistry.Builder(); + JavaScriptModulesConfig.Builder jsModulesBuilder = new JavaScriptModulesConfig.Builder(); + + ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext); + if (mUseDeveloperSupport) { + reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager); + } + + CoreModulesPackage coreModulesPackage = + new CoreModulesPackage(this, mBackBtnHandler); + processPackage(coreModulesPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); + + // TODO(6818138): Solve use-case of native/js modules overriding + for (ReactPackage reactPackage : mPackages) { + processPackage(reactPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); + } + + CatalystInstance.Builder catalystInstanceBuilder = new CatalystInstance.Builder() + .setCatalystQueueConfigurationSpec(CatalystQueueConfigurationSpec.createDefault()) + .setJSExecutor(jsExecutor) + .setRegistry(nativeRegistryBuilder.build()) + .setJSModulesConfig(jsModulesBuilder.build()) + .setJSBundleLoader(jsBundleLoader) + .setNativeModuleCallExceptionHandler(mDevSupportManager); + + CatalystInstance catalystInstance = catalystInstanceBuilder.build(); + if (mBridgeIdleDebugListener != null) { + catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener); + } + + reactContext.initializeWithInstance(catalystInstance); + catalystInstance.initialize(); + mDevSupportManager.onNewReactContextCreated(reactContext); + + moveReactContextToCurrentLifecycleState(reactContext); + + return reactContext; + } + + private void processPackage( + ReactPackage reactPackage, + ReactApplicationContext reactContext, + NativeModuleRegistry.Builder nativeRegistryBuilder, + JavaScriptModulesConfig.Builder jsModulesBuilder) { + for (NativeModule nativeModule : reactPackage.createNativeModules(reactContext)) { + nativeRegistryBuilder.add(nativeModule); + } + for (Class jsModuleClass : reactPackage.createJSModules()) { + jsModulesBuilder.add(jsModuleClass); + } + } + + private void moveReactContextToCurrentLifecycleState(ReactApplicationContext reactContext) { + if (mLifecycleState == LifecycleState.RESUMED) { + reactContext.onResume(); + } + } + + /** + * Builder class for {@link ReactInstanceManager} + */ + public static class Builder { + + private final List mPackages = new ArrayList<>(); + + private @Nullable String mBundleAssetName; + private @Nullable String mJSMainModuleName; + private @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener; + private @Nullable Application mApplication; + private boolean mUseDeveloperSupport; + private @Nullable LifecycleState mInitialLifecycleState; + + private Builder() { + } + + /** + * Name of the JS budle file to be loaded from application's raw assets. + * Example: {@code "index.android.js"} + */ + public Builder setBundleAssetName(String bundleAssetName) { + mBundleAssetName = bundleAssetName; + return this; + } + + /** + * Path to your app's main module on the packager server. This is used when + * reloading JS during development. All paths are relative to the root folder + * the packager is serving files from. + * Examples: + * {@code "index.android"} or + * {@code "subdirectory/index.android"} + */ + public Builder setJSMainModuleName(String jsMainModuleName) { + mJSMainModuleName = jsMainModuleName; + return this; + } + + public Builder addPackage(ReactPackage reactPackage) { + mPackages.add(reactPackage); + return this; + } + + public Builder setBridgeIdleDebugListener( + NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener) { + mBridgeIdleDebugListener = bridgeIdleDebugListener; + return this; + } + + /** + * Required. This must be your {@code Application} instance. + */ + public Builder setApplication(Application application) { + mApplication = application; + return this; + } + + /** + * When {@code true}, developer options such as JS reloading and debugging are enabled. + * Note you still have to call {@link #showDevOptionsDialog} to show the dev menu, + * e.g. when the device Menu button is pressed. + */ + public Builder setUseDeveloperSupport(boolean useDeveloperSupport) { + mUseDeveloperSupport = useDeveloperSupport; + return this; + } + + /** + * Sets the initial lifecycle state of the host. For example, if the host is already resumed at + * creation time, we wouldn't expect an onResume call until we get an onPause call. + */ + public Builder setInitialLifecycleState(LifecycleState initialLifecycleState) { + mInitialLifecycleState = initialLifecycleState; + return this; + } + + /** + * Instantiates a new {@link ReactInstanceManager}. + * Before calling {@code build}, the following must be called: + *

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

    "); + return stringBuilder.toString(); + } + + public static CharSequence jsStackTraceToHtml(ReadableArray stack) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < stack.size(); i++) { + ReadableMap frame = stack.getMap(i); + String methodName = frame.getString("methodName"); + String fileName = new File(frame.getString("file")).getName(); + int lineNumber = frame.getInt("lineNumber"); + int columnNumber = -1; + if (frame.hasKey("column") && !frame.isNull("column")) { + columnNumber = frame.getInt("column"); + } + stringBuilder.append(getStackTraceHtmlComponent( + methodName, fileName, lineNumber, columnNumber)); + } + return Html.fromHtml(stringBuilder.toString()); + } + + public static CharSequence javaStackTraceToHtml(StackTraceElement[] stack) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i< stack.length; i++) { + stringBuilder.append(getStackTraceHtmlComponent( + stack[i].getMethodName(), stack[i].getFileName(), stack[i].getLineNumber(), -1)); + + } + return Html.fromHtml(stringBuilder.toString()); + } + + public static CharSequence debugServerExcStackTraceToHtml(DebugServerException e) { + String s = getStackTraceHtmlComponent("", e.fileName, e.lineNumber, e.column); + return Html.fromHtml(s); + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/FpsView.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/FpsView.java new file mode 100644 index 000000000..dfefbc10c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/FpsView.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.devsupport; + +import java.util.Locale; + +import android.annotation.TargetApi; +import android.view.Choreographer; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.facebook.common.logging.FLog; +import com.facebook.react.R; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.modules.debug.FpsDebugFrameCallback; + +/** + * View that automatically monitors and displays the current app frame rate. Also logs the current + * FPS to logcat while active. + * + * NB: Requires API 16 for use of FpsDebugFrameCallback. + */ +@TargetApi(16) +public class FpsView extends FrameLayout { + + private static final int UPDATE_INTERVAL_MS = 500; + + private final TextView mTextView; + private final FpsDebugFrameCallback mFrameCallback; + private final FPSMonitorRunnable mFPSMonitorRunnable; + + public FpsView(ReactContext reactContext) { + super(reactContext); + inflate(reactContext, R.layout.fps_view, this); + mTextView = (TextView) findViewById(R.id.fps_text); + mFrameCallback = new FpsDebugFrameCallback(Choreographer.getInstance(), reactContext); + mFPSMonitorRunnable = new FPSMonitorRunnable(); + setCurrentFPS(0, 0); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFrameCallback.reset(); + mFrameCallback.start(); + mFPSMonitorRunnable.start(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mFrameCallback.stop(); + mFPSMonitorRunnable.stop(); + } + + private void setCurrentFPS(double currentFPS, double currentJSFPS) { + String fpsString = String.format( + Locale.US, + "UI FPS: %.1f\nJS FPS: %.1f", + currentFPS, + currentJSFPS); + mTextView.setText(fpsString); + FLog.d(ReactConstants.TAG, fpsString); + } + + /** + * Timer that runs every UPDATE_INTERVAL_MS ms and updates the currently displayed FPS. + */ + private class FPSMonitorRunnable implements Runnable { + + private boolean mShouldStop = false; + + @Override + public void run() { + if (mShouldStop) { + return; + } + + setCurrentFPS(mFrameCallback.getFPS(), mFrameCallback.getJSFPS()); + mFrameCallback.reset(); + + postDelayed(this, UPDATE_INTERVAL_MS); + } + + public void start() { + mShouldStop = false; + post(this); + } + + public void stop() { + mShouldStop = true; + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevCommandsHandler.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevCommandsHandler.java new file mode 100644 index 000000000..5489853b0 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceDevCommandsHandler.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.devsupport; + +import com.facebook.react.bridge.ProxyJavaScriptExecutor; + +/** + * Interface used by {@link DevSupportManager} for requesting React instance recreation + * based on the option that user select in developers menu. + */ +public interface ReactInstanceDevCommandsHandler { + + /** + * Request react instance recreation with JS debugging enabled. + */ + void onReloadWithJSDebugger(ProxyJavaScriptExecutor proxyExecutor); + + /** + * Notify react instance manager about new JS bundle version downloaded from the server. + */ + void onJSBundleLoadedFromServer(); + + /** + * Request to toggle the react element inspector. + */ + void toggleElementInspector(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java new file mode 100644 index 000000000..1a3973a3d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.devsupport; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Typeface; +import android.text.method.ScrollingMovementMethod; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.widget.Button; +import android.widget.TextView; + +import com.facebook.react.R; + +/** + * Dialog for displaying JS errors in an eye-catching form (red box). + */ +/* package */ class RedBoxDialog extends Dialog { + + private final DevSupportManager mDevSupportManager; + + private TextView mTitle; + private TextView mDetails; + private Button mReloadJs; + private int mCookie = 0; + + protected RedBoxDialog(Context context, DevSupportManager devSupportManager) { + super(context, R.style.Theme_Catalyst_RedBox); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + + setContentView(R.layout.redbox_view); + + mDevSupportManager = devSupportManager; + + mTitle = (TextView) findViewById(R.id.catalyst_redbox_title); + mDetails = (TextView) findViewById(R.id.catalyst_redbox_details); + mDetails.setTypeface(Typeface.MONOSPACE); + mDetails.setHorizontallyScrolling(true); + mDetails.setMovementMethod(new ScrollingMovementMethod()); + mReloadJs = (Button) findViewById(R.id.catalyst_redbox_reloadjs); + mReloadJs.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mDevSupportManager.handleReloadJS(); + } + }); + } + + public void setTitle(String title) { + mTitle.setText(title); + } + + public void setDetails(CharSequence details) { + mDetails.setText(details); + } + + public void setErrorCookie(int cookie) { + mCookie = cookie; + } + + public int getErrorCookie() { + return mCookie; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU) { + mDevSupportManager.showDevOptionsDialog(); + return true; + } + + return super.onKeyUp(keyCode, event); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/common/ModuleDataCleaner.java b/ReactAndroid/src/main/java/com/facebook/react/modules/common/ModuleDataCleaner.java new file mode 100644 index 000000000..0e811e1a0 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/common/ModuleDataCleaner.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.common; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.common.ReactConstants; + +/** + * Cleans sensitive user data from native modules that implement the {@code Cleanable} interface. + * This is useful e.g. when a user logs out from an app. + */ +public class ModuleDataCleaner { + + /** + * Indicates a module may contain sensitive user data and should be cleaned on logout. + * + * Types of data that should be cleaned: + * - Persistent data (disk) that may contain user information or content. + * - Retained (static) in-memory data that may contain user info or content. + * + * Note that the following types of modules do not need to be cleaned here: + * - Modules whose user data is kept in memory in non-static fields, assuming the app uses a + * separate instance for each viewer context. + * - Modules that remove all persistent data (temp files, etc) when the catalyst instance is + * destroyed. This is because logout implies that the instance is destroyed. Apps should enforce + * this. + */ + public interface Cleanable { + + void clearSensitiveData(); + } + + public static void cleanDataFromModules(CatalystInstance catalystInstance) { + for (NativeModule nativeModule : catalystInstance.getNativeModules()) { + if (nativeModule instanceof Cleanable) { + FLog.d(ReactConstants.TAG, "Cleaning data from " + nativeModule.getName()); + ((Cleanable) nativeModule).clearSensitiveData(); + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/DefaultHardwareBackBtnHandler.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/DefaultHardwareBackBtnHandler.java new file mode 100644 index 000000000..55c2810bb --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/DefaultHardwareBackBtnHandler.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.core; + +/** + * Interface used by {@link DeviceEventManagerModule} to delegate hardware back button events. It's + * suppose to provide a default behavior since it would be triggered in the case when JS side + * doesn't want to handle back press events. + */ +public interface DefaultHardwareBackBtnHandler { + + /** + * By default, all onBackPress() calls should not execute the default backpress handler and should + * instead propagate it to the JS instance. If JS doesn't want to handle the back press itself, + * it shall call back into native to invoke this function which should execute the default handler + */ + void invokeDefaultOnBackPressed(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java new file mode 100644 index 000000000..1329a5b7c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.core; + +import javax.annotation.Nullable; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.UiThreadUtil; + +/** + * Native module that handles device hardware events like hardware back presses. + */ +public class DeviceEventManagerModule extends ReactContextBaseJavaModule { + + public static interface RCTDeviceEventEmitter extends JavaScriptModule { + void emit(String eventName, @Nullable Object data); + } + + private final Runnable mInvokeDefaultBackPressRunnable; + + public DeviceEventManagerModule( + ReactApplicationContext reactContext, + final DefaultHardwareBackBtnHandler backBtnHandler) { + super(reactContext); + mInvokeDefaultBackPressRunnable = new Runnable() { + @Override + public void run() { + UiThreadUtil.assertOnUiThread(); + backBtnHandler.invokeDefaultOnBackPressed(); + } + }; + } + + /** + * Sends an event to the JS instance that the hardware back has been pressed. + */ + public void emitHardwareBackPressed() { + getReactApplicationContext() + .getJSModule(RCTDeviceEventEmitter.class) + .emit("hardwareBackPress", null); + } + + /** + * Invokes the default back handler for the host of this catalyst instance. This should be invoked + * if JS does not want to handle the back press itself. + */ + @ReactMethod + public void invokeDefaultBackPressHandler() { + getReactApplicationContext().runOnUiQueueThread(mInvokeDefaultBackPressRunnable); + } + + @Override + public String getName() { + return "DeviceEventManager"; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java new file mode 100644 index 000000000..87ca48b39 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.core; + +import java.io.File; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.devsupport.DevSupportManager; +import com.facebook.react.common.ReactConstants; + +public class ExceptionsManagerModule extends BaseJavaModule { + + private final DevSupportManager mDevSupportManager; + + public ExceptionsManagerModule(DevSupportManager devSupportManager) { + mDevSupportManager = devSupportManager; + } + + @Override + public String getName() { + return "RKExceptionsManager"; + } + + private String stackTraceToString(ReadableArray stack) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < stack.size(); i++) { + ReadableMap frame = stack.getMap(i); + stringBuilder.append(frame.getString("methodName")); + stringBuilder.append("\n "); + stringBuilder.append(new File(frame.getString("file")).getName()); + stringBuilder.append(":"); + stringBuilder.append(frame.getInt("lineNumber")); + if (frame.hasKey("column") && !frame.isNull("column")) { + stringBuilder + .append(":") + .append(frame.getInt("column")); + } + stringBuilder.append("\n"); + } + return stringBuilder.toString(); + } + + @ReactMethod + public void reportFatalException(String title, ReadableArray details, int exceptionId) { + showOrThrowError(title, details, exceptionId); + } + + @ReactMethod + public void reportSoftException(String title, ReadableArray details) { + FLog.e(ReactConstants.TAG, title + "\n" + stackTraceToString(details)); + } + + private void showOrThrowError(String title, ReadableArray details, int exceptionId) { + if (mDevSupportManager.getDevSupportEnabled()) { + mDevSupportManager.showNewJSError(title, details, exceptionId); + } else { + throw new JavascriptException(stackTraceToString(details)); + } + } + + @ReactMethod + public void updateExceptionMessage(String title, ReadableArray details, int exceptionId) { + if (mDevSupportManager.getDevSupportEnabled()) { + mDevSupportManager.updateJSError(title, details, exceptionId); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java new file mode 100644 index 000000000..67f5ca231 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.core; + +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.WritableArray; + +public interface JSTimersExecution extends JavaScriptModule { + + public void callTimers(WritableArray timerIDs); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavascriptException.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavascriptException.java new file mode 100644 index 000000000..ef2fcb29d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavascriptException.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.core; + +/** + * A JS exception that was propagated to native. In debug mode, these exceptions are normally shown + * to developers in a redbox. + */ +public class JavascriptException extends RuntimeException { + + public JavascriptException(String jsStackTrace) { + super(jsStackTrace); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java new file mode 100644 index 000000000..326b6c58e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java @@ -0,0 +1,204 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.core; + +import javax.annotation.Nullable; + +import java.util.Comparator; +import java.util.PriorityQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +import android.util.SparseArray; +import android.view.Choreographer; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.uimanager.ReactChoreographer; +import com.facebook.react.common.SystemClock; +import com.facebook.infer.annotation.Assertions; + +/** + * Native module for JS timer execution. Timers fire on frame boundaries. + */ +public final class Timing extends ReactContextBaseJavaModule implements LifecycleEventListener { + + private static class Timer { + + private final int mCallbackID; + private final boolean mRepeat; + private final int mInterval; + private long mTargetTime; + + private Timer(int callbackID, long initialTargetTime, int duration, boolean repeat) { + mCallbackID = callbackID; + mTargetTime = initialTargetTime; + mInterval = duration; + mRepeat = repeat; + } + } + + private class FrameCallback implements Choreographer.FrameCallback { + + /** + * Calls all timers that have expired since the last time this frame callback was called. + */ + @Override + public void doFrame(long frameTimeNanos) { + if (isPaused.get()) { + return; + } + + long frameTimeMillis = frameTimeNanos / 1000000; + WritableArray timersToCall = null; + synchronized (mTimerGuard) { + while (!mTimers.isEmpty() && mTimers.peek().mTargetTime < frameTimeMillis) { + Timer timer = mTimers.poll(); + if (timersToCall == null) { + timersToCall = Arguments.createArray(); + } + timersToCall.pushInt(timer.mCallbackID); + if (timer.mRepeat) { + timer.mTargetTime = frameTimeMillis + timer.mInterval; + mTimers.add(timer); + } else { + mTimerIdsToTimers.remove(timer.mCallbackID); + } + } + } + + if (timersToCall != null) { + Assertions.assertNotNull(mJSTimersModule).callTimers(timersToCall); + } + + mReactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this); + } + } + + private final Object mTimerGuard = new Object(); + private final PriorityQueue mTimers; + private final SparseArray mTimerIdsToTimers; + private final AtomicBoolean isPaused = new AtomicBoolean(false); + private final ReactChoreographer mReactChoreographer; + private final FrameCallback mFrameCallback = new FrameCallback(); + private @Nullable JSTimersExecution mJSTimersModule; + private boolean mFrameCallbackPosted = false; + + public Timing(ReactApplicationContext reactContext) { + super(reactContext); + mReactChoreographer = ReactChoreographer.getInstance(); + // We store timers sorted by finish time. + mTimers = new PriorityQueue( + 11, // Default capacity: for some reason they don't expose a (Comparator) constructor + new Comparator() { + @Override + public int compare(Timer lhs, Timer rhs) { + long diff = lhs.mTargetTime - rhs.mTargetTime; + if (diff == 0) { + return 0; + } else if (diff < 0) { + return -1; + } else { + return 1; + } + } + }); + mTimerIdsToTimers = new SparseArray(); + } + + @Override + public void initialize() { + mJSTimersModule = getReactApplicationContext().getCatalystInstance() + .getJSModule(JSTimersExecution.class); + getReactApplicationContext().addLifecycleEventListener(this); + setChoreographerCallback(); + } + + @Override + public void onHostPause() { + isPaused.set(true); + clearChoreographerCallback(); + } + + @Override + public void onHostDestroy() { + clearChoreographerCallback(); + } + + @Override + public void onHostResume() { + isPaused.set(false); + // TODO(5195192) Investigate possible problems related to restarting all tasks at the same + // moment + setChoreographerCallback(); + } + + @Override + public void onCatalystInstanceDestroy() { + clearChoreographerCallback(); + } + + private void setChoreographerCallback() { + if (!mFrameCallbackPosted) { + mReactChoreographer.postFrameCallback( + ReactChoreographer.CallbackType.TIMERS_EVENTS, + mFrameCallback); + mFrameCallbackPosted = true; + } + } + + private void clearChoreographerCallback() { + if (mFrameCallbackPosted) { + mReactChoreographer.removeFrameCallback( + ReactChoreographer.CallbackType.TIMERS_EVENTS, + mFrameCallback); + mFrameCallbackPosted = false; + } + } + + @Override + public String getName() { + return "RKTiming"; + } + + @ReactMethod + public void createTimer( + final int callbackID, + final int duration, + final double jsSchedulingTime, + final boolean repeat) { + // Adjust for the amount of time it took for native to receive the timer registration call + long adjustedDuration = (long) Math.max( + 0, + jsSchedulingTime - SystemClock.currentTimeMillis() + duration); + long initialTargetTime = SystemClock.nanoTime() / 1000000 + adjustedDuration; + Timer timer = new Timer(callbackID, initialTargetTime, duration, repeat); + synchronized (mTimerGuard) { + mTimers.add(timer); + mTimerIdsToTimers.put(callbackID, timer); + } + } + + @ReactMethod + public void deleteTimer(int timerId) { + synchronized (mTimerGuard) { + Timer timer = mTimerIdsToTimers.get(timerId); + if (timer != null) { + // We may have already called/removed it + mTimerIdsToTimers.remove(timerId); + mTimers.remove(timer); + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/AnimationsDebugModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/AnimationsDebugModule.java new file mode 100644 index 000000000..6b914aae2 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/AnimationsDebugModule.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.debug; + +import javax.annotation.Nullable; + +import java.util.Locale; + +import android.os.Build; +import android.view.Choreographer; +import android.widget.Toast; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.JSApplicationCausedNativeException; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.common.ReactConstants; + +/** + * Module that records debug information during transitions (animated navigation events such as + * going from one screen to another). + */ +public class AnimationsDebugModule extends ReactContextBaseJavaModule { + + private @Nullable FpsDebugFrameCallback mFrameCallback; + private final DeveloperSettings mCatalystSettings; + + public AnimationsDebugModule( + ReactApplicationContext reactContext, + DeveloperSettings catalystSettings) { + super(reactContext); + mCatalystSettings = catalystSettings; + } + + @Override + public String getName() { + return "AnimationsDebugModule"; + } + + @ReactMethod + public void startRecordingFps() { + if (!mCatalystSettings.isAnimationFpsDebugEnabled()) { + return; + } + + if (mFrameCallback != null) { + throw new JSApplicationCausedNativeException("Already recording FPS!"); + } + checkAPILevel(); + + mFrameCallback = new FpsDebugFrameCallback( + Choreographer.getInstance(), + getReactApplicationContext()); + mFrameCallback.startAndRecordFpsAtEachFrame(); + } + + /** + * Called when an animation finishes. The caller should include the animation stop time in ms + * (unix time) so that we know when the animation stopped from the JS perspective and we don't + * count time after as being part of the animation. + */ + @ReactMethod + public void stopRecordingFps(double animationStopTimeMs) { + if (mFrameCallback == null) { + return; + } + checkAPILevel(); + + mFrameCallback.stop(); + + // Casting to long is safe here since animationStopTimeMs is unix time and thus relatively small + FpsDebugFrameCallback.FpsInfo fpsInfo = mFrameCallback.getFpsInfo((long) animationStopTimeMs); + + if (fpsInfo == null) { + Toast.makeText(getReactApplicationContext(), "Unable to get FPS info", Toast.LENGTH_LONG); + } else { + String fpsString = String.format( + Locale.US, + "FPS: %.2f, %d frames (%d expected)", + fpsInfo.fps, + fpsInfo.totalFrames, + fpsInfo.totalExpectedFrames); + String jsFpsString = String.format( + Locale.US, + "JS FPS: %.2f, %d frames (%d expected)", + fpsInfo.jsFps, + fpsInfo.totalJsFrames, + fpsInfo.totalExpectedFrames); + String debugString = fpsString + "\n" + jsFpsString + "\n" + + "Total Time MS: " + String.format(Locale.US, "%d", fpsInfo.totalTimeMs); + FLog.d(ReactConstants.TAG, debugString); + Toast.makeText(getReactApplicationContext(), debugString, Toast.LENGTH_LONG).show(); + } + + mFrameCallback = null; + } + + @Override + public void onCatalystInstanceDestroy() { + if (mFrameCallback != null) { + mFrameCallback.stop(); + mFrameCallback = null; + } + } + + private static void checkAPILevel() { + if (Build.VERSION.SDK_INT < 16) { + throw new JSApplicationCausedNativeException( + "Animation debugging is not supported in API <16"); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DeveloperSettings.java b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DeveloperSettings.java new file mode 100644 index 000000000..2ecad91c3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DeveloperSettings.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.debug; + +/** + * Provides access to React Native developers settings. + */ +public interface DeveloperSettings { + + /** + * @return whether an overlay showing current FPS should be shown. + */ + boolean isFpsDebugEnabled(); + + /** + * @return Whether debug information about transitions should be displayed. + */ + boolean isAnimationFpsDebugEnabled(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.java b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.java new file mode 100644 index 000000000..28144b9af --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DidJSUpdateUiDuringFrameDetector.java @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.debug; + +import android.view.Choreographer; + +import com.facebook.react.bridge.ReactBridge; +import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; +import com.facebook.react.common.LongArray; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.debug.NotThreadSafeUiManagerDebugListener; + +/** + * Debug object that listens to bridge busy/idle events and UiManagerModule dispatches and uses it + * to calculate whether JS was able to update the UI during a given frame. After being installed + * on a {@link ReactBridge} and a {@link UIManagerModule}, + * {@link #getDidJSHitFrameAndCleanup} should be called once per frame via a + * {@link Choreographer.FrameCallback}. + */ +public class DidJSUpdateUiDuringFrameDetector implements NotThreadSafeBridgeIdleDebugListener, + NotThreadSafeUiManagerDebugListener { + + private final LongArray mTransitionToIdleEvents = LongArray.createWithInitialCapacity(20); + private final LongArray mTransitionToBusyEvents = LongArray.createWithInitialCapacity(20); + private final LongArray mViewHierarchyUpdateEnqueuedEvents = + LongArray.createWithInitialCapacity(20); + private final LongArray mViewHierarchyUpdateFinishedEvents = + LongArray.createWithInitialCapacity(20); + private volatile boolean mWasIdleAtEndOfLastFrame = true; + + @Override + public synchronized void onTransitionToBridgeIdle() { + mTransitionToIdleEvents.add(System.nanoTime()); + } + + @Override + public synchronized void onTransitionToBridgeBusy() { + mTransitionToBusyEvents.add(System.nanoTime()); + } + + @Override + public synchronized void onViewHierarchyUpdateEnqueued() { + mViewHierarchyUpdateEnqueuedEvents.add(System.nanoTime()); + } + + @Override + public synchronized void onViewHierarchyUpdateFinished() { + mViewHierarchyUpdateFinishedEvents.add(System.nanoTime()); + } + + /** + * Designed to be called from a {@link Choreographer.FrameCallback#doFrame} call. + * + * There are two 'success' cases that will cause {@link #getDidJSHitFrameAndCleanup} to + * return true for a given frame: + * + * 1) UIManagerModule finished dispatching a batched UI update on the UI thread during the frame. + * This means that during the next hierarchy traversal, new UI will be drawn if needed (good). + * 2) The bridge ended the frame idle (meaning there were no JS nor native module calls still in + * flight) AND there was no UiManagerModule update enqueued that didn't also finish. NB: if + * there was one enqueued that actually finished, we'd have case 1), so effectively we just + * look for whether one was enqueued. + * + * NB: This call can only be called once for a given frame time range because it cleans up + * events it recorded for that frame. + * + * NB2: This makes the assumption that onViewHierarchyUpdateEnqueued is called from the + * {@link UIManagerModule#onBatchComplete()}, e.g. while the bridge is still considered busy, + * which means there is no race condition where the bridge has gone idle but a hierarchy update is + * waiting to be enqueued. + * + * @param frameStartTimeNanos the time in nanos that the last frame started + * @param frameEndTimeNanos the time in nanos that the last frame ended + */ + public synchronized boolean getDidJSHitFrameAndCleanup( + long frameStartTimeNanos, + long frameEndTimeNanos) { + // Case 1: We dispatched a UI update + boolean finishedUiUpdate = hasEventBetweenTimestamps( + mViewHierarchyUpdateFinishedEvents, + frameStartTimeNanos, + frameEndTimeNanos); + boolean didEndFrameIdle = didEndFrameIdle(frameStartTimeNanos, frameEndTimeNanos); + + boolean hitFrame; + if (finishedUiUpdate) { + hitFrame = true; + } else { + // Case 2: Ended idle but no UI was enqueued during that frame + hitFrame = didEndFrameIdle && !hasEventBetweenTimestamps( + mViewHierarchyUpdateEnqueuedEvents, + frameStartTimeNanos, + frameEndTimeNanos); + } + + cleanUp(mTransitionToIdleEvents, frameEndTimeNanos); + cleanUp(mTransitionToBusyEvents, frameEndTimeNanos); + cleanUp(mViewHierarchyUpdateEnqueuedEvents, frameEndTimeNanos); + cleanUp(mViewHierarchyUpdateFinishedEvents, frameEndTimeNanos); + + mWasIdleAtEndOfLastFrame = didEndFrameIdle; + + return hitFrame; + } + + private static boolean hasEventBetweenTimestamps( + LongArray eventArray, + long startTime, + long endTime) { + for (int i = 0; i < eventArray.size(); i++) { + long time = eventArray.get(i); + if (time >= startTime && time < endTime) { + return true; + } + } + return false; + } + + private static long getLastEventBetweenTimestamps( + LongArray eventArray, + long startTime, + long endTime) { + long lastEvent = -1; + for (int i = 0; i < eventArray.size(); i++) { + long time = eventArray.get(i); + if (time >= startTime && time < endTime) { + lastEvent = time; + } else if (time >= endTime) { + break; + } + } + return lastEvent; + } + + private boolean didEndFrameIdle(long startTime, long endTime) { + long lastIdleTransition = getLastEventBetweenTimestamps( + mTransitionToIdleEvents, + startTime, + endTime); + long lastBusyTransition = getLastEventBetweenTimestamps( + mTransitionToBusyEvents, + startTime, + endTime); + + if (lastIdleTransition == -1 && lastBusyTransition == -1) { + return mWasIdleAtEndOfLastFrame; + } + + return lastIdleTransition > lastBusyTransition; + } + + private static void cleanUp(LongArray eventArray, long endTime) { + int size = eventArray.size(); + int indicesToRemove = 0; + for (int i = 0; i < size; i++) { + if (eventArray.get(i) < endTime) { + indicesToRemove++; + } + } + + if (indicesToRemove > 0) { + for (int i = 0; i < size - indicesToRemove; i++) { + eventArray.set(i, eventArray.get(i + indicesToRemove)); + } + eventArray.dropTail(indicesToRemove); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/FpsDebugFrameCallback.java b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/FpsDebugFrameCallback.java new file mode 100644 index 000000000..1e63f525d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/FpsDebugFrameCallback.java @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.debug; + +import javax.annotation.Nullable; + +import java.util.Map; +import java.util.TreeMap; + +import android.annotation.TargetApi; +import android.view.Choreographer; + +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.infer.annotation.Assertions; + +/** + * Each time a frame is drawn, records whether it should have expected any more callbacks since + * the last time a frame was drawn (i.e. was a frame skipped?). Uses this plus total elapsed time + * to determine FPS. Can also record total and expected frame counts, though NB, since the expected + * frame rate is estimated, the expected frame count will lose accuracy over time. + * + * Also records the JS FPS, i.e. the frames per second with which either JS updated the UI or was + * idle and not trying to update the UI. This is different from the FPS above since JS rendering is + * async. + * + * TargetApi 16 for use of Choreographer. + */ +@TargetApi(16) +public class FpsDebugFrameCallback implements Choreographer.FrameCallback { + + public static class FpsInfo { + + public final int totalFrames; + public final int totalJsFrames; + public final int totalExpectedFrames; + public final double fps; + public final double jsFps; + public final int totalTimeMs; + + public FpsInfo( + int totalFrames, + int totalJsFrames, + int totalExpectedFrames, + double fps, + double jsFps, + int totalTimeMs) { + this.totalFrames = totalFrames; + this.totalJsFrames = totalJsFrames; + this.totalExpectedFrames = totalExpectedFrames; + this.fps = fps; + this.jsFps = jsFps; + this.totalTimeMs = totalTimeMs; + } + } + + private static final double EXPECTED_FRAME_TIME = 16.9; + + private final Choreographer mChoreographer; + private final ReactContext mReactContext; + private final UIManagerModule mUIManagerModule; + private final DidJSUpdateUiDuringFrameDetector mDidJSUpdateUiDuringFrameDetector; + + private boolean mShouldStop = false; + private long mFirstFrameTime = -1; + private long mLastFrameTime = -1; + private int mNumFrameCallbacks = 0; + private int mNumFrameCallbacksWithBatchDispatches = 0; + private boolean mIsRecordingFpsInfoAtEachFrame = false; + private @Nullable TreeMap mTimeToFps; + + public FpsDebugFrameCallback(Choreographer choreographer, ReactContext reactContext) { + mChoreographer = choreographer; + mReactContext = reactContext; + mUIManagerModule = reactContext.getNativeModule(UIManagerModule.class); + mDidJSUpdateUiDuringFrameDetector = new DidJSUpdateUiDuringFrameDetector(); + } + + @Override + public void doFrame(long l) { + if (mShouldStop) { + return; + } + + if (mFirstFrameTime == -1) { + mFirstFrameTime = l; + } + + long lastFrameStartTime = mLastFrameTime; + mLastFrameTime = l; + + if (mDidJSUpdateUiDuringFrameDetector.getDidJSHitFrameAndCleanup( + lastFrameStartTime, + l)) { + mNumFrameCallbacksWithBatchDispatches++; + } + + mNumFrameCallbacks++; + + if (mIsRecordingFpsInfoAtEachFrame) { + Assertions.assertNotNull(mTimeToFps); + FpsInfo info = new FpsInfo( + getNumFrames(), + getNumJSFrames(), + getExpectedNumFrames(), + getFPS(), + getJSFPS(), + getTotalTimeMS()); + mTimeToFps.put(System.currentTimeMillis(), info); + } + + mChoreographer.postFrameCallback(this); + } + + public void start() { + mShouldStop = false; + mReactContext.getCatalystInstance().addBridgeIdleDebugListener( + mDidJSUpdateUiDuringFrameDetector); + mUIManagerModule.setUiManagerDebugListener(mDidJSUpdateUiDuringFrameDetector); + mChoreographer.postFrameCallback(this); + } + + public void startAndRecordFpsAtEachFrame() { + mTimeToFps = new TreeMap(); + mIsRecordingFpsInfoAtEachFrame = true; + start(); + } + + public void stop() { + mShouldStop = true; + mReactContext.getCatalystInstance().removeBridgeIdleDebugListener( + mDidJSUpdateUiDuringFrameDetector); + mUIManagerModule.setUiManagerDebugListener(null); + } + + public double getFPS() { + if (mLastFrameTime == mFirstFrameTime) { + return 0; + } + return ((double) (getNumFrames()) * 1e9) / (mLastFrameTime - mFirstFrameTime); + } + + public double getJSFPS() { + if (mLastFrameTime == mFirstFrameTime) { + return 0; + } + return ((double) (getNumJSFrames()) * 1e9) / (mLastFrameTime - mFirstFrameTime); + } + + public int getNumFrames() { + return mNumFrameCallbacks - 1; + } + + public int getNumJSFrames() { + return mNumFrameCallbacksWithBatchDispatches - 1; + } + + public int getExpectedNumFrames() { + double totalTimeMS = getTotalTimeMS(); + int expectedFrames = (int) (totalTimeMS / EXPECTED_FRAME_TIME + 1); + return expectedFrames; + } + + public int getTotalTimeMS() { + return (int) ((double) mLastFrameTime - mFirstFrameTime) / 1000000; + } + + /** + * Returns the FpsInfo as if stop had been called at the given upToTimeMs. Only valid if + * monitoring was started with {@link #startAndRecordFpsAtEachFrame()}. + */ + public @Nullable FpsInfo getFpsInfo(long upToTimeMs) { + Assertions.assertNotNull(mTimeToFps, "FPS was not recorded at each frame!"); + Map.Entry bestEntry = mTimeToFps.floorEntry(upToTimeMs); + if (bestEntry == null) { + return null; + } + return bestEntry.getValue(); + } + + public void reset() { + mFirstFrameTime = -1; + mLastFrameTime = -1; + mNumFrameCallbacks = 0; + mNumFrameCallbacksWithBatchDispatches = 0; + mIsRecordingFpsInfoAtEachFrame = false; + mTimeToFps = null; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java new file mode 100644 index 000000000..07022a7e7 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.debug; + +import javax.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; + +/** + * Module that exposes the URL to the source code map (used for exception stack trace parsing) to JS + */ +public class SourceCodeModule extends BaseJavaModule { + + private final String mSourceMapUrl; + private final String mSourceUrl; + + public SourceCodeModule(String sourceUrl, String sourceMapUrl) { + mSourceMapUrl = sourceMapUrl; + mSourceUrl = sourceUrl; + } + + @Override + public String getName() { + return "RKSourceCode"; + } + + @ReactMethod + public void getScriptText(final Callback onSuccess, final Callback onError) { + WritableMap map = new WritableNativeMap(); + map.putString("fullSourceMappingURL", mSourceMapUrl); + onSuccess.invoke(map); + } + + @Override + public @Nullable Map getConstants() { + HashMap constants = new HashMap(); + constants.put("scriptURL", mSourceUrl); + return constants; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java new file mode 100644 index 000000000..20f163fe3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.fresco; + +import android.content.Context; + +import com.facebook.cache.common.CacheKey; +import com.facebook.common.internal.AndroidPredicates; +import com.facebook.common.soloader.SoLoaderShim; +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.imagepipeline.backends.okhttp.OkHttpImagePipelineConfigFactory; +import com.facebook.imagepipeline.core.ImagePipelineConfig; +import com.facebook.imagepipeline.core.ImagePipelineFactory; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.modules.common.ModuleDataCleaner; +import com.facebook.react.modules.network.OkHttpClientProvider; +import com.facebook.soloader.SoLoader; + +import com.squareup.okhttp.OkHttpClient; + +/** + * Module to initialize the Fresco library. + * + *

    Does not expose any methods to JavaScript code. For initialization and cleanup only. + */ +public class FrescoModule extends ReactContextBaseJavaModule implements + ModuleDataCleaner.Cleanable { + + public FrescoModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public void initialize() { + super.initialize(); + // Make sure the SoLoaderShim is configured to use our loader for native libraries. + // This code can be removed if using Fresco from Maven rather than from source + SoLoaderShim.setHandler( + new SoLoaderShim.Handler() { + @Override + public void loadLibrary(String libraryName) { + SoLoader.loadLibrary(libraryName); + } + }); + Context context = this.getReactApplicationContext().getApplicationContext(); + OkHttpClient okHttpClient = OkHttpClientProvider.getOkHttpClient(); + ImagePipelineConfig config = OkHttpImagePipelineConfigFactory + .newBuilder(context, okHttpClient) + .setDownsampleEnabled(false) + .build(); + Fresco.initialize(context, config); + } + + @Override + public String getName() { + return "FrescoModule"; + } + + @Override + public void clearSensitiveData() { + // Clear image cache. + ImagePipelineFactory imagePipelineFactory = Fresco.getImagePipelineFactory(); + imagePipelineFactory.getBitmapMemoryCache().removeAll(AndroidPredicates.True()); + imagePipelineFactory.getEncodedMemoryCache().removeAll(AndroidPredicates.True()); + imagePipelineFactory.getMainDiskStorageCache().clearAll(); + imagePipelineFactory.getSmallImageDiskStorageCache().clearAll(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java new file mode 100644 index 000000000..b957173cb --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java @@ -0,0 +1,289 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.network; + +import javax.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.GuardedAsyncTask; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.modules.network.OkHttpClientProvider; + +import com.squareup.okhttp.Headers; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.MultipartBuilder; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; + +/** + * Implements the XMLHttpRequest JavaScript interface. + */ +public final class NetworkingModule extends ReactContextBaseJavaModule { + + private static final String CONTENT_ENCODING_HEADER_NAME = "content-encoding"; + private static final String CONTENT_TYPE_HEADER_NAME = "content-type"; + private static final String REQUEST_BODY_KEY_STRING = "string"; + private static final String REQUEST_BODY_KEY_URI = "uri"; + private static final String REQUEST_BODY_KEY_FORMDATA = "formData"; + private static final String USER_AGENT_HEADER_NAME = "user-agent"; + + private final OkHttpClient mClient; + private final @Nullable String mDefaultUserAgent; + private boolean mShuttingDown; + + /* package */ NetworkingModule( + ReactApplicationContext reactContext, + @Nullable String defaultUserAgent, + OkHttpClient client) { + super(reactContext); + mClient = client; + mShuttingDown = false; + mDefaultUserAgent = defaultUserAgent; + } + + /** + * @param reactContext the ReactContext of the application + */ + public NetworkingModule(ReactApplicationContext reactContext) { + this(reactContext, null, OkHttpClientProvider.getOkHttpClient()); + } + + /** + * @param reactContext the ReactContext of the application + * @param defaultUserAgent the User-Agent header that will be set for all requests where the + * caller does not provide one explicitly + */ + public NetworkingModule(ReactApplicationContext reactContext, String defaultUserAgent) { + this(reactContext, defaultUserAgent, OkHttpClientProvider.getOkHttpClient()); + } + + @Override + public String getName() { + return "RCTNetworking"; + } + + @Override + public void onCatalystInstanceDestroy() { + mShuttingDown = true; + mClient.cancel(null); + } + + @ReactMethod + public void sendRequest( + String method, + String url, + int requestId, + ReadableArray headers, + ReadableMap data, + final Callback callback) { + // We need to call the callback to avoid leaking memory on JS even when input for sending + // request is erroneous or insufficient. For non-http based failures we use code 0, which is + // interpreted as a transport error. + // Callback accepts following arguments: responseCode, headersString, responseBody + + Request.Builder requestBuilder = new Request.Builder().url(url); + + if (requestId != 0) { + requestBuilder.tag(requestId); + } + + Headers requestHeaders = extractHeaders(headers, data); + if (requestHeaders == null) { + callback.invoke(0, null, "Unrecognized headers format"); + return; + } + String contentType = requestHeaders.get(CONTENT_TYPE_HEADER_NAME); + String contentEncoding = requestHeaders.get(CONTENT_ENCODING_HEADER_NAME); + requestBuilder.headers(requestHeaders); + + if (data == null) { + requestBuilder.method(method, null); + } else if (data.hasKey(REQUEST_BODY_KEY_STRING)) { + if (contentType == null) { + callback.invoke(0, null, "Payload is set but no content-type header specified"); + return; + } + String body = data.getString(REQUEST_BODY_KEY_STRING); + MediaType contentMediaType = MediaType.parse(contentType); + if (RequestBodyUtil.isGzipEncoding(contentEncoding)) { + RequestBody requestBody = RequestBodyUtil.createGzip(contentMediaType, body); + if (requestBody == null) { + callback.invoke(0, null, "Failed to gzip request body"); + return; + } + requestBuilder.method(method, requestBody); + } else { + requestBuilder.method(method, RequestBody.create(contentMediaType, body)); + } + } else if (data.hasKey(REQUEST_BODY_KEY_URI)) { + if (contentType == null) { + callback.invoke(0, null, "Payload is set but no content-type header specified"); + return; + } + String uri = data.getString(REQUEST_BODY_KEY_URI); + InputStream fileInputStream = + RequestBodyUtil.getFileInputStream(getReactApplicationContext(), uri); + if (fileInputStream == null) { + callback.invoke(0, null, "Could not retrieve file for uri " + uri); + return; + } + requestBuilder.method( + method, + RequestBodyUtil.create(MediaType.parse(contentType), fileInputStream)); + } else if (data.hasKey(REQUEST_BODY_KEY_FORMDATA)) { + if (contentType == null) { + contentType = "multipart/form-data"; + } + ReadableArray parts = data.getArray(REQUEST_BODY_KEY_FORMDATA); + MultipartBuilder multipartBuilder = constructMultipartBody(parts, contentType, callback); + if (multipartBuilder == null) { + return; + } + requestBuilder.method(method, multipartBuilder.build()); + } else { + // Nothing in data payload, at least nothing we could understand anyway. + // Ignore and treat it as if it were null. + requestBuilder.method(method, null); + } + + mClient.newCall(requestBuilder.build()).enqueue( + new com.squareup.okhttp.Callback() { + @Override + public void onFailure(Request request, IOException e) { + if (mShuttingDown) { + return; + } + // We need to call the callback to avoid leaking memory on JS even when input for + // sending request is erronous or insufficient. For non-http based failures we use + // code 0, which is interpreted as a transport error + callback.invoke(0, null, e.getMessage()); + } + + @Override + public void onResponse(Response response) throws IOException { + if (mShuttingDown) { + return; + } + // TODO(5472580) handle headers properly + String responseBody; + try { + responseBody = response.body().string(); + } catch (IOException e) { + // The stream has been cancelled or closed, nothing we can do + callback.invoke(0, null, e.getMessage()); + return; + } + callback.invoke(response.code(), null, responseBody); + } + }); + } + + @ReactMethod + public void abortRequest(final int requestId) { + // We have to use AsyncTask since this might trigger a NetworkOnMainThreadException, this is an + // open issue on OkHttp: https://github.com/square/okhttp/issues/869 + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + mClient.cancel(requestId); + } + }.execute(); + } + + private @Nullable MultipartBuilder constructMultipartBody( + ReadableArray body, + String contentType, + Callback callback) { + MultipartBuilder multipartBuilder = new MultipartBuilder(); + multipartBuilder.type(MediaType.parse(contentType)); + + for (int i = 0, size = body.size(); i < size; i++) { + ReadableMap bodyPart = body.getMap(i); + + // Determine part's content type. + ReadableArray headersArray = bodyPart.getArray("headers"); + Headers headers = extractHeaders(headersArray, null); + if (headers == null) { + callback.invoke(0, null, "Missing or invalid header format for FormData part."); + return null; + } + MediaType partContentType = null; + String partContentTypeStr = headers.get(CONTENT_TYPE_HEADER_NAME); + if (partContentTypeStr != null) { + partContentType = MediaType.parse(partContentTypeStr); + // Remove the content-type header because MultipartBuilder gets it explicitly as an + // argument and doesn't expect it in the headers array. + headers = headers.newBuilder().removeAll(CONTENT_TYPE_HEADER_NAME).build(); + } + + if (bodyPart.hasKey(REQUEST_BODY_KEY_STRING)) { + String bodyValue = bodyPart.getString(REQUEST_BODY_KEY_STRING); + multipartBuilder.addPart(headers, RequestBody.create(partContentType, bodyValue)); + } else if (bodyPart.hasKey(REQUEST_BODY_KEY_URI)) { + if (partContentType == null) { + callback.invoke(0, null, "Binary FormData part needs a content-type header."); + return null; + } + String fileContentUriStr = bodyPart.getString(REQUEST_BODY_KEY_URI); + InputStream fileInputStream = + RequestBodyUtil.getFileInputStream(getReactApplicationContext(), fileContentUriStr); + if (fileInputStream == null) { + callback.invoke(0, null, "Could not retrieve file for uri " + fileContentUriStr); + return null; + } + multipartBuilder.addPart(headers, RequestBodyUtil.create(partContentType, fileInputStream)); + } else { + callback.invoke(0, null, "Unrecognized FormData part."); + } + } + return multipartBuilder; + } + + /** + * Extracts the headers from the Array. If the format is invalid, this method will return null. + */ + private @Nullable Headers extractHeaders( + @Nullable ReadableArray headersArray, + @Nullable ReadableMap requestData) { + if (headersArray == null) { + return null; + } + Headers.Builder headersBuilder = new Headers.Builder(); + for (int headersIdx = 0, size = headersArray.size(); headersIdx < size; headersIdx++) { + ReadableArray header = headersArray.getArray(headersIdx); + if (header == null || header.size() != 2) { + return null; + } + String headerName = header.getString(0); + String headerValue = header.getString(1); + headersBuilder.add(headerName, headerValue); + } + if (headersBuilder.get(USER_AGENT_HEADER_NAME) == null && mDefaultUserAgent != null) { + headersBuilder.add(USER_AGENT_HEADER_NAME, mDefaultUserAgent); + } + + // Sanitize content encoding header, supported only when request specify payload as string + boolean isGzipSupported = requestData != null && requestData.hasKey(REQUEST_BODY_KEY_STRING); + if (!isGzipSupported) { + headersBuilder.removeAll(CONTENT_ENCODING_HEADER_NAME); + } + + return headersBuilder.build(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java new file mode 100644 index 000000000..fb7002013 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.network; + +import java.util.concurrent.TimeUnit; +import com.squareup.okhttp.OkHttpClient; + +/** + * Helper class that provides the same OkHttpClient instance that will be used for all networking + * requests. + */ +public class OkHttpClientProvider { + + // Centralized OkHttpClient for all networking requests. + private static OkHttpClient sClient; + + public static OkHttpClient getOkHttpClient() { + if (sClient == null) { + // TODO: #7108751 plug in stetho + sClient = new OkHttpClient(); + + // No timeouts by default + sClient.setConnectTimeout(0, TimeUnit.MILLISECONDS); + sClient.setReadTimeout(0, TimeUnit.MILLISECONDS); + sClient.setWriteTimeout(0, TimeUnit.MILLISECONDS); + } + return sClient; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java new file mode 100644 index 000000000..7ce69c37e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.network; + +import javax.annotation.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.GZIPOutputStream; + +import android.content.Context; +import android.net.Uri; + +import com.facebook.common.logging.FLog; +import com.facebook.react.common.ReactConstants; + +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.internal.Util; +import okio.BufferedSink; +import okio.Okio; +import okio.Source; + +/** + * Helper class that provides the necessary methods for creating the RequestBody from a file + * specification, such as a contentUri. + */ +/*package*/ class RequestBodyUtil { + + private static final String CONTENT_ENCODING_GZIP = "gzip"; + + /** + * Returns whether encode type indicates the body needs to be gzip-ed. + */ + public static boolean isGzipEncoding(@Nullable final String encodingType) { + return CONTENT_ENCODING_GZIP.equalsIgnoreCase(encodingType); + } + + /** + * Returns the input stream for a file given by its contentUri. Returns null if the file has not + * been found or if an error as occurred. + */ + public static @Nullable InputStream getFileInputStream( + Context context, + String fileContentUriStr) { + try { + Uri fileContentUri = Uri.parse(fileContentUriStr); + return context.getContentResolver().openInputStream(fileContentUri); + } catch (Exception e) { + FLog.e( + ReactConstants.TAG, + "Could not retrieve file for contentUri " + fileContentUriStr, + e); + return null; + } + } + + /** + * Creates a RequestBody from a mediaType and gzip-ed body string + */ + public static @Nullable RequestBody createGzip( + final MediaType mediaType, + final String body) { + ByteArrayOutputStream gzipByteArrayOutputStream = new ByteArrayOutputStream(); + try { + OutputStream gzipOutputStream = new GZIPOutputStream(gzipByteArrayOutputStream); + gzipOutputStream.write(body.getBytes()); + gzipOutputStream.close(); + } catch (IOException e) { + return null; + } + return RequestBody.create(mediaType, gzipByteArrayOutputStream.toByteArray()); + } + + /** + * Creates a RequestBody from a mediaType and inputStream given. + */ + public static RequestBody create(final MediaType mediaType, final InputStream inputStream) { + return new RequestBody() { + @Override + public MediaType contentType() { + return mediaType; + } + + @Override + public long contentLength() { + try { + return inputStream.available(); + } catch (IOException e) { + return 0; + } + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + Source source = null; + try { + source = Okio.source(inputStream); + sink.writeAll(source); + } finally { + Util.closeQuietly(source); + } + } + }; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java new file mode 100644 index 000000000..36340f0aa --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncLocalStorageUtil.java @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.storage; + +import javax.annotation.Nullable; + +import java.util.Arrays; +import java.util.Iterator; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; + +import com.facebook.react.bridge.ReadableArray; + +import org.json.JSONException; +import org.json.JSONObject; + +import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN; +import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST; +import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN; + +/** + * Helper for database operations. + */ +/* package */ class AsyncLocalStorageUtil { + + /** + * Build the String required for an SQL select statement: + * WHERE key IN (?, ?, ..., ?) + * without 'WHERE' and with selectionCount '?' + */ + /* package */ static String buildKeySelection(int selectionCount) { + String[] list = new String[selectionCount]; + Arrays.fill(list, "?"); + return KEY_COLUMN + " IN (" + TextUtils.join(", ", list) + ")"; + } + + /** + * Build the String[] arguments needed for an SQL selection, i.e.: + * {a, b, c} + * to be used in the SQL select statement: WHERE key in (?, ?, ?) + */ + /* package */ static String[] buildKeySelectionArgs(ReadableArray keys) { + String[] selectionArgs = new String[keys.size()]; + for (int keyIndex = 0; keyIndex < keys.size(); keyIndex++) { + selectionArgs[keyIndex] = keys.getString(keyIndex); + } + return selectionArgs; + } + + /** + * Returns the value of the given key, or null if not found. + */ + /* package */ static @Nullable String getItemImpl(SQLiteDatabase db, String key) { + String[] columns = {VALUE_COLUMN}; + String[] selectionArgs = {key}; + + Cursor cursor = db.query( + TABLE_CATALYST, + columns, + KEY_COLUMN + "=?", + selectionArgs, + null, + null, + null); + + try { + if (!cursor.moveToFirst()) { + return null; + } else { + return cursor.getString(0); + } + } finally { + cursor.close(); + } + } + + /** + * Sets the value for the key given, returns true if successful, false otherwise. + */ + /* package */ static boolean setItemImpl(SQLiteDatabase db, String key, String value) { + ContentValues contentValues = new ContentValues(); + contentValues.put(KEY_COLUMN, key); + contentValues.put(VALUE_COLUMN, value); + + long inserted = db.insertWithOnConflict( + TABLE_CATALYST, + null, + contentValues, + SQLiteDatabase.CONFLICT_REPLACE); + + return (-1 != inserted); + } + + /** + * Does the actual merge of the (key, value) pair with the value stored in the database. + * NB: This assumes that a database lock is already in effect! + * @return the errorCode of the operation + */ + /* package */ static boolean mergeImpl(SQLiteDatabase db, String key, String value) + throws JSONException { + String oldValue = getItemImpl(db, key); + String newValue; + + if (oldValue == null) { + newValue = value; + } else { + JSONObject oldJSON = new JSONObject(oldValue); + JSONObject newJSON = new JSONObject(value); + deepMergeInto(oldJSON, newJSON); + newValue = oldJSON.toString(); + } + + return setItemImpl(db, key, newValue); + } + + /** + * Merges two {@link JSONObject}s. The newJSON object will be merged with the oldJSON object by + * either overriding its values, or merging them (if the values of the same key in both objects + * are of type {@link JSONObject}). oldJSON will contain the result of this merge. + */ + private static void deepMergeInto(JSONObject oldJSON, JSONObject newJSON) + throws JSONException { + Iterator keys = newJSON.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + + JSONObject newJSONObject = newJSON.optJSONObject(key); + JSONObject oldJSONObject = oldJSON.optJSONObject(key); + if (newJSONObject != null && oldJSONObject != null) { + deepMergeInto(oldJSONObject, newJSONObject); + oldJSON.put(key, oldJSONObject); + } else { + oldJSON.put(key, newJSON.get(key)); + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageErrorUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageErrorUtil.java new file mode 100644 index 000000000..75f25617e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageErrorUtil.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.storage; + +import javax.annotation.Nullable; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; + +/** + * Helper class for database errors. + */ +public class AsyncStorageErrorUtil { + + /** + * Create Error object to be passed back to the JS callback. + */ + /* package */ static WritableMap getError(@Nullable String key, String errorMessage) { + WritableMap errorMap = Arguments.createMap(); + errorMap.putString("message", errorMessage); + if (key != null) { + errorMap.putString("key", key); + } + return errorMap; + } + + /* package */ static WritableMap getInvalidKeyError(@Nullable String key) { + return getError(key, "Invalid key"); + } + + /* package */ static WritableMap getInvalidValueError(@Nullable String key) { + return getError(key, "Invalid Value"); + } + + /* package */ static WritableMap getDBError(@Nullable String key) { + return getError(key, "Database Error"); + } + + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java new file mode 100644 index 000000000..601528a0f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java @@ -0,0 +1,369 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.storage; + +import javax.annotation.Nullable; + +import java.util.HashSet; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; + +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.GuardedAsyncTask; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.SetBuilder; +import com.facebook.react.modules.common.ModuleDataCleaner; + +import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.KEY_COLUMN; +import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.TABLE_CATALYST; +import static com.facebook.react.modules.storage.CatalystSQLiteOpenHelper.VALUE_COLUMN; + +public final class AsyncStorageModule + extends ReactContextBaseJavaModule implements ModuleDataCleaner.Cleanable { + + private @Nullable SQLiteDatabase mDb; + private boolean mShuttingDown = false; + + public AsyncStorageModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "AsyncSQLiteDBStorage"; + } + + @Override + public void initialize() { + super.initialize(); + mShuttingDown = false; + } + + @Override + public void onCatalystInstanceDestroy() { + mShuttingDown = true; + if (mDb != null && mDb.isOpen()) { + mDb.close(); + mDb = null; + } + } + + @Override + public void clearSensitiveData() { + // Clear local storage. If fails, crash, since the app is potentially in a bad state and could + // cause a privacy violation. We're still not recovering from this well, but at least the error + // will be reported to the server. + clear( + new Callback() { + @Override + public void invoke(Object... args) { + if (args.length > 0) { + throw new RuntimeException("Clearing AsyncLocalStorage failed: " + args[0]); + } + FLog.d(ReactConstants.TAG, "Cleaned AsyncLocalStorage."); + } + }); + } + + /** + * Given an array of keys, this returns a map of (key, value) pairs for the keys found, and + * (key, null) for the keys that haven't been found. + */ + @ReactMethod + public void multiGet(final ReadableArray keys, final Callback callback) { + if (keys == null) { + callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null), null); + return; + } + + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if (!ensureDatabase()) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null), null); + return; + } + + String[] columns = {KEY_COLUMN, VALUE_COLUMN}; + HashSet keysRemaining = SetBuilder.newHashSet(); + WritableArray data = Arguments.createArray(); + Cursor cursor = Assertions.assertNotNull(mDb).query( + TABLE_CATALYST, + columns, + AsyncLocalStorageUtil.buildKeySelection(keys.size()), + AsyncLocalStorageUtil.buildKeySelectionArgs(keys), + null, + null, + null); + + try { + if (cursor.getCount() != keys.size()) { + // some keys have not been found - insert them with null into the final array + for (int keyIndex = 0; keyIndex < keys.size(); keyIndex++) { + keysRemaining.add(keys.getString(keyIndex)); + } + } + + if (cursor.moveToFirst()) { + do { + WritableArray row = Arguments.createArray(); + row.pushString(cursor.getString(0)); + row.pushString(cursor.getString(1)); + data.pushArray(row); + keysRemaining.remove(cursor.getString(0)); + } while (cursor.moveToNext()); + + } + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Exception in database multiGet ", e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null); + } finally { + cursor.close(); + } + + for (String key : keysRemaining) { + WritableArray row = Arguments.createArray(); + row.pushString(key); + row.pushNull(); + data.pushArray(row); + } + keysRemaining.clear(); + callback.invoke(null, data); + } + }.execute(); + } + + /** + * Inserts multiple (key, value) pairs. If one or more of the pairs cannot be inserted, this will + * return AsyncLocalStorageFailure, but all other pairs will have been inserted. + * The insertion will replace conflicting (key, value) pairs. + */ + @ReactMethod + public void multiSet(final ReadableArray keyValueArray, final Callback callback) { + if (keyValueArray.size() == 0) { + callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null)); + return; + } + + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if (!ensureDatabase()) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null)); + return; + } + + String sql = "INSERT OR REPLACE INTO " + TABLE_CATALYST + " VALUES (?, ?);"; + SQLiteStatement statement = Assertions.assertNotNull(mDb).compileStatement(sql); + mDb.beginTransaction(); + try { + for (int idx=0; idx < keyValueArray.size(); idx++) { + if (keyValueArray.getArray(idx).size() != 2) { + callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null)); + return; + } + if (keyValueArray.getArray(idx).getString(0) == null) { + callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null)); + return; + } + if (keyValueArray.getArray(idx).getString(1) == null) { + callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null)); + return; + } + + statement.clearBindings(); + statement.bindString(1, keyValueArray.getArray(idx).getString(0)); + statement.bindString(2, keyValueArray.getArray(idx).getString(1)); + statement.execute(); + } + mDb.setTransactionSuccessful(); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Exception in database multiSet ", e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); + } finally { + mDb.endTransaction(); + } + callback.invoke(); + } + }.execute(); + } + + /** + * Removes all rows of the keys given. + */ + @ReactMethod + public void multiRemove(final ReadableArray keys, final Callback callback) { + if (keys.size() == 0) { + callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null)); + return; + } + + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if (!ensureDatabase()) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null)); + return; + } + + try { + Assertions.assertNotNull(mDb).delete( + TABLE_CATALYST, + AsyncLocalStorageUtil.buildKeySelection(keys.size()), + AsyncLocalStorageUtil.buildKeySelectionArgs(keys)); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Exception in database multiRemove ", e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); + } + callback.invoke(); + } + }.execute(); + } + + /** + * Given an array of (key, value) pairs, this will merge the given values with the stored values + * of the given keys, if they exist. + */ + @ReactMethod + public void multiMerge(final ReadableArray keyValueArray, final Callback callback) { + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if (!ensureDatabase()) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null)); + return; + } + Assertions.assertNotNull(mDb).beginTransaction(); + try { + for (int idx = 0; idx < keyValueArray.size(); idx++) { + if (keyValueArray.getArray(idx).size() != 2) { + callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null)); + return; + } + + if (keyValueArray.getArray(idx).getString(0) == null) { + callback.invoke(AsyncStorageErrorUtil.getInvalidKeyError(null)); + return; + } + + if (keyValueArray.getArray(idx).getString(1) == null) { + callback.invoke(AsyncStorageErrorUtil.getInvalidValueError(null)); + return; + } + + if (!AsyncLocalStorageUtil.mergeImpl( + mDb, + keyValueArray.getArray(idx).getString(0), + keyValueArray.getArray(idx).getString(1))) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null)); + return; + } + } + mDb.setTransactionSuccessful(); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, e.getMessage(), e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); + } finally { + mDb.endTransaction(); + } + callback.invoke(); + } + }.execute(); + } + + /** + * Clears the database. + */ + @ReactMethod + public void clear(final Callback callback) { + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if (!ensureDatabase()) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null)); + return; + } + try { + Assertions.assertNotNull(mDb).delete(TABLE_CATALYST, null, null); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Exception in database clear ", e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage())); + } + callback.invoke(); + } + }.execute(); + } + + /** + * Returns an array with all keys from the database. + */ + @ReactMethod + public void getAllKeys(final Callback callback) { + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if (!ensureDatabase()) { + callback.invoke(AsyncStorageErrorUtil.getDBError(null), null); + return; + } + WritableArray data = Arguments.createArray(); + String[] columns = {KEY_COLUMN}; + Cursor cursor = Assertions.assertNotNull(mDb) + .query(TABLE_CATALYST, columns, null, null, null, null, null); + try { + if (cursor.moveToFirst()) { + do { + data.pushString(cursor.getString(0)); + } while (cursor.moveToNext()); + } + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Exception in database getAllKeys ", e); + callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()), null); + } finally { + cursor.close(); + } + callback.invoke(null, data); + } + }.execute(); + } + + /** + * Verify the database exists and is open. + */ + private boolean ensureDatabase() { + if (mShuttingDown) { + return false; + } + if (mDb != null && mDb.isOpen()) { + return true; + } + mDb = initializeDatabase(); + return true; + } + + /** + * Create and/or open the database. + */ + private SQLiteDatabase initializeDatabase() { + CatalystSQLiteOpenHelper helperForDb = + new CatalystSQLiteOpenHelper(getReactApplicationContext()); + return helperForDb.getWritableDatabase(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/storage/CatalystSQLiteOpenHelper.java b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/CatalystSQLiteOpenHelper.java new file mode 100644 index 000000000..facf52e15 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/storage/CatalystSQLiteOpenHelper.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.storage; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +// VisibleForTesting +public class CatalystSQLiteOpenHelper extends SQLiteOpenHelper { + + // VisibleForTesting + public static final String DATABASE_NAME = "RKStorage"; + static final int DATABASE_VERSION = 1; + + static final String TABLE_CATALYST = "catalystLocalStorage"; + static final String KEY_COLUMN = "key"; + static final String VALUE_COLUMN = "value"; + + static final String VERSION_TABLE_CREATE = + "CREATE TABLE " + TABLE_CATALYST + " (" + + KEY_COLUMN + " TEXT PRIMARY KEY, " + + VALUE_COLUMN + " TEXT NOT NULL" + + ")"; + + private Context mContext; + + public CatalystSQLiteOpenHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + mContext = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(VERSION_TABLE_CREATE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // TODO: t5494781 implement data migration + if (oldVersion != newVersion) { + mContext.deleteDatabase(DATABASE_NAME); + onCreate(db); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java new file mode 100644 index 000000000..d07eb4a5f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.systeminfo; + +import javax.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +import android.os.Build; + +import com.facebook.react.bridge.BaseJavaModule; + +/** + * Module that exposes Android Constants to JS. + */ +public class AndroidInfoModule extends BaseJavaModule { + + @Override + public String getName() { + return "AndroidConstants"; + } + + @Override + public @Nullable Map getConstants() { + HashMap constants = new HashMap(); + constants.put("Version", Build.VERSION.SDK_INT); + return constants; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/toast/ToastModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/toast/ToastModule.java new file mode 100644 index 000000000..d401bfa1d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/toast/ToastModule.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.toast; + +import android.widget.Toast; + +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.common.MapBuilder; + +import java.util.Map; + +/** + * {@link NativeModule} that allows JS to show an Android Toast. + */ +public class ToastModule extends ReactContextBaseJavaModule { + + private static final String DURATION_SHORT_KEY = "SHORT"; + private static final String DURATION_LONG_KEY = "LONG"; + + public ToastModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "ToastAndroid"; + } + + @Override + public Map getConstants() { + final Map constants = MapBuilder.newHashMap(); + constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT); + constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG); + return constants; + } + + @ReactMethod + public void show(String message, int duration) { + Toast.makeText(getReactApplicationContext(), message, duration).show(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java new file mode 100644 index 000000000..65b7b38bc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.shell; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.modules.fresco.FrescoModule; +import com.facebook.react.modules.network.NetworkingModule; +import com.facebook.react.modules.storage.AsyncStorageModule; +import com.facebook.react.modules.toast.ToastModule; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.views.drawer.ReactDrawerLayoutManager; +import com.facebook.react.views.image.ReactImageManager; +import com.facebook.react.views.progressbar.ReactProgressBarViewManager; +import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager; +import com.facebook.react.views.scroll.ReactScrollViewManager; +import com.facebook.react.views.switchviewview.ReactSwitchManager; +import com.facebook.react.views.text.ReactRawTextManager; +import com.facebook.react.views.text.ReactTextViewManager; +import com.facebook.react.views.text.ReactVirtualTextViewManager; +import com.facebook.react.views.textinput.ReactTextInputManager; +import com.facebook.react.views.toolbar.ReactToolbarManager; +import com.facebook.react.views.view.ReactViewManager; + +/** + * Package defining basic modules and view managers. + */ +public class MainReactPackage implements ReactPackage { + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList( + new AsyncStorageModule(reactContext), + new FrescoModule(reactContext), + new NetworkingModule(reactContext), + new ToastModule(reactContext)); + } + + @Override + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Arrays.asList( + new ReactDrawerLayoutManager(), + new ReactHorizontalScrollViewManager(), + new ReactImageManager(), + new ReactProgressBarViewManager(), + new ReactRawTextManager(), + new ReactScrollViewManager(), + new ReactSwitchManager(), + new ReactTextInputManager(), + new ReactTextViewManager(), + new ReactToolbarManager(), + new ReactViewManager(), + new ReactVirtualTextViewManager()); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/touch/CatalystInterceptingViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/touch/CatalystInterceptingViewGroup.java new file mode 100644 index 000000000..6bc10f034 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/touch/CatalystInterceptingViewGroup.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.touch; + + +/** + * This interface should be implemented by all {@link ViewGroup} subviews that can be instantiating + * by {@link NativeViewHierarchyManager}. It is used to configure onInterceptTouch event listener + * which then is used to control touch event flow in cases in which they requested to be intercepted + * by some parent view based on a JS gesture detector. + */ +public interface CatalystInterceptingViewGroup { + + /** + * A {@link ViewGroup} instance that implement this interface is responsible for storing the + * listener passed as an argument and then calling + * {@link OnInterceptTouchEventListener#onInterceptTouchEvent} from + * {@link ViewGroup#onInterceptTouchEvent} and returning the result. If some custom handling of + * this method apply for the view, it should be called after the listener returns and only in + * a case when it returns false. + * + * @param listener A callback that {@link ViewGroup} should delegate calls for + * {@link ViewGroup#onInterceptTouchEvent} to + */ + public void setOnInterceptTouchEventListener(OnInterceptTouchEventListener listener); + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/touch/JSResponderHandler.java b/ReactAndroid/src/main/java/com/facebook/react/touch/JSResponderHandler.java new file mode 100644 index 000000000..2e8ba61f2 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/touch/JSResponderHandler.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.touch; + +import javax.annotation.Nullable; + +import android.view.MotionEvent; +import android.view.ViewGroup; +import android.view.ViewParent; + +/** + * This class coordinates JSResponder commands for {@link UIManagerModule}. It should be set as + * OnInterceptTouchEventListener for all newly created native views that implements + * {@link CatalystInterceptingViewGroup} and thanks to the information whether JSResponder is set + * and to which view it will correctly coordinate the return values of + * {@link OnInterceptTouchEventListener} such that touch events will be dispatched to the view + * selected by JS gesture recognizer. + * + * Single {@link CatalystInstance} should reuse same instance of this class. + */ +public class JSResponderHandler implements OnInterceptTouchEventListener { + + private static final int JS_RESPONDER_UNSET = -1; + + private volatile int mCurrentJSResponder = JS_RESPONDER_UNSET; + // We're holding on to the ViewParent that blocked native responders so that we can clear it + // when we change or clear the current JS responder. + private @Nullable ViewParent mViewParentBlockingNativeResponder; + + public void setJSResponder(int tag, @Nullable ViewParent viewParentBlockingNativeResponder) { + mCurrentJSResponder = tag; + // We need to unblock the native responder first, otherwise we can get in a bad state: a + // ViewParent sets requestDisallowInterceptTouchEvent to true, which sets this setting to true + // to all of its ancestors. Now, if one of its ancestors sets requestDisallowInterceptTouchEvent + // to false, it unsets the setting for itself and all of its ancestors, which means that they + // can intercept events again. + maybeUnblockNativeResponder(); + if (viewParentBlockingNativeResponder != null) { + viewParentBlockingNativeResponder.requestDisallowInterceptTouchEvent(true); + mViewParentBlockingNativeResponder = viewParentBlockingNativeResponder; + } + } + + public void clearJSResponder() { + mCurrentJSResponder = JS_RESPONDER_UNSET; + maybeUnblockNativeResponder(); + } + + private void maybeUnblockNativeResponder() { + if (mViewParentBlockingNativeResponder != null) { + mViewParentBlockingNativeResponder.requestDisallowInterceptTouchEvent(false); + mViewParentBlockingNativeResponder = null; + } + } + + @Override + public boolean onInterceptTouchEvent(ViewGroup v, MotionEvent event) { + int currentJSResponder = mCurrentJSResponder; + if (currentJSResponder != JS_RESPONDER_UNSET && event.getAction() != MotionEvent.ACTION_UP) { + // Don't intercept ACTION_UP events. If we return true here than UP event will not be + // delivered. That is because intercepted touch events are converted into CANCEL events + // and make all further events to be delivered to the view that intercepted the event. + // Therefore since "UP" event is the last event in a gesture, we should just let it reach the + // original target that is a child view of {@param v}. + // http://developer.android.com/reference/android/view/ViewGroup.html#onInterceptTouchEvent(android.view.MotionEvent) + return v.getId() == currentJSResponder; + } + return false; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/touch/OnInterceptTouchEventListener.java b/ReactAndroid/src/main/java/com/facebook/react/touch/OnInterceptTouchEventListener.java new file mode 100644 index 000000000..299d2f4ae --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/touch/OnInterceptTouchEventListener.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.touch; + +import android.view.MotionEvent; +import android.view.ViewGroup; + +/** + * Interface definition for a callback to be invoked when a onInterceptTouch is called on a + * {@link ViewGroup}. + */ +public interface OnInterceptTouchEventListener { + + /** + * Called when a onInterceptTouch is invoked on a view group + * @param v The view group the onInterceptTouch has been called on + * @param event The motion event being dispatched down the hierarchy. + * @return Return true to steal motion event from the children and have the dispatched to this + * view, or return false to allow motion event to be delivered to children view + */ + public boolean onInterceptTouchEvent(ViewGroup v, MotionEvent event); + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityHelper.java new file mode 100644 index 000000000..036badada --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityHelper.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Button; +import android.widget.RadioButton; + +/** + * Helper class containing logic for setting accessibility View properties. + */ +/* package */ class AccessibilityHelper { + + private static final String BUTTON = "button"; + private static final String RADIOBUTTON_CHECKED = "radiobutton_checked"; + private static final String RADIOBUTTON_UNCHECKED = "radiobutton_unchecked"; + + private static final View.AccessibilityDelegate BUTTON_DELEGATE = + new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(Button.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(Button.class.getName()); + } + }; + + private static final View.AccessibilityDelegate RADIOBUTTON_CHECKED_DELEGATE = + new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(RadioButton.class.getName()); + event.setChecked(true); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(RadioButton.class.getName()); + info.setCheckable(true); + info.setChecked(true); + } + }; + + private static final View.AccessibilityDelegate RADIOBUTTON_UNCHECKED_DELEGATE = + new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(RadioButton.class.getName()); + event.setChecked(false); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(RadioButton.class.getName()); + info.setCheckable(true); + info.setChecked(false); + } + }; + + public static void updateAccessibilityComponentType(View view, String componentType) { + if (componentType == null) { + view.setAccessibilityDelegate(null); + return; + } + switch (componentType) { + case BUTTON: + view.setAccessibilityDelegate(BUTTON_DELEGATE); + break; + case RADIOBUTTON_CHECKED: + view.setAccessibilityDelegate(RADIOBUTTON_CHECKED_DELEGATE); + break; + case RADIOBUTTON_UNCHECKED: + view.setAccessibilityDelegate(RADIOBUTTON_UNCHECKED_DELEGATE); + break; + default: + view.setAccessibilityDelegate(null); + break; + } + } + + public static void sendAccessibilityEvent(View view, int eventType) { + view.sendAccessibilityEvent(eventType); + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/AndroidManifest.xml b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AndroidManifest.xml new file mode 100644 index 000000000..1b7a37bdb --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/AppRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AppRegistry.java new file mode 100644 index 000000000..dcf4457eb --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AppRegistry.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.WritableMap; + +/** + * JS module interface - main entry point for launching react application for a given key. + */ +public interface AppRegistry extends JavaScriptModule { + void runApplication(String appKey, WritableMap appParameters); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseCSSPropertyApplicator.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseCSSPropertyApplicator.java new file mode 100644 index 000000000..810abd520 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseCSSPropertyApplicator.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.Locale; + +import com.facebook.csslayout.CSSAlign; +import com.facebook.csslayout.CSSConstants; +import com.facebook.csslayout.CSSFlexDirection; +import com.facebook.csslayout.CSSJustify; +import com.facebook.csslayout.CSSNode; +import com.facebook.csslayout.CSSPositionType; +import com.facebook.csslayout.CSSWrap; +import com.facebook.csslayout.Spacing; + +/** + * Takes common style properties from JS and applies them to a given {@link CSSNode}. + */ +public class BaseCSSPropertyApplicator { + + private static final String PROP_ON_LAYOUT = "onLayout"; + + /** + * Takes the base props from updateView/manageChildren and applies any CSS styles (if they exist) + * to the given {@link CSSNode}. + * + * TODO(5241893): Add and test border CSS attributes + */ + public static void applyCSSProperties(ReactShadowNode cssNode, CatalystStylesDiffMap props) { + if (props.hasKey(ViewProps.WIDTH)) { + float width = props.getFloat(ViewProps.WIDTH, CSSConstants.UNDEFINED); + cssNode.setStyleWidth(CSSConstants.isUndefined(width) ? + width : PixelUtil.toPixelFromDIP(width)); + } + + if (props.hasKey(ViewProps.HEIGHT)) { + float height = props.getFloat(ViewProps.HEIGHT, CSSConstants.UNDEFINED); + cssNode.setStyleHeight(CSSConstants.isUndefined(height) ? + height : PixelUtil.toPixelFromDIP(height)); + } + + if (props.hasKey(ViewProps.LEFT)) { + float left = props.getFloat(ViewProps.LEFT, CSSConstants.UNDEFINED); + cssNode.setPositionLeft(CSSConstants.isUndefined(left) ? + left : PixelUtil.toPixelFromDIP(left)); + } + + if (props.hasKey(ViewProps.TOP)) { + float top = props.getFloat(ViewProps.TOP, CSSConstants.UNDEFINED); + cssNode.setPositionTop(CSSConstants.isUndefined(top) ? + top : PixelUtil.toPixelFromDIP(top)); + } + + if (props.hasKey(ViewProps.BOTTOM)) { + float bottom = props.getFloat(ViewProps.BOTTOM, CSSConstants.UNDEFINED); + cssNode.setPositionBottom(CSSConstants.isUndefined(bottom) ? + bottom : PixelUtil.toPixelFromDIP(bottom)); + } + + if (props.hasKey(ViewProps.RIGHT)) { + float right = props.getFloat(ViewProps.RIGHT, CSSConstants.UNDEFINED); + cssNode.setPositionRight(CSSConstants.isUndefined(right) ? + right : PixelUtil.toPixelFromDIP(right)); + } + + if (props.hasKey(ViewProps.FLEX)) { + cssNode.setFlex(props.getFloat(ViewProps.FLEX, 0.f)); + } + + if (props.hasKey(ViewProps.FLEX_DIRECTION)) { + String flexDirectionString = props.getString(ViewProps.FLEX_DIRECTION); + cssNode.setFlexDirection(flexDirectionString == null ? + CSSFlexDirection.COLUMN : CSSFlexDirection.valueOf( + flexDirectionString.toUpperCase(Locale.US))); + } + + if (props.hasKey(ViewProps.FLEX_WRAP)) { + String flexWrapString = props.getString(ViewProps.FLEX_WRAP); + cssNode.setWrap(flexWrapString == null ? + CSSWrap.NOWRAP : CSSWrap.valueOf(flexWrapString.toUpperCase(Locale.US))); + } + + if (props.hasKey(ViewProps.ALIGN_SELF)) { + String alignSelfString = props.getString(ViewProps.ALIGN_SELF); + cssNode.setAlignSelf(alignSelfString == null ? + CSSAlign.AUTO : CSSAlign.valueOf( + alignSelfString.toUpperCase(Locale.US).replace("-", "_"))); + } + + if (props.hasKey(ViewProps.ALIGN_ITEMS)) { + String alignItemsString = props.getString(ViewProps.ALIGN_ITEMS); + cssNode.setAlignItems(alignItemsString == null ? + CSSAlign.STRETCH : CSSAlign.valueOf( + alignItemsString.toUpperCase(Locale.US).replace("-", "_"))); + } + + if (props.hasKey(ViewProps.JUSTIFY_CONTENT)) { + String justifyContentString = props.getString(ViewProps.JUSTIFY_CONTENT); + cssNode.setJustifyContent(justifyContentString == null ? CSSJustify.FLEX_START + : CSSJustify.valueOf(justifyContentString.toUpperCase(Locale.US).replace("-", "_"))); + } + + for (int i = 0; i < ViewProps.MARGINS.length; i++) { + if (props.hasKey(ViewProps.MARGINS[i])) { + cssNode.setMargin( + ViewProps.PADDING_MARGIN_SPACING_TYPES[i], + PixelUtil.toPixelFromDIP(props.getFloat(ViewProps.MARGINS[i], 0.f))); + } + } + + for (int i = 0; i < ViewProps.PADDINGS.length; i++) { + if (props.hasKey(ViewProps.PADDINGS[i])) { + float value = props.getFloat(ViewProps.PADDINGS[i], CSSConstants.UNDEFINED); + cssNode.setPadding( + ViewProps.PADDING_MARGIN_SPACING_TYPES[i], + CSSConstants.isUndefined(value) ? value : PixelUtil.toPixelFromDIP(value)); + } + } + + for (int i = 0; i < ViewProps.BORDER_WIDTHS.length; i++) { + if (props.hasKey(ViewProps.BORDER_WIDTHS[i])) { + cssNode.setBorder( + ViewProps.BORDER_SPACING_TYPES[i], + PixelUtil.toPixelFromDIP(props.getFloat(ViewProps.BORDER_WIDTHS[i], 0.f))); + } + } + + if (props.hasKey(ViewProps.POSITION)) { + String positionString = props.getString(ViewProps.POSITION); + CSSPositionType positionType = positionString == null ? + CSSPositionType.RELATIVE : CSSPositionType.valueOf(positionString.toUpperCase(Locale.US)); + cssNode.setPositionType(positionType); + } + + if (props.hasKey(PROP_ON_LAYOUT)) { + cssNode.setShouldNotifyOnLayout(props.getBoolean(PROP_ON_LAYOUT, false)); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewPropertyApplicator.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewPropertyApplicator.java new file mode 100644 index 000000000..d8f00bfcc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewPropertyApplicator.java @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.Collections; +import java.util.Map; +import java.util.HashMap; + +import android.graphics.Color; +import android.os.Build; +import android.view.View; +import com.facebook.react.bridge.ReadableMap; + +/** + * Takes common view properties from JS and applies them to a given {@link View}. + */ +public class BaseViewPropertyApplicator { + + private static final String PROP_BACKGROUND_COLOR = ViewProps.BACKGROUND_COLOR; + private static final String PROP_DECOMPOSED_MATRIX = "decomposedMatrix"; + private static final String PROP_DECOMPOSED_MATRIX_ROTATE = "rotate"; + private static final String PROP_DECOMPOSED_MATRIX_SCALE_X = "scaleX"; + private static final String PROP_DECOMPOSED_MATRIX_SCALE_Y = "scaleY"; + private static final String PROP_DECOMPOSED_MATRIX_TRANSLATE_X = "translateX"; + private static final String PROP_DECOMPOSED_MATRIX_TRANSLATE_Y = "translateY"; + private static final String PROP_OPACITY = "opacity"; + private static final String PROP_RENDER_TO_HARDWARE_TEXTURE = "renderToHardwareTextureAndroid"; + private static final String PROP_ACCESSIBILITY_LABEL = "accessibilityLabel"; + private static final String PROP_ACCESSIBILITY_COMPONENT_TYPE = "accessibilityComponentType"; + private static final String PROP_ACCESSIBILITY_LIVE_REGION = "accessibilityLiveRegion"; + private static final String PROP_IMPORTANT_FOR_ACCESSIBILITY = "importantForAccessibility"; + + // DEPRECATED + private static final String PROP_ROTATION = "rotation"; + private static final String PROP_SCALE_X = "scaleX"; + private static final String PROP_SCALE_Y = "scaleY"; + private static final String PROP_TRANSLATE_X = "translateX"; + private static final String PROP_TRANSLATE_Y = "translateY"; + + /** + * Used to locate views in end-to-end (UI) tests. + */ + public static final String PROP_TEST_ID = "testID"; + + private static final Map mCommonProps; + static { + Map props = new HashMap(); + props.put(PROP_ACCESSIBILITY_LABEL, UIProp.Type.STRING); + props.put(PROP_ACCESSIBILITY_COMPONENT_TYPE, UIProp.Type.STRING); + props.put(PROP_ACCESSIBILITY_LIVE_REGION, UIProp.Type.STRING); + props.put(PROP_BACKGROUND_COLOR, UIProp.Type.STRING); + props.put(PROP_IMPORTANT_FOR_ACCESSIBILITY, UIProp.Type.STRING); + props.put(PROP_OPACITY, UIProp.Type.NUMBER); + props.put(PROP_ROTATION, UIProp.Type.NUMBER); + props.put(PROP_SCALE_X, UIProp.Type.NUMBER); + props.put(PROP_SCALE_Y, UIProp.Type.NUMBER); + props.put(PROP_TRANSLATE_X, UIProp.Type.NUMBER); + props.put(PROP_TRANSLATE_Y, UIProp.Type.NUMBER); + props.put(PROP_TEST_ID, UIProp.Type.STRING); + props.put(PROP_RENDER_TO_HARDWARE_TEXTURE, UIProp.Type.BOOLEAN); + mCommonProps = Collections.unmodifiableMap(props); + } + + public static Map getCommonProps() { + return mCommonProps; + } + + public static void applyCommonViewProperties(View view, CatalystStylesDiffMap props) { + if (props.hasKey(PROP_BACKGROUND_COLOR)) { + String backgroundString = props.getString(PROP_BACKGROUND_COLOR); + if (backgroundString == null) { + view.setBackgroundColor(Color.TRANSPARENT); + } else { + view.setBackgroundColor(CSSColorUtil.getColor(backgroundString)); + } + } + if (props.hasKey(PROP_DECOMPOSED_MATRIX)) { + ReadableMap decomposedMatrix = props.getMap(PROP_DECOMPOSED_MATRIX); + if (decomposedMatrix == null) { + resetTransformMatrix(view); + } else { + setTransformMatrix(view, decomposedMatrix); + } + } + if (props.hasKey(PROP_OPACITY)) { + view.setAlpha(props.getFloat(PROP_OPACITY, 1.f)); + } + if (props.hasKey(PROP_RENDER_TO_HARDWARE_TEXTURE)) { + boolean useHWTexture = props.getBoolean(PROP_RENDER_TO_HARDWARE_TEXTURE, false); + view.setLayerType(useHWTexture ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE, null); + } + + if (props.hasKey(PROP_TEST_ID)) { + view.setTag(props.getString(PROP_TEST_ID)); + } + + if (props.hasKey(PROP_ACCESSIBILITY_LABEL)) { + view.setContentDescription(props.getString(PROP_ACCESSIBILITY_LABEL)); + } + if (props.hasKey(PROP_ACCESSIBILITY_COMPONENT_TYPE)) { + AccessibilityHelper.updateAccessibilityComponentType( + view, + props.getString(PROP_ACCESSIBILITY_COMPONENT_TYPE)); + } + if (props.hasKey(PROP_ACCESSIBILITY_LIVE_REGION)) { + if (Build.VERSION.SDK_INT >= 19) { + String liveRegionString = props.getString(PROP_ACCESSIBILITY_LIVE_REGION); + if (liveRegionString == null || liveRegionString.equals("none")) { + view.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE); + } else if (liveRegionString.equals("polite")) { + view.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + } else if (liveRegionString.equals("assertive")) { + view.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE); + } + } + } + if (props.hasKey(PROP_IMPORTANT_FOR_ACCESSIBILITY)) { + String importantForAccessibility = props.getString(PROP_IMPORTANT_FOR_ACCESSIBILITY); + if (importantForAccessibility == null || importantForAccessibility.equals("auto")) { + view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); + } else if (importantForAccessibility.equals("yes")) { + view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } else if (importantForAccessibility.equals("no")) { + view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + } else if (importantForAccessibility.equals("no-hide-descendants")) { + view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } + } + + // DEPRECATED + if (props.hasKey(PROP_ROTATION)) { + view.setRotation(props.getFloat(PROP_ROTATION, 0)); + } + if (props.hasKey(PROP_SCALE_X)) { + view.setScaleX(props.getFloat(PROP_SCALE_X, 1.f)); + } + if (props.hasKey(PROP_SCALE_Y)) { + view.setScaleY(props.getFloat(PROP_SCALE_Y, 1.f)); + } + if (props.hasKey(PROP_TRANSLATE_X)) { + view.setTranslationX(PixelUtil.toPixelFromDIP(props.getFloat(PROP_TRANSLATE_X, 0))); + } + if (props.hasKey(PROP_TRANSLATE_Y)) { + view.setTranslationY(PixelUtil.toPixelFromDIP(props.getFloat(PROP_TRANSLATE_Y, 0))); + } + } + + private static void setTransformMatrix(View view, ReadableMap matrix) { + view.setTranslationX(PixelUtil.toPixelFromDIP( + (float) matrix.getDouble(PROP_DECOMPOSED_MATRIX_TRANSLATE_X))); + view.setTranslationY(PixelUtil.toPixelFromDIP( + (float) matrix.getDouble(PROP_DECOMPOSED_MATRIX_TRANSLATE_Y))); + view.setRotation( + (float) matrix.getDouble(PROP_DECOMPOSED_MATRIX_ROTATE)); + view.setScaleX( + (float) matrix.getDouble(PROP_DECOMPOSED_MATRIX_SCALE_X)); + view.setScaleY( + (float) matrix.getDouble(PROP_DECOMPOSED_MATRIX_SCALE_Y)); + } + + private static void resetTransformMatrix(View view) { + view.setTranslationX(PixelUtil.toPixelFromDIP(0)); + view.setTranslationY(PixelUtil.toPixelFromDIP(0)); + view.setRotation(0); + view.setScaleX(1); + view.setScaleY(1); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/CSSColorUtil.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/CSSColorUtil.java new file mode 100644 index 000000000..e61150ba9 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/CSSColorUtil.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import android.graphics.Color; + +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.common.annotations.VisibleForTesting; + +/** + * Translates the different color formats to their actual colors. + */ +public class CSSColorUtil { + + static final Pattern RGB_COLOR_PATTERN = + Pattern.compile("rgb\\(\\s*([0-9]{1,3}),\\s*([0-9]{1,3}),\\s*([0-9]{1,3})\\s*\\)"); + + static final Pattern RGBA_COLOR_PATTERN = Pattern.compile( + "rgba\\(\\s*([0-9]{1,3}),\\s*([0-9]{1,3}),\\s*([0-9]{1,3})\\s*,\\s*(0*(\\.\\d{1,3})?|1(\\.0+)?)\\)"); + + private static final HashMap sColorNameMap = new HashMap(); + + static { + // List of HTML4 colors: http://www.w3.org/TR/css3-color/#html4 + sColorNameMap.put("black", Color.argb(255, 0, 0, 0)); + sColorNameMap.put("silver", Color.argb(255, 192, 192, 192)); + sColorNameMap.put("gray", Color.argb(255, 128, 128, 128)); + sColorNameMap.put("grey", Color.argb(255, 128, 128, 128)); + sColorNameMap.put("white", Color.argb(255, 255, 255, 255)); + sColorNameMap.put("maroon", Color.argb(255, 128, 0, 0)); + sColorNameMap.put("red", Color.argb(255, 255, 0, 0)); + sColorNameMap.put("purple", Color.argb(255, 128, 0, 128)); + sColorNameMap.put("fuchsia", Color.argb(255, 255, 0, 255)); + sColorNameMap.put("green", Color.argb(255, 0, 128, 0)); + sColorNameMap.put("lime", Color.argb(255, 0, 255, 0)); + sColorNameMap.put("olive", Color.argb(255, 128, 128, 0)); + sColorNameMap.put("yellow", Color.argb(255, 255, 255, 0)); + sColorNameMap.put("navy", Color.argb(255, 0, 0, 128)); + sColorNameMap.put("blue", Color.argb(255, 0, 0, 255)); + sColorNameMap.put("teal", Color.argb(255, 0, 128, 128)); + sColorNameMap.put("aqua", Color.argb(255, 0, 255, 255)); + + // Extended colors + sColorNameMap.put("orange", Color.argb(255, 255, 165, 0)); + sColorNameMap.put("transparent", Color.argb(0, 0, 0, 0)); + } + + /** + * Parses the given color string and returns the corresponding color int value. + * + * The following color formats are supported: + *

      + *
    • #rgb - Example: "#F02" (will be expanded to "#FF0022")
    • + *
    • #rrggbb - Example: "#FF0022"
    • + *
    • rgb(r, g, b) - Example: "rgb(255, 0, 34)"
    • + *
    • rgba(r, g, b, a) - Example: "rgba(255, 0, 34, 0.2)"
    • + *
    • Color names - Example: "red" or "transparent"
    • + *
    + * @param colorString the string representation of the color + * @return the color int + */ + public static int getColor(String colorString) { + if (colorString.startsWith("rgb(")) { + Matcher rgbMatcher = RGB_COLOR_PATTERN.matcher(colorString); + if (rgbMatcher.matches()) { + return Color.rgb( + validateColorComponent(Integer.parseInt(rgbMatcher.group(1))), + validateColorComponent(Integer.parseInt(rgbMatcher.group(2))), + validateColorComponent(Integer.parseInt(rgbMatcher.group(3)))); + } else { + throw new JSApplicationIllegalArgumentException("Invalid color: " + colorString); + } + } else if (colorString.startsWith("rgba(")) { + Matcher rgbaMatcher = RGBA_COLOR_PATTERN.matcher(colorString); + if (rgbaMatcher.matches()) { + return Color.argb( + (int) (Float.parseFloat(rgbaMatcher.group(4)) * 255), + validateColorComponent(Integer.parseInt(rgbaMatcher.group(1))), + validateColorComponent(Integer.parseInt(rgbaMatcher.group(2))), + validateColorComponent(Integer.parseInt(rgbaMatcher.group(3)))); + } else { + throw new JSApplicationIllegalArgumentException("Invalid color: " + colorString); + } + } else if (colorString.startsWith("#")) { + if (colorString.length() == 4) { + int r = parseHexChar(colorString.charAt(1)); + int g = parseHexChar(colorString.charAt(2)); + int b = parseHexChar(colorString.charAt(3)); + + // double the character + // since parseHexChar only returns values from 0-15, we don't need & 0xff + r = r | (r << 4); + g = g | (g << 4); + b = b | (b << 4); + return Color.rgb(r, g, b); + } else { + // check if we have #RRGGBB + if (colorString.length() == 7) { + // Color.parseColor(...) can throw an IllegalArgumentException("Unknown color"). + // For consistency, we hide the original exception and throw our own exception instead. + try { + return Color.parseColor(colorString); + } catch (IllegalArgumentException ex) { + throw new JSApplicationIllegalArgumentException("Invalid color: " + colorString); + } + } else { + throw new JSApplicationIllegalArgumentException("Invalid color: " + colorString); + } + } + } else { + Integer color = sColorNameMap.get(colorString.toLowerCase()); + if (color != null) { + return color; + } + throw new JSApplicationIllegalArgumentException("Unknown color: " + colorString); + } + } + + /** + * Convert a single hex character (0-9, a-f, A-F) to a number (0-15). + * + * @param hexChar the hex character to convert + * @return the value between 0 and 15 + */ + @VisibleForTesting + /*package*/ static int parseHexChar(char hexChar) { + if (hexChar >= '0' && hexChar <= '9') { + return hexChar - '0'; + } else if (hexChar >= 'A' && hexChar <= 'F') { + return hexChar - 'A' + 10; + } else if (hexChar >= 'a' && hexChar <= 'f') { + return hexChar - 'a' + 10; + } + throw new JSApplicationIllegalArgumentException("Invalid hex character: " + hexChar); + } + + private static int validateColorComponent(int color) { + if (color < 0 || color > 255) { + throw new JSApplicationIllegalArgumentException("Invalid color component: " + color); + } + return color; + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/CatalystStylesDiffMap.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/CatalystStylesDiffMap.java new file mode 100644 index 000000000..80bdec219 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/CatalystStylesDiffMap.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import javax.annotation.Nullable; + +import android.view.View; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; + +/** + * Wrapper for {@link ReadableMap} which should be used for styles property map. It extends + * some of the accessor methods of {@link ReadableMap} by adding a default value property + * such that caller is enforced to provide a default value for a style property. + * + * Instances of this class are used to update {@link View} or {@link CSSNode} style properties. + * Since properties are generated by React framework based on what has been updated each value + * in this map should either be interpreted as a new value set for a style property or as a "reset + * this property to default" command in case when value is null (this is a way React communicates + * change in which the style key that was previously present in a map has been removed). + * + * NOTE: Accessor method with default value will throw an exception when the key is not present in + * the map. Style applicator logic should verify whether the key exists in the map using + * {@link #hasKey} before fetching the value. The motivation behind this is that in case when the + * updated style diff map doesn't contain a certain style key it means that the corresponding view + * property shouldn't be updated (whereas in all other cases it should be updated to the new value + * or the property should be reset). + */ +public class CatalystStylesDiffMap { + + /* package */ final ReadableMap mBackingMap; + + public CatalystStylesDiffMap(ReadableMap props) { + mBackingMap = props; + } + + public boolean hasKey(String name) { + return mBackingMap.hasKey(name); + } + + public boolean isNull(String name) { + return mBackingMap.isNull(name); + } + + public boolean getBoolean(String name, boolean restoreNullToDefaultValue) { + return mBackingMap.isNull(name) ? restoreNullToDefaultValue : mBackingMap.getBoolean(name); + } + + public double getDouble(String name, double restoreNullToDefaultValue) { + return mBackingMap.isNull(name) ? restoreNullToDefaultValue : mBackingMap.getDouble(name); + } + + public float getFloat(String name, float restoreNullToDefaultValue) { + return mBackingMap.isNull(name) ? + restoreNullToDefaultValue : (float) mBackingMap.getDouble(name); + } + + public int getInt(String name, int restoreNullToDefaultValue) { + return mBackingMap.isNull(name) ? restoreNullToDefaultValue : (int) mBackingMap.getDouble(name); + } + + @Nullable + public String getString(String name) { + return mBackingMap.getString(name); + } + + @Nullable + public ReadableArray getArray(String key) { + return mBackingMap.getArray(key); + } + + @Nullable + public ReadableMap getMap(String key) { + return mBackingMap.getMap(key); + } + + @Override + public String toString() { + return "{ " + getClass().getSimpleName() + ": " + mBackingMap.toString() + " }"; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java new file mode 100644 index 000000000..18135146e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.util.DisplayMetrics; + +/** + * Holds an instance of the current DisplayMetrics so we don't have to thread it through all the + * classes that need it. + */ +public class DisplayMetricsHolder { + + private static DisplayMetrics sCurrentDisplayMetrics; + + public static void setDisplayMetrics(DisplayMetrics displayMetrics) { + sCurrentDisplayMetrics = displayMetrics; + } + + public static DisplayMetrics getDisplayMetrics() { + return sCurrentDisplayMetrics; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/GuardedChoreographerFrameCallback.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/GuardedChoreographerFrameCallback.java new file mode 100644 index 000000000..7abbdae89 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/GuardedChoreographerFrameCallback.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.view.Choreographer; + +import com.facebook.react.bridge.ReactContext; + +/** + * Abstract base for a Choreographer FrameCallback that should have any RuntimeExceptions it throws + * handled by the {@link com.facebook.react.bridge.NativeModuleCallExceptionHandler} registered if + * the app is in dev mode. + */ +public abstract class GuardedChoreographerFrameCallback implements Choreographer.FrameCallback { + + private final ReactContext mReactContext; + + protected GuardedChoreographerFrameCallback(ReactContext reactContext) { + mReactContext = reactContext; + } + + @Override + public final void doFrame(long frameTimeNanos) { + try { + doFrameGuarded(frameTimeNanos); + } catch (RuntimeException e) { + mReactContext.handleException(e); + } + } + + /** + * Like the standard doFrame but RuntimeExceptions will be caught and passed to + * {@link com.facebook.react.bridge.ReactContext#handleException(RuntimeException)}. + */ + protected abstract void doFrameGuarded(long frameTimeNanos); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java new file mode 100644 index 000000000..d515ef10d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import com.facebook.react.bridge.JSApplicationCausedNativeException; + +/** + * An exception caused by JS requesting the UI manager to perform an illegal view operation. + */ +public class IllegalViewOperationException extends JSApplicationCausedNativeException { + + public IllegalViewOperationException(String msg) { + super(msg); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/MeasureSpecAssertions.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/MeasureSpecAssertions.java new file mode 100644 index 000000000..3247709e6 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/MeasureSpecAssertions.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.view.View; + +/** + * Shared utility for asserting on MeasureSpecs. + */ +public class MeasureSpecAssertions { + + public static final void assertExplicitMeasureSpec(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); + int heightMode = View.MeasureSpec.getMode(heightMeasureSpec); + + if (widthMode == View.MeasureSpec.UNSPECIFIED || heightMode == View.MeasureSpec.UNSPECIFIED) { + throw new IllegalStateException( + "A catalyst view must have an explicit width and height given to it. This should " + + "normally happen as part of the standard catalyst UI framework."); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java new file mode 100644 index 000000000..01b220102 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -0,0 +1,607 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.NotThreadSafe; + +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.PopupMenu; + +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.animation.Animation; +import com.facebook.react.animation.AnimationListener; +import com.facebook.react.animation.AnimationRegistry; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.SoftAssertions; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.touch.JSResponderHandler; +import com.facebook.react.uimanager.events.EventDispatcher; + +/** + * Delegate of {@link UIManagerModule} that owns the native view hierarchy and mapping between + * native view names used in JS and corresponding instances of {@link ViewManager}. The + * {@link UIManagerModule} communicates with this class by it's public interface methods: + * - {@link #updateProperties} + * - {@link #updateLayout} + * - {@link #createView} + * - {@link #manageChildren} + * executing all the scheduled UI operations at the end of JS batch. + * + * NB: All native view management methods listed above must be called from the UI thread. + * + * The {@link ReactContext} instance that is passed to views that this manager creates differs + * from the one that we pass as a constructor. Instead we wrap the provided instance of + * {@link ReactContext} in an instance of {@link ThemedReactContext} that additionally provide + * a correct theme based on the root view for a view tree that we attach newly created view to. + * Therefore this view manager will create a copy of {@link ThemedReactContext} that wraps + * the instance of {@link ReactContext} for each root view added to the manager (see + * {@link #addRootView}). + * + * TODO(5483031): Only dispatch updates when shadow views have changed + */ +@NotThreadSafe +/* package */ final class NativeViewHierarchyManager { + + private final AnimationRegistry mAnimationRegistry; + private final SparseArray mTagsToViews; + private final SparseArray mTagsToViewManagers; + private final SparseBooleanArray mRootTags; + private final SparseArray mRootViewsContext; + private final ViewManagerRegistry mViewManagers; + private final JSResponderHandler mJSResponderHandler = new JSResponderHandler(); + private final RootViewManager mRootViewManager = new RootViewManager(); + + public NativeViewHierarchyManager( + AnimationRegistry animationRegistry, + ViewManagerRegistry viewManagers) { + mAnimationRegistry = animationRegistry; + mViewManagers = viewManagers; + mTagsToViews = new SparseArray<>(); + mTagsToViewManagers = new SparseArray<>(); + mRootTags = new SparseBooleanArray(); + mRootViewsContext = new SparseArray<>(); + } + + public void updateProperties(int tag, CatalystStylesDiffMap props) { + UiThreadUtil.assertOnUiThread(); + + ViewManager viewManager = mTagsToViewManagers.get(tag); + if (viewManager == null) { + throw new IllegalViewOperationException("ViewManager for tag " + tag + " could not be found"); + } + + View viewToUpdate = mTagsToViews.get(tag); + if (viewToUpdate == null) { + throw new IllegalViewOperationException("Trying to update view with tag " + tag + + " which doesn't exist"); + } + viewManager.updateView(viewToUpdate, props); + } + + public void updateViewExtraData(int tag, Object extraData) { + UiThreadUtil.assertOnUiThread(); + + ViewManager viewManager = mTagsToViewManagers.get(tag); + if (viewManager == null) { + throw new IllegalViewOperationException("ViewManager for tag " + tag + " could not be found"); + } + + View viewToUpdate = mTagsToViews.get(tag); + if (viewToUpdate == null) { + throw new IllegalViewOperationException("Trying to update view with tag " + tag + " which " + + "doesn't exist"); + } + viewManager.updateExtraData(viewToUpdate, extraData); + } + + public void updateLayout( + int parentTag, + int tag, + int x, + int y, + int width, + int height) { + UiThreadUtil.assertOnUiThread(); + + View viewToUpdate = mTagsToViews.get(tag); + if (viewToUpdate == null) { + throw new IllegalViewOperationException("Trying to update view with tag " + tag + " which " + + "doesn't exist"); + } + + // Even though we have exact dimensions, we still call measure because some platform views (e.g. + // Switch) assume that method will always be called before onLayout and onDraw. They use it to + // calculate and cache information used in the draw pass. For most views, onMeasure can be + // stubbed out to only call setMeasuredDimensions. For ViewGroups, onLayout should be stubbed + // out to not recursively call layout on its children: React Native already handles doing that. + // + // Also, note measure and layout need to be called *after* all View properties have been updated + // because of caching and calculation that may occur in onMeasure and onLayout. Layout + // operations should also follow the native view hierarchy and go top to bottom for consistency + // with standard layout passes (some views may depend on this). + + viewToUpdate.measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)); + + // Check if the parent of the view has to layout the view, or the child has to lay itself out. + if (!mRootTags.get(parentTag)) { + ViewManager parentViewManager = mTagsToViewManagers.get(parentTag); + ViewGroupManager parentViewGroupManager; + if (parentViewManager instanceof ViewGroupManager) { + parentViewGroupManager = (ViewGroupManager) parentViewManager; + } else { + throw new IllegalViewOperationException("Trying to use view with tag " + tag + + " as a parent, but its Manager doesn't extends ViewGroupManager"); + } + if (parentViewGroupManager != null + && !parentViewGroupManager.needsCustomLayoutForChildren()) { + viewToUpdate.layout(x, y, x + width, y + height); + } + } else { + viewToUpdate.layout(x, y, x + width, y + height); + } + } + + public void createView( + int rootViewTagForContext, + int tag, + String className, + @Nullable CatalystStylesDiffMap initialProps) { + UiThreadUtil.assertOnUiThread(); + ViewManager viewManager = mViewManagers.get(className); + + View view = + viewManager.createView(mRootViewsContext.get(rootViewTagForContext), mJSResponderHandler); + mTagsToViews.put(tag, view); + mTagsToViewManagers.put(tag, viewManager); + + // Use android View id field to store React tag. This is possible since we don't inflate + // React views from layout xmls. Thus it is easier to just reuse that field instead of + // creating another (potentially much more expensive) mapping from view to React tag + view.setId(tag); + if (initialProps != null) { + viewManager.updateView(view, initialProps); + } + } + + private static String constructManageChildrenErrorMessage( + ViewGroup viewToManage, + ViewGroupManager viewManager, + @Nullable int[] indicesToRemove, + @Nullable ViewAtIndex[] viewsToAdd, + @Nullable int[] tagsToDelete) { + StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append("View tag:" + viewToManage.getId() + "\n"); + stringBuilder.append(" children(" + viewManager.getChildCount(viewToManage) + "): [\n"); + for (int index=0; index= 0; i--) { + int indexToRemove = indicesToRemove[i]; + if (indexToRemove < 0) { + throw new IllegalViewOperationException( + "Trying to remove a negative view index:" + + indexToRemove + " view tag: " + tag + "\n detail: " + + constructManageChildrenErrorMessage( + viewToManage, + viewManager, + indicesToRemove, + viewsToAdd, + tagsToDelete)); + } + if (indexToRemove >= viewManager.getChildCount(viewToManage)) { + throw new IllegalViewOperationException( + "Trying to remove a view index above child " + + "count " + indexToRemove + " view tag: " + tag + "\n detail: " + + constructManageChildrenErrorMessage( + viewToManage, + viewManager, + indicesToRemove, + viewsToAdd, + tagsToDelete)); + } + if (indexToRemove >= lastIndexToRemove) { + throw new IllegalViewOperationException( + "Trying to remove an out of order view index:" + + indexToRemove + " view tag: " + tag + "\n detail: " + + constructManageChildrenErrorMessage( + viewToManage, + viewManager, + indicesToRemove, + viewsToAdd, + tagsToDelete)); + } + View childView = viewManager.getChildAt(viewToManage, indicesToRemove[i]); + if (childView == null) { + throw new IllegalViewOperationException( + "Trying to remove a null view at index:" + + indexToRemove + " view tag: " + tag + "\n detail: " + + constructManageChildrenErrorMessage( + viewToManage, + viewManager, + indicesToRemove, + viewsToAdd, + tagsToDelete)); + } + viewManager.removeView(viewToManage, childView); + lastIndexToRemove = indexToRemove; + } + } + + if (viewsToAdd != null) { + for (int i = 0; i < viewsToAdd.length; i++) { + ViewAtIndex viewAtIndex = viewsToAdd[i]; + View viewToAdd = mTagsToViews.get(viewAtIndex.mTag); + if (viewToAdd == null) { + throw new IllegalViewOperationException( + "Trying to add unknown view tag: " + + viewAtIndex.mTag + "\n detail: " + + constructManageChildrenErrorMessage( + viewToManage, + viewManager, + indicesToRemove, + viewsToAdd, + tagsToDelete)); + } + viewManager.addView(viewToManage, viewToAdd, viewAtIndex.mIndex); + } + } + + if (tagsToDelete != null) { + for (int i = 0; i < tagsToDelete.length; i++) { + int tagToDelete = tagsToDelete[i]; + View viewToDestroy = mTagsToViews.get(tagToDelete); + if (viewToDestroy == null) { + throw new IllegalViewOperationException( + "Trying to destroy unknown view tag: " + + tagToDelete + "\n detail: " + + constructManageChildrenErrorMessage( + viewToManage, + viewManager, + indicesToRemove, + viewsToAdd, + tagsToDelete)); + } + dropView(viewToDestroy); + } + } + } + + /** + * See {@link UIManagerModule#addMeasuredRootView}. + * + * Must be called from the UI thread. + */ + public void addRootView( + int tag, + SizeMonitoringFrameLayout view, + ThemedReactContext themedContext) { + UiThreadUtil.assertOnUiThread(); + if (view.getId() != View.NO_ID) { + throw new IllegalViewOperationException( + "Trying to add a root view with an explicit id already set. React Native uses " + + "the id field to track react tags and will overwrite this field. If that is fine, " + + "explicitly overwrite the id field to View.NO_ID before calling addMeasuredRootView."); + } + + mTagsToViews.put(tag, view); + mTagsToViewManagers.put(tag, mRootViewManager); + mRootTags.put(tag, true); + mRootViewsContext.put(tag, themedContext); + view.setId(tag); + } + + /** + * Releases all references to given native View. + */ + private void dropView(View view) { + UiThreadUtil.assertOnUiThread(); + if (!mRootTags.get(view.getId())) { + // For non-root views we notify viewmanager with {@link ViewManager#onDropInstance} + Assertions.assertNotNull(mTagsToViewManagers.get(view.getId())).onDropViewInstance( + (ThemedReactContext) view.getContext(), + view); + } + ViewManager viewManager = mTagsToViewManagers.get(view.getId()); + if (view instanceof ViewGroup && viewManager instanceof ViewGroupManager) { + ViewGroup viewGroup = (ViewGroup) view; + ViewGroupManager viewGroupManager = (ViewGroupManager) viewManager; + for (int i = 0; i < viewGroupManager.getChildCount(viewGroup); i++) { + View child = viewGroupManager.getChildAt(viewGroup, i); + if (mTagsToViews.get(child.getId()) != null) { + dropView(child); + } + } + } + mTagsToViews.remove(view.getId()); + mTagsToViewManagers.remove(view.getId()); + } + + public void removeRootView(int rootViewTag) { + UiThreadUtil.assertOnUiThread(); + SoftAssertions.assertCondition( + mRootTags.get(rootViewTag), + "View with tag " + rootViewTag + " is not registered as a root view"); + View rootView = mTagsToViews.get(rootViewTag); + dropView(rootView); + mRootTags.delete(rootViewTag); + mRootViewsContext.remove(rootViewTag); + } + + /** + * Returns true on success, false on failure. If successful, after calling, output buffer will be + * {x, y, width, height}. + */ + public void measure(int tag, int[] outputBuffer) { + UiThreadUtil.assertOnUiThread(); + View v = mTagsToViews.get(tag); + if (v == null) { + throw new NoSuchNativeViewException("No native view for " + tag + " currently exists"); + } + + // Puts x/y in outputBuffer[0]/[1] + v.getLocationOnScreen(outputBuffer); + outputBuffer[2] = v.getWidth(); + outputBuffer[3] = v.getHeight(); + } + + public int findTargetTagForTouch(int reactTag, float touchX, float touchY) { + View view = mTagsToViews.get(reactTag); + if (view == null) { + throw new JSApplicationIllegalArgumentException("Could not find view with tag " + reactTag); + } + return TouchTargetHelper.findTargetTagForTouch(touchY, touchX, (ViewGroup) view); + } + + public void setJSResponder(int reactTag, boolean blockNativeResponder) { + SoftAssertions.assertCondition( + !mRootTags.get(reactTag), + "Cannot block native responder on " + reactTag + " that is a root view"); + ViewParent viewParent = blockNativeResponder ? mTagsToViews.get(reactTag).getParent() : null; + mJSResponderHandler.setJSResponder(reactTag, viewParent); + } + + public void clearJSResponder() { + mJSResponderHandler.clearJSResponder(); + } + + /* package */ void startAnimationForNativeView( + int reactTag, + Animation animation, + @Nullable final Callback animationCallback) { + UiThreadUtil.assertOnUiThread(); + View view = mTagsToViews.get(reactTag); + final int animationId = animation.getAnimationID(); + if (view != null) { + animation.setAnimationListener(new AnimationListener() { + @Override + public void onFinished() { + Animation removedAnimation = mAnimationRegistry.removeAnimation(animationId); + + // There's a chance that there was already a removeAnimation call enqueued on the main + // thread when this callback got enqueued on the main thread, but the Animation class + // should handle only calling one of onFinished and onCancel exactly once. + Assertions.assertNotNull(removedAnimation, "Animation was already removed somehow!"); + if (animationCallback != null) { + animationCallback.invoke(true); + } + } + + @Override + public void onCancel() { + Animation removedAnimation = mAnimationRegistry.removeAnimation(animationId); + + Assertions.assertNotNull(removedAnimation, "Animation was already removed somehow!"); + if (animationCallback != null) { + animationCallback.invoke(false); + } + } + }); + animation.start(view); + } else { + // TODO(5712813): cleanup callback in JS callbacks table in case of an error + throw new IllegalViewOperationException("View with tag " + reactTag + " not found"); + } + } + + public void dispatchCommand(int reactTag, int commandId, @Nullable ReadableArray args) { + UiThreadUtil.assertOnUiThread(); + View view = mTagsToViews.get(reactTag); + if (view == null) { + throw new IllegalViewOperationException("Trying to send command to a non-existing view " + + "with tag " + reactTag); + } + + ViewManager viewManager = mTagsToViewManagers.get(reactTag); + if (viewManager == null) { + throw new IllegalViewOperationException( + "ViewManager for view tag " + reactTag + " could not be found"); + } + + viewManager.receiveCommand(view, commandId, args); + } + + /** + * Show a {@link PopupMenu}. + * + * @param reactTag the tag of the anchor view (the PopupMenu is displayed next to this view); this + * needs to be the tag of a native view (shadow views can not be anchors) + * @param items the menu items as an array of strings + * @param success will be called with the position of the selected item as the first argument, or + * no arguments if the menu is dismissed + */ + public void showPopupMenu(int reactTag, ReadableArray items, Callback success) { + UiThreadUtil.assertOnUiThread(); + View anchor = mTagsToViews.get(reactTag); + if (anchor == null) { + throw new JSApplicationIllegalArgumentException("Could not find view with tag " + reactTag); + } + PopupMenu popupMenu = new PopupMenu(getReactContextForView(reactTag), anchor); + + Menu menu = popupMenu.getMenu(); + for (int i = 0; i < items.size(); i++) { + menu.add(Menu.NONE, Menu.NONE, i, items.getString(i)); + } + + PopupMenuCallbackHandler handler = new PopupMenuCallbackHandler(success); + popupMenu.setOnMenuItemClickListener(handler); + popupMenu.setOnDismissListener(handler); + + popupMenu.show(); + } + + private static class PopupMenuCallbackHandler implements PopupMenu.OnMenuItemClickListener, + PopupMenu.OnDismissListener { + + final Callback mSuccess; + boolean mConsumed = false; + + private PopupMenuCallbackHandler(Callback success) { + mSuccess = success; + } + + @Override + public void onDismiss(PopupMenu menu) { + if (!mConsumed) { + mSuccess.invoke(UIManagerModuleConstants.ACTION_DISMISSED); + mConsumed = true; + } + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (!mConsumed) { + mSuccess.invoke(UIManagerModuleConstants.ACTION_ITEM_SELECTED, item.getOrder()); + mConsumed = true; + return true; + } + return false; + } + } + + /** + * @return Themed React context for view with a given {@param reactTag} - in the case of root + * view it returns the context from {@link #mRootViewsContext} and all the other cases it gets the + * context directly from the view using {@link View#getContext}. + */ + private ThemedReactContext getReactContextForView(int reactTag) { + if (mRootTags.get(reactTag)) { + return Assertions.assertNotNull(mRootViewsContext.get(reactTag)); + } + View view = mTagsToViews.get(reactTag); + if (view == null) { + throw new JSApplicationIllegalArgumentException("Could not find view with tag " + reactTag); + } + return (ThemedReactContext) view.getContext(); + } + + public void sendAccessibilityEvent(int tag, int eventType) { + View view = mTagsToViews.get(tag); + if (view == null) { + throw new JSApplicationIllegalArgumentException("Could not find view with tag " + tag); + } + AccessibilityHelper.sendAccessibilityEvent(view, eventType); + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java new file mode 100644 index 000000000..94f6ccab6 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java @@ -0,0 +1,428 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import javax.annotation.Nullable; + +import android.util.SparseBooleanArray; + +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.ReadableMapKeySeyIterator; + +/** + * Class responsible for optimizing the native view hierarchy while still respecting the final UI + * product specified by JS. Basically, JS sends us a hierarchy of nodes that, while easy to reason + * about in JS, are very inefficient to translate directly to native views. This class sits in + * between {@link UIManagerModule}, which directly receives view commands from JS, and + * {@link UIViewOperationQueue}, which enqueues actual operations on the native view hierarchy. It + * is able to take instructions from UIManagerModule and output instructions to the native view + * hierarchy that achieve the same displayed UI but with fewer views. + * + * Currently this class is only used to remove layout-only views, that is to say views that only + * affect the positions of their children but do not draw anything themselves. These views are + * fairly common because 1) containers are used to do layouting via flexbox and 2) the return of + * each Component#render() call in JS must be exactly one view, which means views are often wrapped + * in a unnecessary layer of hierarchy. + * + * This optimization is implemented by keeping track of both the unoptimized JS hierarchy and the + * optimized native hierarchy in {@link ReactShadowNode}. + * + * This optimization is important for view hierarchy depth (which can cause stack overflows during + * view traversal for complex apps), memory usage, amount of time spent during GCs, + * and time-to-display. + * + * Some examples of the optimizations this class will do based on commands from JS: + * - Create a view with only layout props: a description of that view is created as a + * {@link ReactShadowNode} in UIManagerModule, but this class will not output any commands to + * create the view in the native view hierarchy. + * - Update a layout-only view to have non-layout props: before issuing the updateProperties call + * to the native view hierarchy, issue commands to create the view we optimized away move it into + * the view hierarchy + * - Manage the children of a view: multiple manageChildren calls for various parent views may be + * issued to the native view hierarchy depending on where the views being added/removed are + * attached in the optimized hierarchy + */ +public class NativeViewHierarchyOptimizer { + + private static final boolean ENABLED = true; + + private final UIViewOperationQueue mUIViewOperationQueue; + private final ShadowNodeRegistry mShadowNodeRegistry; + private final SparseBooleanArray mTagsWithLayoutVisited = new SparseBooleanArray(); + + public NativeViewHierarchyOptimizer( + UIViewOperationQueue uiViewOperationQueue, + ShadowNodeRegistry shadowNodeRegistry) { + mUIViewOperationQueue = uiViewOperationQueue; + mShadowNodeRegistry = shadowNodeRegistry; + } + + /** + * Handles a createView call. May or may not actually create a native view. + */ + public void handleCreateView( + ReactShadowNode node, + int rootViewTag, + @Nullable CatalystStylesDiffMap initialProps) { + if (!ENABLED) { + int tag = node.getReactTag(); + mUIViewOperationQueue.enqueueCreateView(rootViewTag, tag, node.getViewClass(), initialProps); + return; + } + + boolean isLayoutOnly = node.getViewClass().equals(ViewProps.VIEW_CLASS_NAME) && + isLayoutOnlyAndCollapsable(initialProps); + node.setIsLayoutOnly(isLayoutOnly); + + if (!isLayoutOnly) { + mUIViewOperationQueue.enqueueCreateView( + rootViewTag, + node.getReactTag(), + node.getViewClass(), + initialProps); + } + } + + /** + * Handles an updateView call. If a view transitions from being layout-only to not (or vice-versa) + * this could result in some number of additional createView and manageChildren calls. If the + * view is layout only, no updateView call will be dispatched to the native hierarchy. + */ + public void handleUpdateView( + ReactShadowNode node, + String className, + CatalystStylesDiffMap props) { + if (!ENABLED) { + mUIViewOperationQueue.enqueueUpdateProperties(node.getReactTag(), className, props); + return; + } + + boolean needsToLeaveLayoutOnly = node.isLayoutOnly() && !isLayoutOnlyAndCollapsable(props); + if (needsToLeaveLayoutOnly) { + transitionLayoutOnlyViewToNativeView(node, props); + } else if (!node.isLayoutOnly()) { + mUIViewOperationQueue.enqueueUpdateProperties(node.getReactTag(), className, props); + } + } + + /** + * Handles a manageChildren call. This may translate into multiple manageChildren calls for + * multiple other views. + * + * NB: the assumption for calling this method is that all corresponding ReactShadowNodes have + * been updated **but tagsToDelete have NOT been deleted yet**. This is because we need to use + * the metadata from those nodes to figure out the correct commands to dispatch. This is unlike + * all other calls on this class where we assume all operations on the shadow hierarchy have + * already completed by the time a corresponding method here is called. + */ + public void handleManageChildren( + ReactShadowNode nodeToManage, + int[] indicesToRemove, + int[] tagsToRemove, + ViewAtIndex[] viewsToAdd, + int[] tagsToDelete) { + if (!ENABLED) { + mUIViewOperationQueue.enqueueManageChildren( + nodeToManage.getReactTag(), + indicesToRemove, + viewsToAdd, + tagsToDelete); + return; + } + + // We operate on tagsToRemove instead of indicesToRemove because by the time this method is + // called, these views have already been removed from the shadow hierarchy and the indices are + // no longer useful to operate on + for (int i = 0; i < tagsToRemove.length; i++) { + int tagToRemove = tagsToRemove[i]; + boolean delete = false; + for (int j = 0; j < tagsToDelete.length; j++) { + if (tagsToDelete[j] == tagToRemove) { + delete = true; + break; + } + } + ReactShadowNode nodeToRemove = mShadowNodeRegistry.getNode(tagToRemove); + removeNodeFromParent(nodeToRemove, delete); + } + + for (int i = 0; i < viewsToAdd.length; i++) { + ViewAtIndex toAdd = viewsToAdd[i]; + ReactShadowNode nodeToAdd = mShadowNodeRegistry.getNode(toAdd.mTag); + addNodeToNode(nodeToManage, nodeToAdd, toAdd.mIndex); + } + } + + /** + * Handles an updateLayout call. All updateLayout calls are collected and dispatched at the end + * of a batch because updateLayout calls to layout-only nodes can necessitate multiple + * updateLayout calls for all its children. + */ + public void handleUpdateLayout(ReactShadowNode node) { + if (!ENABLED) { + mUIViewOperationQueue.enqueueUpdateLayout( + Assertions.assertNotNull(node.getParent()).getReactTag(), + node.getReactTag(), + node.getScreenX(), + node.getScreenY(), + node.getScreenWidth(), + node.getScreenHeight()); + return; + } + + applyLayoutBase(node); + } + + /** + * Processes the shadow hierarchy to dispatch all necessary updateLayout calls to the native + * hierarchy. Should be called after all updateLayout calls for a batch have been handled. + */ + public void onBatchComplete() { + mTagsWithLayoutVisited.clear(); + } + + private void addNodeToNode(ReactShadowNode parent, ReactShadowNode child, int index) { + int indexInNativeChildren = parent.getNativeOffsetForChild(parent.getChildAt(index)); + boolean parentIsLayoutOnly = parent.isLayoutOnly(); + boolean childIsLayoutOnly = child.isLayoutOnly(); + + // Switch on the four cases of: + // add (layout-only|not layout-only) to (layout-only|not layout-only) + if (!parentIsLayoutOnly && !childIsLayoutOnly) { + addNonLayoutNodeToNonLayoutNode(parent, child, indexInNativeChildren); + } else if (!childIsLayoutOnly) { + addNonLayoutOnlyNodeToLayoutOnlyNode(parent, child, indexInNativeChildren); + } else if (!parentIsLayoutOnly) { + addLayoutOnlyNodeToNonLayoutOnlyNode(parent, child, indexInNativeChildren); + } else { + addLayoutOnlyNodeToLayoutOnlyNode(parent, child, indexInNativeChildren); + } + } + + /** + * For handling node removal from manageChildren. In the case of removing a layout-only node, we + * need to instead recursively remove all its children from their native parents. + */ + private void removeNodeFromParent(ReactShadowNode nodeToRemove, boolean shouldDelete) { + ReactShadowNode nativeNodeToRemoveFrom = nodeToRemove.getNativeParent(); + + if (nativeNodeToRemoveFrom != null) { + int index = nativeNodeToRemoveFrom.indexOfNativeChild(nodeToRemove); + nativeNodeToRemoveFrom.removeNativeChildAt(index); + + mUIViewOperationQueue.enqueueManageChildren( + nativeNodeToRemoveFrom.getReactTag(), + new int[]{index}, + null, + shouldDelete ? new int[]{nodeToRemove.getReactTag()} : null); + } else { + for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) { + removeNodeFromParent(nodeToRemove.getChildAt(i), shouldDelete); + } + } + } + + private void addLayoutOnlyNodeToLayoutOnlyNode( + ReactShadowNode parent, + ReactShadowNode child, + int index) { + ReactShadowNode parentParent = parent.getParent(); + + // If the parent hasn't been attached to its parent yet, don't issue commands to the native + // hierarchy. We'll do that when the parent node actually gets attached somewhere. + if (parentParent == null) { + return; + } + + int transformedIndex = index + parentParent.getNativeOffsetForChild(parent); + if (parentParent.isLayoutOnly()) { + addLayoutOnlyNodeToLayoutOnlyNode(parentParent, child, transformedIndex); + } else { + addLayoutOnlyNodeToNonLayoutOnlyNode(parentParent, child, transformedIndex); + } + } + + private void addNonLayoutOnlyNodeToLayoutOnlyNode( + ReactShadowNode layoutOnlyNode, + ReactShadowNode nonLayoutOnlyNode, + int index) { + ReactShadowNode parent = layoutOnlyNode.getParent(); + + // If the parent hasn't been attached to its parent yet, don't issue commands to the native + // hierarchy. We'll do that when the parent node actually gets attached somewhere. + if (parent == null) { + return; + } + + int transformedIndex = index + parent.getNativeOffsetForChild(layoutOnlyNode); + if (parent.isLayoutOnly()) { + addNonLayoutOnlyNodeToLayoutOnlyNode(parent, nonLayoutOnlyNode, transformedIndex); + } else { + addNonLayoutNodeToNonLayoutNode(parent, nonLayoutOnlyNode, transformedIndex); + } + } + + private void addLayoutOnlyNodeToNonLayoutOnlyNode( + ReactShadowNode nonLayoutOnlyNode, + ReactShadowNode layoutOnlyNode, + int index) { + // Add all of the layout-only node's children to its parent instead + int currentIndex = index; + for (int i = 0; i < layoutOnlyNode.getChildCount(); i++) { + ReactShadowNode childToAdd = layoutOnlyNode.getChildAt(i); + Assertions.assertCondition(childToAdd.getNativeParent() == null); + + if (childToAdd.isLayoutOnly()) { + // Adding this layout-only child could result in adding multiple native views + int childCountBefore = nonLayoutOnlyNode.getNativeChildCount(); + addLayoutOnlyNodeToNonLayoutOnlyNode( + nonLayoutOnlyNode, + childToAdd, + currentIndex); + int childCountAfter = nonLayoutOnlyNode.getNativeChildCount(); + currentIndex += childCountAfter - childCountBefore; + } else { + addNonLayoutNodeToNonLayoutNode(nonLayoutOnlyNode, childToAdd, currentIndex); + currentIndex++; + } + } + } + + private void addNonLayoutNodeToNonLayoutNode( + ReactShadowNode parent, + ReactShadowNode child, + int index) { + parent.addNativeChildAt(child, index); + mUIViewOperationQueue.enqueueManageChildren( + parent.getReactTag(), + null, + new ViewAtIndex[]{new ViewAtIndex(child.getReactTag(), index)}, + null); + } + + private void applyLayoutBase(ReactShadowNode node) { + int tag = node.getReactTag(); + if (mTagsWithLayoutVisited.get(tag)) { + return; + } + mTagsWithLayoutVisited.put(tag, true); + + ReactShadowNode parent = node.getParent(); + + // We use screenX/screenY (which round to integer pixels) at each node in the hierarchy to + // emulate what the layout would look like if it were actually built with native views which + // have to have integral top/left/bottom/right values + int x = node.getScreenX(); + int y = node.getScreenY(); + + while (parent != null && parent.isLayoutOnly()) { + // TODO(7854667): handle and test proper clipping + x += Math.round(parent.getLayoutX()); + y += Math.round(parent.getLayoutY()); + + parent = parent.getParent(); + } + + applyLayoutRecursive(node, x, y); + } + + private void applyLayoutRecursive(ReactShadowNode toUpdate, int x, int y) { + if (!toUpdate.isLayoutOnly() && toUpdate.getNativeParent() != null) { + int tag = toUpdate.getReactTag(); + mUIViewOperationQueue.enqueueUpdateLayout( + toUpdate.getNativeParent().getReactTag(), + tag, + x, + y, + toUpdate.getScreenWidth(), + toUpdate.getScreenHeight()); + return; + } + + for (int i = 0; i < toUpdate.getChildCount(); i++) { + ReactShadowNode child = toUpdate.getChildAt(i); + int childTag = child.getReactTag(); + if (mTagsWithLayoutVisited.get(childTag)) { + continue; + } + mTagsWithLayoutVisited.put(childTag, true); + + int childX = child.getScreenX(); + int childY = child.getScreenY(); + + childX += x; + childY += y; + + applyLayoutRecursive(child, childX, childY); + } + } + + private void transitionLayoutOnlyViewToNativeView( + ReactShadowNode node, + @Nullable CatalystStylesDiffMap props) { + ReactShadowNode parent = node.getParent(); + if (parent == null) { + node.setIsLayoutOnly(false); + return; + } + + // First, remove the node from its parent. This causes the parent to update its native children + // count. The removeNodeFromParent call will cause all the view's children to be detached from + // their native parent. + int childIndex = parent.indexOf(node); + parent.removeChildAt(childIndex); + removeNodeFromParent(node, false); + + node.setIsLayoutOnly(false); + + // Create the view since it doesn't exist in the native hierarchy yet + mUIViewOperationQueue.enqueueCreateView( + node.getRootNode().getReactTag(), + node.getReactTag(), + node.getViewClass(), + props); + + // Add the node and all its children as if we are adding a new nodes + parent.addChildAt(node, childIndex); + addNodeToNode(parent, node, childIndex); + for (int i = 0; i < node.getChildCount(); i++) { + addNodeToNode(node, node.getChildAt(i), i); + } + + // Update layouts since the children of the node were offset by its x/y position previously. + // Bit of a hack: we need to update the layout of this node's children now that it's no longer + // layout-only, but we may still receive more layout updates at the end of this batch that we + // don't want to ignore. + Assertions.assertCondition(mTagsWithLayoutVisited.size() == 0); + applyLayoutBase(node); + for (int i = 0; i < node.getChildCount(); i++) { + applyLayoutBase(node.getChildAt(i)); + } + mTagsWithLayoutVisited.clear(); + } + + private static boolean isLayoutOnlyAndCollapsable(@Nullable CatalystStylesDiffMap props) { + if (props == null) { + return true; + } + + if (props.hasKey(ViewProps.COLLAPSABLE) && !props.getBoolean(ViewProps.COLLAPSABLE, true)) { + return false; + } + + ReadableMapKeySeyIterator keyIterator = props.mBackingMap.keySetIterator(); + while (keyIterator.hasNextKey()) { + if (!ViewProps.isLayoutOnly(keyIterator.nextKey())) { + return false; + } + } + return true; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NoSuchNativeViewException.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NoSuchNativeViewException.java new file mode 100644 index 000000000..d8c125a72 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NoSuchNativeViewException.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +/** + * Exception thrown when a class tries to access a native view by a tag that has no native view + * associated with it. + */ +public class NoSuchNativeViewException extends IllegalViewOperationException { + + public NoSuchNativeViewException(String detailMessage) { + super(detailMessage); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/OnLayoutEvent.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/OnLayoutEvent.java new file mode 100644 index 000000000..f553858bd --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/OnLayoutEvent.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event used to notify JS component about changes of its position or dimensions + */ +/* package */ class OnLayoutEvent extends Event { + + private final int mX, mY, mWidth, mHeight; + + protected OnLayoutEvent(int viewTag, int x, int y, int width, int height) { + super(viewTag, 0); + mX = x; + mY = y; + mWidth = width; + mHeight = height; + } + + @Override + public String getEventName() { + return "topLayout"; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + WritableMap layout = Arguments.createMap(); + layout.putDouble("x", PixelUtil.toDIPFromPixel(mX)); + layout.putDouble("y", PixelUtil.toDIPFromPixel(mY)); + layout.putDouble("width", PixelUtil.toDIPFromPixel(mWidth)); + layout.putDouble("height", PixelUtil.toDIPFromPixel(mHeight)); + + WritableMap event = Arguments.createMap(); + event.putMap("layout", layout); + event.putInt("target", getViewTag()); + + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), event); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.java new file mode 100644 index 000000000..bcb24667f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/PixelUtil.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.util.TypedValue; + +/** + * Android dp to pixel manipulation + */ +public class PixelUtil { + + /** + * Convert from DIP to PX + */ + public static float toPixelFromDIP(float value) { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + value, + DisplayMetricsHolder.getDisplayMetrics()); + } + + /** + * Convert from DIP to PX + */ + public static float toPixelFromDIP(double value) { + return toPixelFromDIP((float) value); + } + + /** + * Convert from SP to PX + */ + public static float toPixelFromSP(float value) { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + value, + DisplayMetricsHolder.getDisplayMetrics()); + } + + /** + * Convert from SP to PX + */ + public static float toPixelFromSP(double value) { + return toPixelFromSP((float) value); + } + + /** + * Convert from PX to DP + */ + public static float toDIPFromPixel(float value) { + return value / DisplayMetricsHolder.getDisplayMetrics().density; + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/PointerEvents.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/PointerEvents.java new file mode 100644 index 000000000..1d86fe749 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/PointerEvents.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +/** + * Possible values for pointer events that a view and its descendants should receive. See + * https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events for more info. + */ +public enum PointerEvents { + + /** + * Neither the container nor its children receive events. + */ + NONE, + + /** + * Container doesn't get events but all of its children do. + */ + BOX_NONE, + + /** + * Container gets events but none of its children do. + */ + BOX_ONLY, + + /** + * Container and all of its children receive touch events (like pointerEvents is unspecified). + */ + AUTO, + ; +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java new file mode 100644 index 000000000..5b23ceda7 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.ArrayDeque; + +import android.view.Choreographer; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.common.ReactConstants; + +/** + * A simple wrapper around Choreographer that allows us to control the order certain callbacks + * are executed within a given frame. The main difference is that we enforce this is accessed from + * the UI thread: this is because this ordering cannot be guaranteed across multiple threads. + */ +public class ReactChoreographer { + + public static enum CallbackType { + /** + * For use by {@link com.facebook.react.uimanager.UIManagerModule} + */ + DISPATCH_UI(0), + + /** + * Events that make JS do things. + */ + TIMERS_EVENTS(1), + ; + + private final int mOrder; + + private CallbackType(int order) { + mOrder = order; + } + + /*package*/ int getOrder() { + return mOrder; + } + } + + private static ReactChoreographer sInstance; + + public static ReactChoreographer getInstance() { + UiThreadUtil.assertOnUiThread(); + if (sInstance == null) { + sInstance = new ReactChoreographer(); + } + return sInstance; + } + + private final Choreographer mChoreographer; + private final ReactChoreographerDispatcher mReactChoreographerDispatcher; + private final ArrayDeque[] mCallbackQueues; + + private int mTotalCallbacks = 0; + private boolean mHasPostedCallback = false; + + private ReactChoreographer() { + mChoreographer = Choreographer.getInstance(); + mReactChoreographerDispatcher = new ReactChoreographerDispatcher(); + mCallbackQueues = new ArrayDeque[CallbackType.values().length]; + for (int i = 0; i < mCallbackQueues.length; i++) { + mCallbackQueues[i] = new ArrayDeque<>(); + } + } + + public void postFrameCallback(CallbackType type, Choreographer.FrameCallback frameCallback) { + UiThreadUtil.assertOnUiThread(); + mCallbackQueues[type.getOrder()].addLast(frameCallback); + mTotalCallbacks++; + Assertions.assertCondition(mTotalCallbacks > 0); + if (!mHasPostedCallback) { + mChoreographer.postFrameCallback(mReactChoreographerDispatcher); + mHasPostedCallback = true; + } + } + + public void removeFrameCallback(CallbackType type, Choreographer.FrameCallback frameCallback) { + UiThreadUtil.assertOnUiThread(); + if (mCallbackQueues[type.getOrder()].removeFirstOccurrence(frameCallback)) { + mTotalCallbacks--; + maybeRemoveFrameCallback(); + } else { + FLog.e(ReactConstants.TAG, "Tried to remove non-existent frame callback"); + } + } + + private void maybeRemoveFrameCallback() { + Assertions.assertCondition(mTotalCallbacks >= 0); + if (mTotalCallbacks == 0 && mHasPostedCallback) { + mChoreographer.removeFrameCallback(mReactChoreographerDispatcher); + mHasPostedCallback = false; + } + } + + private class ReactChoreographerDispatcher implements Choreographer.FrameCallback { + + @Override + public void doFrame(long frameTimeNanos) { + mHasPostedCallback = false; + for (int i = 0; i < mCallbackQueues.length; i++) { + int initialLength = mCallbackQueues[i].size(); + for (int callback = 0; callback < initialLength; callback++) { + mCallbackQueues[i].removeFirst().doFrame(frameTimeNanos); + mTotalCallbacks--; + } + } + maybeRemoveFrameCallback(); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactCompoundView.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactCompoundView.java new file mode 100644 index 000000000..13abb16e2 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactCompoundView.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.view.MotionEvent; +import android.view.View; + +/** + * This interface should be implemented be native {@link View} subclasses that can represent more + * than a single react node (e.g. TextView). It is use by touch event emitter for determining the + * react tag of the inner-view element that was touched. + */ +public interface ReactCompoundView { + + /** + * Return react tag for touched element. Event coordinates are relative to the view + * @param touchX the X touch coordinate relative to the view + * @param touchY the Y touch coordinate relative to the view + */ + int reactTagForTouch(float touchX, float touchY); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactInvalidPropertyException.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactInvalidPropertyException.java new file mode 100644 index 000000000..e219f18cc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactInvalidPropertyException.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +public class ReactInvalidPropertyException extends RuntimeException { + + public ReactInvalidPropertyException(String property, String value, String expectedValues) { + super("Invalid React property `" + property + "` with value `" + value + + "`, expected " + expectedValues); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactNative.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactNative.java new file mode 100644 index 000000000..2d99e79f5 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactNative.java @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import com.facebook.react.bridge.JavaScriptModule; + +/** + * JS module interface - used by UIManager to communicate with main React JS module methods + */ +public interface ReactNative extends JavaScriptModule { + void unmountComponentAtNodeAndRemoveContainer(int rootNodeTag); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactPointerEventsView.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactPointerEventsView.java new file mode 100644 index 000000000..e47e13e48 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactPointerEventsView.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.view.View; + +/** + * This interface should be implemented be native {@link View} subclasses that support pointer + * events handling. It is used to find the target View of a touch event. + */ +public interface ReactPointerEventsView { + + /** + * Return the PointerEvents of the View. + */ + PointerEvents getPointerEvents(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java new file mode 100644 index 000000000..c4d5aa7b7 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java @@ -0,0 +1,374 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import javax.annotation.Nullable; + +import java.util.ArrayList; + +import com.facebook.csslayout.CSSNode; +import com.facebook.infer.annotation.Assertions; + +/** + * Base node class for representing virtual tree of React nodes. Shadow nodes are used primarily + * for layouting therefore it extends {@link CSSNode} to allow that. They also help with handling + * Common base subclass of {@link CSSNode} for all layout nodes for react-based view. It extends + * {@link CSSNode} by adding additional capabilities. + * + * Instances of this class receive property updates from JS via @{link UIManagerModule}. Subclasses + * may use {@link #updateProperties} to persist some of the updated fields in the node instance that + * corresponds to a particular view type. + * + * Subclasses of {@link ReactShadowNode} should be created only from {@link ViewManager} that + * corresponds to a certain type of native view. They will be updated and accessed only from JS + * thread. Subclasses of {@link ViewManager} may choose to use base class {@link ReactShadowNode} or + * custom subclass of it if necessary. + * + * The primary use-case for {@link ReactShadowNode} nodes is to calculate layouting. Although this + * might be extended. For some examples please refer to ARTGroupCSSNode or ReactTextCSSNode. + * + * This class allows for the native view hierarchy to not be an exact copy of the hierarchy received + * from JS by keeping track of both JS children (e.g. {@link #getChildCount()} and separately native + * children (e.g. {@link #getNativeChildCount()}). See {@link NativeViewHierarchyOptimizer} for more + * information. + */ +public class ReactShadowNode extends CSSNode { + + private int mReactTag; + private @Nullable String mViewClassName; + private @Nullable ReactShadowNode mRootNode; + private @Nullable ThemedReactContext mThemedContext; + private boolean mShouldNotifyOnLayout; + private boolean mNodeUpdated = true; + + // layout-only nodes + private boolean mIsLayoutOnly; + private int mTotalNativeChildren = 0; + private @Nullable ReactShadowNode mNativeParent; + private @Nullable ArrayList mNativeChildren; + private float mAbsoluteLeft; + private float mAbsoluteTop; + private float mAbsoluteRight; + private float mAbsoluteBottom; + + /** + * Nodes that return {@code true} will be treated as "virtual" nodes. That is, nodes that are not + * mapped into native views (e.g. nested text node). By default this method returns {@code false}. + */ + public boolean isVirtual() { + return false; + } + + /** + * Nodes that return {@code true} will be treated as a root view for the virtual nodes tree. It + * means that {@link NativeViewHierarchyManager} will not try to perform {@code manageChildren} + * operation on such views. Good example is {@code InputText} view that may have children + * {@code Text} nodes but this whole hierarchy will be mapped to a single android {@link EditText} + * view. + */ + public boolean isVirtualAnchor() { + return false; + } + + public final String getViewClass() { + return Assertions.assertNotNull(mViewClassName); + } + + public final boolean hasUpdates() { + return mNodeUpdated || hasNewLayout() || isDirty(); + } + + public final void markUpdateSeen() { + mNodeUpdated = false; + if (hasNewLayout()) { + markLayoutSeen(); + } + } + + protected void markUpdated() { + if (mNodeUpdated) { + return; + } + mNodeUpdated = true; + ReactShadowNode parent = getParent(); + if (parent != null) { + parent.markUpdated(); + } + } + + @Override + protected void dirty() { + if (!isVirtual()) { + super.dirty(); + } + } + + @Override + public void addChildAt(CSSNode child, int i) { + super.addChildAt(child, i); + markUpdated(); + ReactShadowNode node = (ReactShadowNode) child; + + int increase = node.mIsLayoutOnly ? node.mTotalNativeChildren : 1; + mTotalNativeChildren += increase; + + if (mIsLayoutOnly) { + ReactShadowNode parent = getParent(); + while (parent != null) { + parent.mTotalNativeChildren += increase; + if (!parent.mIsLayoutOnly) { + break; + } + parent = parent.getParent(); + } + } + } + + @Override + public ReactShadowNode removeChildAt(int i) { + ReactShadowNode removed = (ReactShadowNode) super.removeChildAt(i); + markUpdated(); + + int decrease = removed.mIsLayoutOnly ? removed.mTotalNativeChildren : 1; + mTotalNativeChildren -= decrease; + if (mIsLayoutOnly) { + ReactShadowNode parent = getParent(); + while (parent != null) { + parent.mTotalNativeChildren -= decrease; + if (!parent.mIsLayoutOnly) { + break; + } + parent = parent.getParent(); + } + } + return removed; + } + + /** + * This method will be called by {@link UIManagerModule} once per batch, before calculating + * layout. Will be only called for nodes that are marked as updated with {@link #markUpdated()} + * or require layouting (marked with {@link #dirty()}). + */ + public void onBeforeLayout() { + } + + public void updateProperties(CatalystStylesDiffMap styles) { + BaseCSSPropertyApplicator.applyCSSProperties(this, styles); + } + + /** + * Called after layout step at the end of the UI batch from {@link UIManagerModule}. May be used + * to enqueue additional ui operations for the native view. Will only be called on nodes marked + * as updated either with {@link #dirty()} or {@link #markUpdated()}. + * + * @param uiViewOperationQueue interface for enqueueing UI operations + */ + public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { + } + + /* package */ void dispatchUpdates( + float absoluteX, + float absoluteY, + UIViewOperationQueue uiViewOperationQueue, + NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) { + if (mNodeUpdated) { + onCollectExtraUpdates(uiViewOperationQueue); + } + + if (hasNewLayout()) { + mAbsoluteLeft = Math.round(absoluteX + getLayoutX()); + mAbsoluteTop = Math.round(absoluteY + getLayoutY()); + mAbsoluteRight = Math.round(absoluteX + getLayoutX() + getLayoutWidth()); + mAbsoluteBottom = Math.round(absoluteY + getLayoutY() + getLayoutHeight()); + + nativeViewHierarchyOptimizer.handleUpdateLayout(this); + } + } + + public final int getReactTag() { + return mReactTag; + } + + /* package */ final void setReactTag(int reactTag) { + mReactTag = reactTag; + } + + public final ReactShadowNode getRootNode() { + return Assertions.assertNotNull(mRootNode); + } + + /* package */ final void setRootNode(ReactShadowNode rootNode) { + mRootNode = rootNode; + } + + /* package */ final void setViewClassName(String viewClassName) { + mViewClassName = viewClassName; + } + + @Override + public final ReactShadowNode getChildAt(int i) { + return (ReactShadowNode) super.getChildAt(i); + } + + @Override + public final @Nullable ReactShadowNode getParent() { + return (ReactShadowNode) super.getParent(); + } + + /** + * Get the {@link ThemedReactContext} associated with this {@link ReactShadowNode}. This will + * never change during the lifetime of a {@link ReactShadowNode} instance, but different instances + * can have different contexts; don't cache any calculations based on theme values globally. + */ + public ThemedReactContext getThemedContext() { + return Assertions.assertNotNull(mThemedContext); + } + + protected void setThemedContext(ThemedReactContext themedContext) { + mThemedContext = themedContext; + } + + /* package */ void setShouldNotifyOnLayout(boolean shouldNotifyOnLayout) { + mShouldNotifyOnLayout = shouldNotifyOnLayout; + } + + /* package */ boolean shouldNotifyOnLayout() { + return mShouldNotifyOnLayout; + } + + /** + * Adds a child that the native view hierarchy will have at this index in the native view + * corresponding to this node. + */ + public void addNativeChildAt(ReactShadowNode child, int nativeIndex) { + Assertions.assertCondition(!mIsLayoutOnly); + Assertions.assertCondition(!child.mIsLayoutOnly); + + if (mNativeChildren == null) { + mNativeChildren = new ArrayList<>(4); + } + + mNativeChildren.add(nativeIndex, child); + child.mNativeParent = this; + } + + public ReactShadowNode removeNativeChildAt(int i) { + Assertions.assertNotNull(mNativeChildren); + ReactShadowNode removed = mNativeChildren.remove(i); + removed.mNativeParent = null; + return removed; + } + + public int getNativeChildCount() { + return mNativeChildren == null ? 0 : mNativeChildren.size(); + } + + public int indexOfNativeChild(ReactShadowNode nativeChild) { + Assertions.assertNotNull(mNativeChildren); + return mNativeChildren.indexOf(nativeChild); + } + + public @Nullable ReactShadowNode getNativeParent() { + return mNativeParent; + } + + /** + * Sets whether this node only contributes to the layout of its children without doing any + * drawing or functionality itself. + */ + public void setIsLayoutOnly(boolean isLayoutOnly) { + Assertions.assertCondition(getParent() == null, "Must remove from no opt parent first"); + Assertions.assertCondition(mNativeParent == null, "Must remove from native parent first"); + Assertions.assertCondition(getNativeChildCount() == 0, "Must remove all native children first"); + mIsLayoutOnly = isLayoutOnly; + } + + public boolean isLayoutOnly() { + return mIsLayoutOnly; + } + + public int getTotalNativeChildren() { + return mTotalNativeChildren; + } + + /** + * Returns the offset within the native children owned by all layout-only nodes in the subtree + * rooted at this node for the given child. Put another way, this returns the number of native + * nodes (nodes not optimized out of the native tree) that are a) to the left (visited before by a + * DFS) of the given child in the subtree rooted at this node and b) do not have a native parent + * in this subtree (which means that the given child will be a sibling of theirs in the final + * native hierarchy since they'll get attached to the same native parent). + * + * Basically, a view might have children that have been optimized away by + * {@link NativeViewHierarchyOptimizer}. Since those children will then add their native children + * to this view, we now have ranges of native children that correspond to single unoptimized + * children. The purpose of this method is to return the index within the native children that + * corresponds to the **start** of the native children that belong to the given child. Also, note + * that all of the children of a view might be optimized away, so this could return the same value + * for multiple different children. + * + * Example. Native children are represented by (N) where N is the no-opt child they came from. If + * no children are optimized away it'd look like this: (0) (1) (2) (3) ... (n) + * + * In case some children are optimized away, it might look like this: + * (0) (1) (1) (1) (3) (3) (4) + * + * In that case: + * getNativeOffsetForChild(Node 0) => 0 + * getNativeOffsetForChild(Node 1) => 1 + * getNativeOffsetForChild(Node 2) => 4 + * getNativeOffsetForChild(Node 3) => 4 + * getNativeOffsetForChild(Node 4) => 6 + */ + public int getNativeOffsetForChild(ReactShadowNode child) { + int index = 0; + boolean found = false; + for (int i = 0; i < getChildCount(); i++) { + ReactShadowNode current = getChildAt(i); + if (child == current) { + found = true; + break; + } + index += (current.mIsLayoutOnly ? current.getTotalNativeChildren() : 1); + } + if (!found) { + throw new RuntimeException("Child " + child.mReactTag + " was not a child of " + mReactTag); + } + return index; + } + + /** + * @return the x position of the corresponding view on the screen, rounded to pixels + */ + public int getScreenX() { + return Math.round(getLayoutX()); + } + + /** + * @return the y position of the corresponding view on the screen, rounded to pixels + */ + public int getScreenY() { + return Math.round(getLayoutY()); + } + + /** + * @return width corrected for rounding to pixels. + */ + public int getScreenWidth() { + return Math.round(mAbsoluteRight - mAbsoluteLeft); + } + + /** + * @return height corrected for rounding to pixels. + */ + public int getScreenHeight() { + return Math.round(mAbsoluteBottom - mAbsoluteTop); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java new file mode 100644 index 000000000..05a11ee95 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.view.MotionEvent; + +/** + * Interface for the root native view of a React native application. + */ +public interface RootView { + + /** + * Called when a child starts a native gesture (e.g. a scroll in a ScrollView). Should be called + * from the child's onTouchIntercepted implementation. + */ + void onChildStartedNativeGesture(MotionEvent androidEvent); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewManager.java new file mode 100644 index 000000000..b9ee413b4 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewManager.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.view.ViewGroup; + +/** + * View manager for ReactRootView components. + */ +public class RootViewManager extends ViewGroupManager { + + public static final String REACT_CLASS = "RootView"; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + protected ViewGroup createViewInstance(ThemedReactContext reactContext) { + return new SizeMonitoringFrameLayout(reactContext); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewUtil.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewUtil.java new file mode 100644 index 000000000..e12a76488 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootViewUtil.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.view.View; +import android.view.ViewParent; + +import com.facebook.infer.annotation.Assertions; + +public class RootViewUtil { + + /** + * Returns the root view of a given view in a react application. + */ + public static RootView getRootView(View reactView) { + View current = reactView; + while (true) { + if (current instanceof RootView) { + return (RootView) current; + } + ViewParent next = current.getParent(); + Assertions.assertNotNull(next); + Assertions.assertCondition(next instanceof View); + current = (View) next; + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ShadowNodeRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ShadowNodeRegistry.java new file mode 100644 index 000000000..9ffdcd7a4 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ShadowNodeRegistry.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.util.SparseArray; +import android.util.SparseBooleanArray; + +/** + * Simple container class to keep track of {@link ReactShadowNode}s associated with a particular + * UIManagerModule instance. + */ +/*package*/ class ShadowNodeRegistry { + + private final SparseArray mTagsToCSSNodes; + private final SparseBooleanArray mRootTags; + + public ShadowNodeRegistry() { + mTagsToCSSNodes = new SparseArray<>(); + mRootTags = new SparseBooleanArray(); + } + + public void addRootNode(ReactShadowNode node) { + int tag = node.getReactTag(); + mTagsToCSSNodes.put(tag, node); + mRootTags.put(tag, true); + } + + public void removeRootNode(int tag) { + if (!mRootTags.get(tag)) { + throw new IllegalViewOperationException( + "View with tag " + tag + " is not registered as a root view"); + } + + mTagsToCSSNodes.remove(tag); + mRootTags.delete(tag); + } + + public void addNode(ReactShadowNode node) { + mTagsToCSSNodes.put(node.getReactTag(), node); + } + + public void removeNode(int tag) { + if (mRootTags.get(tag)) { + throw new IllegalViewOperationException( + "Trying to remove root node " + tag + " without using removeRootNode!"); + } + mTagsToCSSNodes.remove(tag); + } + + public ReactShadowNode getNode(int tag) { + return mTagsToCSSNodes.get(tag); + } + + public boolean isRootNode(int tag) { + return mRootTags.get(tag); + } + + public int getRootNodeCount() { + return mRootTags.size(); + } + + public int getRootTag(int index) { + return mRootTags.keyAt(index); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/SimpleViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/SimpleViewManager.java new file mode 100644 index 000000000..f338776e9 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/SimpleViewManager.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.view.View; + +/** + * A partial implementation of {@link ViewManager} that applies common properties such as background + * color, opacity and CSS layout. Implementations should make sure to call + * {@code super.updateView()} in order for these properties to be applied. + * + * @param the view handled by this manager + */ +public abstract class SimpleViewManager extends ViewManager { + + @Override + public ReactShadowNode createCSSNodeInstance() { + return new ReactShadowNode(); + } + + @Override + public void updateView(T root, CatalystStylesDiffMap props) { + BaseViewPropertyApplicator.applyCommonViewProperties(root, props); + } + + @Override + public void updateExtraData(T root, Object extraData) { + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/SizeMonitoringFrameLayout.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/SizeMonitoringFrameLayout.java new file mode 100644 index 000000000..fbfd531c8 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/SizeMonitoringFrameLayout.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import javax.annotation.Nullable; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * Subclass of {@link FrameLayout} that allows registering for size change events. The main purpose + * for this class is to hide complexity of {@link ReactRootView} from the code under + * {@link com.facebook.react.uimanager} package. + */ +public class SizeMonitoringFrameLayout extends FrameLayout { + + public static interface OnSizeChangedListener { + void onSizeChanged(int width, int height, int oldWidth, int oldHeight); + } + + private @Nullable OnSizeChangedListener mOnSizeChangedListener; + + public SizeMonitoringFrameLayout(Context context) { + super(context); + } + + public SizeMonitoringFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SizeMonitoringFrameLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setOnSizeChangedListener(OnSizeChangedListener onSizeChangedListener) { + mOnSizeChangedListener = onSizeChangedListener; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + if (mOnSizeChangedListener != null) { + mOnSizeChangedListener.onSizeChanged(w, h, oldw, oldh); + } + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java new file mode 100644 index 000000000..f3511bd28 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Bundle; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.LifecycleEventListener; + +// + +/** + * Wraps {@link ReactContext} with the base {@link Context} passed into the constructor. + * It provides also a way to start activities using the viewContext to which RN native views belong. + * It delegates lifecycle listener registration to the original instance of {@link ReactContext} + * which is supposed to receive the lifecycle events. At the same time we disallow receiving + * lifecycle events for this wrapper instances. + * TODO: T7538544 Rename ThemedReactContext to be in alignment with name of ReactApplicationContext + */ +public class ThemedReactContext extends ReactContext { + + private final ReactApplicationContext mReactApplicationContext; + + public ThemedReactContext(ReactApplicationContext reactApplicationContext, Context base) { + super(base); + initializeWithInstance(reactApplicationContext.getCatalystInstance()); + mReactApplicationContext = reactApplicationContext; + } + + @Override + public void addLifecycleEventListener(LifecycleEventListener listener) { + mReactApplicationContext.addLifecycleEventListener(listener); + } + + @Override + public void removeLifecycleEventListener(LifecycleEventListener listener) { + mReactApplicationContext.removeLifecycleEventListener(listener); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java new file mode 100644 index 000000000..0902c8dd9 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import javax.annotation.Nullable; + +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.UiThreadUtil; + +/** + * Class responsible for identifying which react view should handle a given {@link MotionEvent}. + * It uses the event coordinates to traverse the view hierarchy and return a suitable view. + */ +public class TouchTargetHelper { + + private static final Rect mVisibleRect = new Rect(); + private static final int[] mViewLocationInScreen = {0, 0}; + + /** + * Find touch event target view within the provided container given the coordinates provided + * via {@link MotionEvent}. + * + * @param eventY the Y screen coordinate of the touch location + * @param eventX the X screen coordinate of the touch location + * @param viewGroup the container view to traverse + * @return the react tag ID of the child view that should handle the event + */ + public static int findTargetTagForTouch( + float eventY, + float eventX, + ViewGroup viewGroup) { + UiThreadUtil.assertOnUiThread(); + int targetTag = viewGroup.getId(); + View nativeTargetView = findTouchTargetView(eventX, eventY, viewGroup); + if (nativeTargetView != null) { + View reactTargetView = findClosestReactAncestor(nativeTargetView); + if (reactTargetView != null) { + targetTag = getTouchTargetForView(reactTargetView, eventX, eventY); + } + } + return targetTag; + } + + private static View findClosestReactAncestor(View view) { + while (view != null && view.getId() <= 0) { + view = (View) view.getParent(); + } + return view; + } + + /** + * Returns the touch target View that is either viewGroup or one if its descendants. + * This is a recursive DFS since view the entire tree must be parsed until the target is found. + * If the search does not backtrack, it is possible to follow a branch that cannot be a target + * (because of pointerEvents). For example, if both C and E can be the target of an event: + * A (pointerEvents: auto) - B (pointerEvents: box-none) - C (pointerEvents: none) + * \ D (pointerEvents: auto) - E (pointerEvents: auto) + * If the search goes down the first branch, it would return A as the target, which is incorrect. + * NB: This method is not thread-safe as it uses static instance of {@link Rect} + */ + private static View findTouchTargetView(float eventX, float eventY, ViewGroup viewGroup) { + int childrenCount = viewGroup.getChildCount(); + for (int i = childrenCount - 1; i >= 0; i--) { + View child = viewGroup.getChildAt(i); + // Views with `removeClippedSubviews` are exposing removed subviews through `getChildAt` to + // support proper view cleanup. Views removed by this option will be detached from it's + // parent, therefore `getGlobalVisibleRect` call will return bogus result as it treat view + // with no parent as a root of the view hierarchy. To prevent this from happening we check + // that view has a parent before visiting it. + if (child.getParent() != null && child.getGlobalVisibleRect(mVisibleRect)) { + if (eventX >= mVisibleRect.left && eventX <= mVisibleRect.right + && eventY >= mVisibleRect.top && eventY <= mVisibleRect.bottom) { + View targetView = findTouchTargetViewWithPointerEvents(eventX, eventY, child); + if (targetView != null) { + return targetView; + } + } + } + } + return viewGroup; + } + + /** + * Returns the touch target View of the event given, or null if neither the given View nor any of + * its descendants are the touch target. + */ + private static @Nullable View findTouchTargetViewWithPointerEvents( + float eventX, + float eventY, + View view) { + PointerEvents pointerEvents = view instanceof ReactPointerEventsView ? + ((ReactPointerEventsView) view).getPointerEvents() : PointerEvents.AUTO; + if (pointerEvents == PointerEvents.NONE) { + // This view and its children can't be the target + return null; + + } else if (pointerEvents == PointerEvents.BOX_ONLY) { + // This view is the target, its children don't matter + return view; + + } else if (pointerEvents == PointerEvents.BOX_NONE) { + // This view can't be the target, but its children might + if (view instanceof ViewGroup) { + View targetView = findTouchTargetView(eventX, eventY, (ViewGroup) view); + return targetView != view ? targetView : null; + } + return null; + + } else if (pointerEvents == PointerEvents.AUTO) { + // Either this view or one of its children is the target + if (view instanceof ViewGroup) { + return findTouchTargetView(eventX, eventY, (ViewGroup) view); + } + return view; + + } else { + throw new JSApplicationIllegalArgumentException( + "Unknown pointer event type: " + pointerEvents.toString()); + } + } + + private static int getTouchTargetForView(View targetView, float eventX, float eventY) { + if (targetView instanceof ReactCompoundView) { + // Use coordinates relative to the view. Use getLocationOnScreen() API, which is slightly more + // expensive than getGlobalVisibleRect(), otherwise partially visible views offset is wrong. + targetView.getLocationOnScreen(mViewLocationInScreen); + return ((ReactCompoundView) targetView).reactTagForTouch( + eventX - mViewLocationInScreen[0], + eventY - mViewLocationInScreen[1]); + } + return targetView.getId(); + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java new file mode 100644 index 000000000..25b97a863 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -0,0 +1,837 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import javax.annotation.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import android.util.DisplayMetrics; + +import com.facebook.csslayout.CSSLayoutContext; +import com.facebook.react.animation.Animation; +import com.facebook.react.animation.AnimationRegistry; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.debug.NotThreadSafeUiManagerDebugListener; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.OnBatchCompleteListener; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.SoftAssertions; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableArray; +import com.facebook.systrace.Systrace; +import com.facebook.systrace.SystraceMessage; + +/** + *

    Native module to allow JS to create and update native Views.

    + * + *

    + *

    == Transactional Requirement ==

    + * A requirement of this class is to make sure that transactional UI updates occur all at, meaning + * that no intermediate state is ever rendered to the screen. For example, if a JS application + * update changes the background of View A to blue and the width of View B to 100, both need to + * appear at once. Practically, this means that all UI update code related to a single transaction + * must be executed as a single code block on the UI thread. Executing as multiple code blocks + * could allow the platform UI system to interrupt and render a partial UI state. + *

    + * + *

    To facilitate this, this module enqueues operations that are then applied to native view + * hierarchy through {@link NativeViewHierarchyManager} at the end of each transaction. + * + *

    + *

    == CSSNodes ==

    + * In order to allow layout and measurement to occur on a non-UI thread, this module also + * operates on intermediate CSSNode objects that correspond to a native view. These CSSNode are able + * to calculate layout according to their styling rules, and then the resulting x/y/width/height of + * that layout is scheduled as an operation that will be applied to native view hierarchy at the end + * of current batch. + *

    + * + * TODO(5241856): Investigate memory usage of creating many small objects in UIManageModule and + * consider implementing a pool + * TODO(5483063): Don't dispatch the view hierarchy at the end of a batch if no UI changes occurred + */ +public class UIManagerModule extends ReactContextBaseJavaModule implements + OnBatchCompleteListener, LifecycleEventListener { + + // Keep in sync with ReactIOSTagHandles JS module - see that file for an explanation on why the + // increment here is 10 + private static final int ROOT_VIEW_TAG_INCREMENT = 10; + + private final NativeViewHierarchyManager mNativeViewHierarchyManager; + private final EventDispatcher mEventDispatcher; + private final AnimationRegistry mAnimationRegistry = new AnimationRegistry(); + private final ShadowNodeRegistry mShadowNodeRegistry = new ShadowNodeRegistry(); + private final ViewManagerRegistry mViewManagers; + private final CSSLayoutContext mLayoutContext = new CSSLayoutContext(); + private final Map mModuleConstants; + private final UIViewOperationQueue mOperationsQueue; + private final NativeViewHierarchyOptimizer mNativeViewHierarchyOptimizer; + private final int[] mMeasureBuffer = new int[4]; + + private @Nullable NotThreadSafeUiManagerDebugListener mUiManagerDebugListener; + private int mNextRootViewTag = 1; + private int mBatchId = 0; + + public UIManagerModule(ReactApplicationContext reactContext, List viewManagerList) { + super(reactContext); + mViewManagers = new ViewManagerRegistry(viewManagerList); + mEventDispatcher = new EventDispatcher(reactContext); + mNativeViewHierarchyManager = new NativeViewHierarchyManager( + mAnimationRegistry, + mViewManagers); + mOperationsQueue = new UIViewOperationQueue( + reactContext, + this, + mNativeViewHierarchyManager, + mAnimationRegistry); + mNativeViewHierarchyOptimizer = new NativeViewHierarchyOptimizer( + mOperationsQueue, + mShadowNodeRegistry); + DisplayMetrics displayMetrics = reactContext.getResources().getDisplayMetrics(); + DisplayMetricsHolder.setDisplayMetrics(displayMetrics); + + mModuleConstants = UIManagerModuleConstantsHelper.createConstants( + displayMetrics, + viewManagerList); + reactContext.addLifecycleEventListener(this); + } + + @Override + public String getName() { + return "RKUIManager"; + } + + @Override + public Map getConstants() { + return mModuleConstants; + } + + @Override + public void onHostResume() { + mOperationsQueue.resumeFrameCallback(); + } + + @Override + public void onHostPause() { + mOperationsQueue.pauseFrameCallback(); + } + + @Override + public void onHostDestroy() { + } + + @Override + public void onCatalystInstanceDestroy() { + super.onCatalystInstanceDestroy(); + mEventDispatcher.onCatalystInstanceDestroyed(); + } + + /** + * Registers a new root view. JS can use the returned tag with manageChildren to add/remove + * children to this view. + * + * Note that this must be called after getWidth()/getHeight() actually return something. See + * CatalystApplicationFragment as an example. + * + * TODO(6242243): Make addMeasuredRootView thread safe + * NB: this method is horribly not-thread-safe, the only reason it works right now is because + * it's called exactly once and is called before any JS calls are made. As soon as that fact no + * longer holds, this method will need to be fixed. + */ + public int addMeasuredRootView(final SizeMonitoringFrameLayout rootView) { + final int tag = mNextRootViewTag; + mNextRootViewTag += ROOT_VIEW_TAG_INCREMENT; + + final ReactShadowNode rootCSSNode = new ReactShadowNode(); + rootCSSNode.setReactTag(tag); + final ThemedReactContext themedRootContext = + new ThemedReactContext(getReactApplicationContext(), rootView.getContext()); + rootCSSNode.setThemedContext(themedRootContext); + // If LayoutParams sets size explicitly, we can use that. Otherwise get the size from the view. + if (rootView.getLayoutParams() != null && + rootView.getLayoutParams().width > 0 && + rootView.getLayoutParams().height > 0) { + rootCSSNode.setStyleWidth(rootView.getLayoutParams().width); + rootCSSNode.setStyleHeight(rootView.getLayoutParams().height); + } else { + rootCSSNode.setStyleWidth(rootView.getWidth()); + rootCSSNode.setStyleHeight(rootView.getHeight()); + } + rootCSSNode.setViewClassName("Root"); + + rootView.setOnSizeChangedListener( + new SizeMonitoringFrameLayout.OnSizeChangedListener() { + @Override + public void onSizeChanged(final int width, final int height, int oldW, int oldH) { + getReactApplicationContext().runOnNativeModulesQueueThread( + new Runnable() { + @Override + public void run() { + updateRootNodeSize(rootCSSNode, width, height); + } + }); + } + }); + + mShadowNodeRegistry.addRootNode(rootCSSNode); + + if (UiThreadUtil.isOnUiThread()) { + mNativeViewHierarchyManager.addRootView(tag, rootView, themedRootContext); + } else { + final Semaphore semaphore = new Semaphore(0); + getReactApplicationContext().runOnUiQueueThread( + new Runnable() { + @Override + public void run() { + mNativeViewHierarchyManager.addRootView(tag, rootView, themedRootContext); + semaphore.release(); + } + }); + try { + SoftAssertions.assertCondition( + semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS), + "Timed out adding root view"); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + return tag; + } + + @ReactMethod + public void removeRootView(int rootViewTag) { + mShadowNodeRegistry.removeRootNode(rootViewTag); + mOperationsQueue.enqueueRemoveRootView(rootViewTag); + } + + private void updateRootNodeSize(ReactShadowNode rootCSSNode, int newWidth, int newHeight) { + getReactApplicationContext().assertOnNativeModulesQueueThread(); + + rootCSSNode.setStyleWidth(newWidth); + rootCSSNode.setStyleHeight(newHeight); + + // If we're in the middle of a batch, the change will automatically be dispatched at the end of + // the batch. As all batches are executed as a single runnable on the event queue this should + // always be empty, but that calling architecture is an implementation detail. + if (mOperationsQueue.isEmpty()) { + dispatchViewUpdates(-1); // -1 = no associated batch id + } + } + + @ReactMethod + public void createView(int tag, String className, int rootViewTag, ReadableMap props) { + ViewManager viewManager = mViewManagers.get(className); + ReactShadowNode cssNode = viewManager.createCSSNodeInstance(); + ReactShadowNode rootNode = mShadowNodeRegistry.getNode(rootViewTag); + cssNode.setReactTag(tag); + cssNode.setViewClassName(className); + cssNode.setRootNode(rootNode); + cssNode.setThemedContext(rootNode.getThemedContext()); + + mShadowNodeRegistry.addNode(cssNode); + + CatalystStylesDiffMap styles = null; + if (props != null) { + styles = new CatalystStylesDiffMap(props); + cssNode.updateProperties(styles); + } + + if (!cssNode.isVirtual()) { + mNativeViewHierarchyOptimizer.handleCreateView(cssNode, rootViewTag, styles); + } + } + + @ReactMethod + public void updateView(int tag, String className, ReadableMap props) { + ViewManager viewManager = mViewManagers.get(className); + if (viewManager == null) { + throw new IllegalViewOperationException("Got unknown view type: " + className); + } + ReactShadowNode cssNode = mShadowNodeRegistry.getNode(tag); + if (cssNode == null) { + throw new IllegalViewOperationException("Trying to update non-existent view with tag " + tag); + } + + if (props != null) { + CatalystStylesDiffMap styles = new CatalystStylesDiffMap(props); + cssNode.updateProperties(styles); + if (!cssNode.isVirtual()) { + mNativeViewHierarchyOptimizer.handleUpdateView(cssNode, className, styles); + } + } + } + + /** + * Interface for adding/removing/moving views within a parent view from JS. + * + * @param viewTag the view tag of the parent view + * @param moveFrom a list of indices in the parent view to move views from + * @param moveTo parallel to moveFrom, a list of indices in the parent view to move views to + * @param addChildTags a list of tags of views to add to the parent + * @param addAtIndices parallel to addChildTags, a list of indices to insert those children at + * @param removeFrom a list of indices of views to permanently remove. The memory for the + * corresponding views and data structures should be reclaimed. + */ + @ReactMethod + public void manageChildren( + int viewTag, + @Nullable ReadableArray moveFrom, + @Nullable ReadableArray moveTo, + @Nullable ReadableArray addChildTags, + @Nullable ReadableArray addAtIndices, + @Nullable ReadableArray removeFrom) { + ReactShadowNode cssNodeToManage = mShadowNodeRegistry.getNode(viewTag); + + int numToMove = moveFrom == null ? 0 : moveFrom.size(); + int numToAdd = addChildTags == null ? 0 : addChildTags.size(); + int numToRemove = removeFrom == null ? 0 : removeFrom.size(); + + if (numToMove != 0 && (moveTo == null || numToMove != moveTo.size())) { + throw new IllegalViewOperationException("Size of moveFrom != size of moveTo!"); + } + + if (numToAdd != 0 && (addAtIndices == null || numToAdd != addAtIndices.size())) { + throw new IllegalViewOperationException("Size of addChildTags != size of addAtIndices!"); + } + + // We treat moves as an add and a delete + ViewAtIndex[] viewsToAdd = new ViewAtIndex[numToMove + numToAdd]; + int[] indicesToRemove = new int[numToMove + numToRemove]; + int[] tagsToRemove = new int[indicesToRemove.length]; + int[] tagsToDelete = new int[numToRemove]; + + if (numToMove > 0) { + Assertions.assertNotNull(moveFrom); + Assertions.assertNotNull(moveTo); + for (int i = 0; i < numToMove; i++) { + int moveFromIndex = moveFrom.getInt(i); + int tagToMove = cssNodeToManage.getChildAt(moveFromIndex).getReactTag(); + viewsToAdd[i] = new ViewAtIndex( + tagToMove, + moveTo.getInt(i)); + indicesToRemove[i] = moveFromIndex; + tagsToRemove[i] = tagToMove; + } + } + + if (numToAdd > 0) { + Assertions.assertNotNull(addChildTags); + Assertions.assertNotNull(addAtIndices); + for (int i = 0; i < numToAdd; i++) { + int viewTagToAdd = addChildTags.getInt(i); + int indexToAddAt = addAtIndices.getInt(i); + viewsToAdd[numToMove + i] = new ViewAtIndex(viewTagToAdd, indexToAddAt); + } + } + + if (numToRemove > 0) { + Assertions.assertNotNull(removeFrom); + for (int i = 0; i < numToRemove; i++) { + int indexToRemove = removeFrom.getInt(i); + int tagToRemove = cssNodeToManage.getChildAt(indexToRemove).getReactTag(); + indicesToRemove[numToMove + i] = indexToRemove; + tagsToRemove[numToMove + i] = tagToRemove; + tagsToDelete[i] = tagToRemove; + } + } + + // NB: moveFrom and removeFrom are both relative to the starting state of the View's children. + // moveTo and addAt are both relative to the final state of the View's children. + // + // 1) Sort the views to add and indices to remove by index + // 2) Iterate the indices being removed from high to low and remove them. Going high to low + // makes sure we remove the correct index when there are multiple to remove. + // 3) Iterate the views being added by index low to high and add them. Like the view removal, + // iteration direction is important to preserve the correct index. + + Arrays.sort(viewsToAdd, ViewAtIndex.COMPARATOR); + Arrays.sort(indicesToRemove); + + // Apply changes to CSSNode hierarchy + int lastIndexRemoved = -1; + for (int i = indicesToRemove.length - 1; i >= 0; i--) { + int indexToRemove = indicesToRemove[i]; + if (indexToRemove == lastIndexRemoved) { + throw new IllegalViewOperationException("Repeated indices in Removal list for view tag: " + + viewTag); + } + cssNodeToManage.removeChildAt(indicesToRemove[i]); + lastIndexRemoved = indicesToRemove[i]; + } + + for (int i = 0; i < viewsToAdd.length; i++) { + ViewAtIndex viewAtIndex = viewsToAdd[i]; + ReactShadowNode cssNodeToAdd = mShadowNodeRegistry.getNode(viewAtIndex.mTag); + if (cssNodeToAdd == null) { + throw new IllegalViewOperationException("Trying to add unknown view tag: " + + viewAtIndex.mTag); + } + cssNodeToManage.addChildAt(cssNodeToAdd, viewAtIndex.mIndex); + } + + if (!cssNodeToManage.isVirtual() && !cssNodeToManage.isVirtualAnchor()) { + mNativeViewHierarchyOptimizer.handleManageChildren( + cssNodeToManage, + indicesToRemove, + tagsToRemove, + viewsToAdd, + tagsToDelete); + } + + for (int i = 0; i < tagsToDelete.length; i++) { + removeCSSNode(tagsToDelete[i]); + } + } + + private void removeCSSNode(int tag) { + ReactShadowNode node = mShadowNodeRegistry.getNode(tag); + mShadowNodeRegistry.removeNode(tag); + for (int i = 0;i < node.getChildCount(); i++) { + removeCSSNode(node.getChildAt(i).getReactTag()); + } + } + + /** + * Replaces the View specified by oldTag with the View specified by newTag within oldTag's parent. + * This resolves to a simple {@link #manageChildren} call, but React doesn't have enough info in + * JS to formulate it itself. + */ + @ReactMethod + public void replaceExistingNonRootView(int oldTag, int newTag) { + if (mShadowNodeRegistry.isRootNode(oldTag) || mShadowNodeRegistry.isRootNode(newTag)) { + throw new IllegalViewOperationException("Trying to add or replace a root tag!"); + } + + ReactShadowNode oldNode = mShadowNodeRegistry.getNode(oldTag); + if (oldNode == null) { + throw new IllegalViewOperationException("Trying to replace unknown view tag: " + oldTag); + } + + ReactShadowNode parent = oldNode.getParent(); + if (parent == null) { + throw new IllegalViewOperationException("Node is not attached to a parent: " + oldTag); + } + + int oldIndex = parent.indexOf(oldNode); + if (oldIndex < 0) { + throw new IllegalStateException("Didn't find child tag in parent"); + } + + WritableArray tagsToAdd = Arguments.createArray(); + tagsToAdd.pushInt(newTag); + + WritableArray addAtIndices = Arguments.createArray(); + addAtIndices.pushInt(oldIndex); + + WritableArray indicesToRemove = Arguments.createArray(); + indicesToRemove.pushInt(oldIndex); + + manageChildren(parent.getReactTag(), null, null, tagsToAdd, addAtIndices, indicesToRemove); + } + + /** + * Method which takes a container tag and then releases all subviews for that container upon + * receipt. + * TODO: The method name is incorrect and will be renamed, #6033872 + * @param containerTag the tag of the container for which the subviews must be removed + */ + @ReactMethod + public void removeSubviewsFromContainerWithID(int containerTag) { + ReactShadowNode containerNode = mShadowNodeRegistry.getNode(containerTag); + if (containerNode == null) { + throw new IllegalViewOperationException( + "Trying to remove subviews of an unknown view tag: " + containerTag); + } + + WritableArray indicesToRemove = Arguments.createArray(); + for (int childIndex = 0; childIndex < containerNode.getChildCount(); childIndex++) { + indicesToRemove.pushInt(childIndex); + } + + manageChildren(containerTag, null, null, null, null, indicesToRemove); + } + + /** + * Determines the location on screen, width, and height of the given view and returns the values + * via an async callback. + */ + @ReactMethod + public void measure(final int reactTag, final Callback callback) { + // This method is called by the implementation of JS touchable interface (see Touchable.js for + // more details) at the moment of touch activation. That is after user starts the gesture from + // a touchable view with a given reactTag, or when user drag finger back into the press + // activation area of a touchable view that have been activated before. + mOperationsQueue.enqueueMeasure(reactTag, callback); + } + + /** + * Measures the view specified by tag relative to the given ancestorTag. This means that the + * returned x, y are relative to the origin x, y of the ancestor view. Results are stored in the + * given outputBuffer. We allow ancestor view and measured view to be the same, in which case + * the position always will be (0, 0) and method will only measure the view dimensions. + * + * NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible + * window which can cause unexpected results when measuring relative to things like ScrollViews + * that can have offset content on the screen. + */ + @ReactMethod + public void measureLayout( + int tag, + int ancestorTag, + Callback errorCallback, + Callback successCallback) { + try { + measureLayout(tag, ancestorTag, mMeasureBuffer); + float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]); + float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]); + float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]); + float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]); + successCallback.invoke(relativeX, relativeY, width, height); + } catch (IllegalViewOperationException e) { + errorCallback.invoke(e.getMessage()); + } + } + + /** + * Like {@link #measure} and {@link #measureLayout} but measures relative to the immediate parent. + * + * NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible + * window which can cause unexpected results when measuring relative to things like ScrollViews + * that can have offset content on the screen. + */ + @ReactMethod + public void measureLayoutRelativeToParent( + int tag, + Callback errorCallback, + Callback successCallback) { + try { + measureLayoutRelativeToParent(tag, mMeasureBuffer); + float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]); + float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]); + float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]); + float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]); + successCallback.invoke(relativeX, relativeY, width, height); + } catch (IllegalViewOperationException e) { + errorCallback.invoke(e.getMessage()); + } + } + + private void measureLayout(int tag, int ancestorTag, int[] outputBuffer) { + ReactShadowNode node = mShadowNodeRegistry.getNode(tag); + ReactShadowNode ancestor = mShadowNodeRegistry.getNode(ancestorTag); + if (node == null || ancestor == null) { + throw new IllegalViewOperationException( + "Tag " + (node == null ? tag : ancestorTag) + " does not exist"); + } + + if (node != ancestor) { + ReactShadowNode currentParent = node.getParent(); + while (currentParent != ancestor) { + if (currentParent == null) { + throw new IllegalViewOperationException( + "Tag " + ancestorTag + " is not an ancestor of tag " + tag); + } + currentParent = currentParent.getParent(); + } + } + + measureLayoutRelativeToVerifiedAncestor(node, ancestor, outputBuffer); + } + + private void measureLayoutRelativeToParent(int tag, int[] outputBuffer) { + ReactShadowNode node = mShadowNodeRegistry.getNode(tag); + if (node == null) { + throw new IllegalViewOperationException("No native view for tag " + tag + " exists!"); + } + ReactShadowNode parent = node.getParent(); + if (parent == null) { + throw new IllegalViewOperationException("View with tag " + tag + " doesn't have a parent!"); + } + + measureLayoutRelativeToVerifiedAncestor(node, parent, outputBuffer); + } + + private void measureLayoutRelativeToVerifiedAncestor( + ReactShadowNode node, + ReactShadowNode ancestor, + int[] outputBuffer) { + int offsetX = 0; + int offsetY = 0; + if (node != ancestor) { + offsetX = Math.round(node.getLayoutX()); + offsetY = Math.round(node.getLayoutY()); + ReactShadowNode current = node.getParent(); + while (current != ancestor) { + Assertions.assertNotNull(current); + assertNodeDoesNotNeedCustomLayoutForChildren(current); + offsetX += Math.round(current.getLayoutX()); + offsetY += Math.round(current.getLayoutY()); + current = current.getParent(); + } + assertNodeDoesNotNeedCustomLayoutForChildren(ancestor); + } + + outputBuffer[0] = offsetX; + outputBuffer[1] = offsetY; + outputBuffer[2] = node.getScreenWidth(); + outputBuffer[3] = node.getScreenHeight(); + } + + private void assertNodeDoesNotNeedCustomLayoutForChildren(ReactShadowNode node) { + ViewManager viewManager = Assertions.assertNotNull(mViewManagers.get(node.getViewClass())); + ViewGroupManager viewGroupManager; + if (viewManager instanceof ViewGroupManager) { + viewGroupManager = (ViewGroupManager) viewManager; + } else { + throw new IllegalViewOperationException("Trying to use view " + node.getViewClass() + + " as a parent, but its Manager doesn't extends ViewGroupManager"); + } + if (viewGroupManager != null && viewGroupManager.needsCustomLayoutForChildren()) { + throw new IllegalViewOperationException( + "Trying to measure a view using measureLayout/measureLayoutRelativeToParent relative to" + + " an ancestor that requires custom layout for it's children (" + node.getViewClass() + + "). Use measure instead."); + } + } + + /** + * Find the touch target child native view in the supplied root view hierarchy, given a react + * target location. + * + * This method is currently used only by Element Inspector DevTool. + * + * @param reactTag the tag of the root view to traverse + * @param point an array containing both X and Y target location + * @param callback will be called if with the identified child view react ID, and measurement + * info. If no view was found, callback will be invoked with no data. + */ + @ReactMethod + public void findSubviewIn( + final int reactTag, + final ReadableArray point, + final Callback callback) { + mOperationsQueue.enqueueFindTargetForTouch( + reactTag, + point.getInt(0), + point.getInt(1), + callback); + } + + /** + * Registers a new Animation that can then be added to a View using {@link #addAnimation}. + */ + public void registerAnimation(Animation animation) { + mOperationsQueue.enqueueRegisterAnimation(animation); + } + + /** + * Adds an Animation previously registered with {@link #registerAnimation} to a View and starts it + */ + public void addAnimation(final int reactTag, final int animationID, final Callback onSuccess) { + assertViewExists(reactTag, "addAnimation"); + mOperationsQueue.enqueueAddAnimation(reactTag, animationID, onSuccess); + } + + /** + * Removes an existing Animation, canceling it if it was in progress. + */ + public void removeAnimation(int reactTag, int animationID) { + assertViewExists(reactTag, "removeAnimation"); + mOperationsQueue.enqueueRemoveAnimation(animationID); + } + + @ReactMethod + public void setJSResponder(int reactTag, boolean blockNativeResponder) { + assertViewExists(reactTag, "setJSResponder"); + mOperationsQueue.enqueueSetJSResponder(reactTag, blockNativeResponder); + } + + @ReactMethod + public void clearJSResponder() { + mOperationsQueue.enqueueClearJSResponder(); + } + + @ReactMethod + public void dispatchViewManagerCommand( + int reactTag, + int commandId, + ReadableArray commandArgs) { + assertViewExists(reactTag, "dispatchViewManagerCommand"); + mOperationsQueue.enqueueDispatchCommand(reactTag, commandId, commandArgs); + } + + /** + * Show a PopupMenu. + * + * @param reactTag the tag of the anchor view (the PopupMenu is displayed next to this view); this + * needs to be the tag of a native view (shadow views can not be anchors) + * @param items the menu items as an array of strings + * @param error will be called if there is an error displaying the menu + * @param success will be called with the position of the selected item as the first argument, or + * no arguments if the menu is dismissed + */ + @ReactMethod + public void showPopupMenu( + int reactTag, + ReadableArray items, + Callback error, + Callback success) { + assertViewExists(reactTag, "showPopupMenu"); + mOperationsQueue.enqueueShowPopupMenu(reactTag, items, error, success); + } + + @ReactMethod + public void setMainScrollViewTag(int reactTag) { + // TODO(6588266): Implement if required + } + + @ReactMethod + public void configureNextLayoutAnimation( + ReadableMap config, + Callback successCallback, + Callback errorCallback) { + // TODO(6588266): Implement if required + } + + private void assertViewExists(int reactTag, String operationNameForExceptionMessage) { + if (mShadowNodeRegistry.getNode(reactTag) == null) { + throw new IllegalViewOperationException( + "Unable to execute operation " + operationNameForExceptionMessage + " on view with " + + "tag: " + reactTag + ", since the view does not exists"); + } + } + + /** + * To implement the transactional requirement mentioned in the class javadoc, we only commit + * UI changes to the actual view hierarchy once a batch of JS->Java calls have been completed. + * We know this is safe because all JS->Java calls that are triggered by a Java->JS call (e.g. + * the delivery of a touch event or execution of 'renderApplication') end up in a single + * JS->Java transaction. + * + * A better way to do this would be to have JS explicitly signal to this module when a UI + * transaction is done. Right now, though, this is how iOS does it, and we should probably + * update the JS and native code and make this change at the same time. + * + * TODO(5279396): Make JS UI library explicitly notify the native UI module of the end of a UI + * transaction using a standard native call + */ + @Override + public void onBatchComplete() { + int batchId = mBatchId; + mBatchId++; + + SystraceMessage.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onBatchCompleteUI") + .arg("BatchId", batchId) + .flush(); + try { + dispatchViewUpdates(batchId); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + + public void setUiManagerDebugListener(@Nullable NotThreadSafeUiManagerDebugListener listener) { + mUiManagerDebugListener = listener; + } + + public EventDispatcher getEventDispatcher() { + return mEventDispatcher; + } + + private void dispatchViewUpdates(final int batchId) { + for (int i = 0; i < mShadowNodeRegistry.getRootNodeCount(); i++) { + int tag = mShadowNodeRegistry.getRootTag(i); + ReactShadowNode cssRoot = mShadowNodeRegistry.getNode(tag); + notifyOnBeforeLayoutRecursive(cssRoot); + cssRoot.calculateLayout(mLayoutContext); + applyUpdatesRecursive(cssRoot, 0f, 0f); + } + + mNativeViewHierarchyOptimizer.onBatchComplete(); + mOperationsQueue.dispatchViewUpdates(batchId); + } + + private void notifyOnBeforeLayoutRecursive(ReactShadowNode cssNode) { + if (!cssNode.hasUpdates()) { + return; + } + for (int i = 0; i < cssNode.getChildCount(); i++) { + notifyOnBeforeLayoutRecursive(cssNode.getChildAt(i)); + } + cssNode.onBeforeLayout(); + } + + private void applyUpdatesRecursive(ReactShadowNode cssNode, float absoluteX, float absoluteY) { + if (!cssNode.hasUpdates()) { + return; + } + + if (!cssNode.isVirtualAnchor()) { + for (int i = 0; i < cssNode.getChildCount(); i++) { + applyUpdatesRecursive( + cssNode.getChildAt(i), + absoluteX + cssNode.getLayoutX(), + absoluteY + cssNode.getLayoutY()); + } + } + + int tag = cssNode.getReactTag(); + if (!mShadowNodeRegistry.isRootNode(tag)) { + cssNode.dispatchUpdates( + absoluteX, + absoluteY, + mOperationsQueue, + mNativeViewHierarchyOptimizer); + + // notify JS about layout event if requested + if (cssNode.shouldNotifyOnLayout()) { + mEventDispatcher.dispatchEvent( + new OnLayoutEvent( + tag, + cssNode.getScreenX(), + cssNode.getScreenY(), + cssNode.getScreenWidth(), + cssNode.getScreenHeight())); + } + } + cssNode.markUpdateSeen(); + } + + /* package */ void notifyOnViewHierarchyUpdateEnqueued() { + if (mUiManagerDebugListener != null) { + mUiManagerDebugListener.onViewHierarchyUpdateEnqueued(); + } + } + + /* package */ void notifyOnViewHierarchyUpdateFinished() { + if (mUiManagerDebugListener != null) { + mUiManagerDebugListener.onViewHierarchyUpdateFinished(); + } + } + + @ReactMethod + public void sendAccessibilityEvent(int tag, int eventType) { + mOperationsQueue.enqueueSendAccessibilityEvent(tag, eventType); + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java new file mode 100644 index 000000000..3287ebf78 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.HashMap; +import java.util.Map; + +import android.text.InputType; +import android.util.DisplayMetrics; +import android.view.accessibility.AccessibilityEvent; +import android.widget.ImageView; + +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.events.TouchEventType; + +/** + * Constants exposed to JS from {@link UIManagerModule}. + */ +/* package */ class UIManagerModuleConstants { + + public static final String ACTION_DISMISSED = "dismissed"; + public static final String ACTION_ITEM_SELECTED = "itemSelected"; + + /* package */ static Map getBubblingEventTypeConstants() { + return MapBuilder.builder() + .put( + "topChange", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onChange", "captured", "onChangeCapture"))) + .put( + "topSelect", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onSelect", "captured", "onSelectCapture"))) + .put( + TouchEventType.START.getJSEventName(), + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of( + "bubbled", + "onTouchStart", + "captured", + "onTouchStartCapture"))) + .put( + TouchEventType.MOVE.getJSEventName(), + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of( + "bubbled", + "onTouchMove", + "captured", + "onTouchMoveCapture"))) + .put( + TouchEventType.END.getJSEventName(), + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of( + "bubbled", + "onTouchEnd", + "captured", + "onTouchEndCapture"))) + .build(); + } + + /* package */ static Map getDirectEventTypeConstants() { + return MapBuilder.builder() + .put("topSelectionChange", MapBuilder.of("registrationName", "onSelectionChange")) + .put("topLoadingStart", MapBuilder.of("registrationName", "onLoadingStart")) + .put("topLoadingFinish", MapBuilder.of("registrationName", "onLoadingFinish")) + .put("topLoadingError", MapBuilder.of("registrationName", "onLoadingError")) + .put("topLayout", MapBuilder.of("registrationName", "onLayout")) + .build(); + } + + public static Map getConstants(DisplayMetrics displayMetrics) { + HashMap constants = new HashMap(); + constants.put( + "UIView", + MapBuilder.of( + "ContentMode", + MapBuilder.of( + "ScaleAspectFit", + ImageView.ScaleType.CENTER_INSIDE.ordinal(), + "ScaleAspectFill", + ImageView.ScaleType.CENTER_CROP.ordinal()))); + + constants.put( + "UIText", + MapBuilder.of( + "AutocapitalizationType", + MapBuilder.of( + "none", + InputType.TYPE_CLASS_TEXT, + "characters", + InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS, + "words", + InputType.TYPE_TEXT_FLAG_CAP_WORDS, + "sentences", + InputType.TYPE_TEXT_FLAG_CAP_SENTENCES))); + + constants.put( + "Dimensions", + MapBuilder.of( + "windowPhysicalPixels", + MapBuilder.of( + "width", + displayMetrics.widthPixels, + "height", + displayMetrics.heightPixels, + "scale", + displayMetrics.density, + "fontScale", + displayMetrics.scaledDensity, + "densityDpi", + displayMetrics.densityDpi))); + + constants.put( + "StyleConstants", + MapBuilder.of( + "PointerEventsValues", + MapBuilder.of( + "none", + PointerEvents.NONE.ordinal(), + "boxNone", + PointerEvents.BOX_NONE.ordinal(), + "boxOnly", + PointerEvents.BOX_ONLY.ordinal(), + "unspecified", + PointerEvents.AUTO.ordinal()))); + + constants.put( + "PopupMenu", + MapBuilder.of( + ACTION_DISMISSED, + ACTION_DISMISSED, + ACTION_ITEM_SELECTED, + ACTION_ITEM_SELECTED)); + + constants.put( + "AccessibilityEventTypes", + MapBuilder.of( + "typeWindowStateChanged", + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, + "typeViewClicked", + AccessibilityEvent.TYPE_VIEW_CLICKED)); + + return constants; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java new file mode 100644 index 000000000..61ca838c2 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import android.util.DisplayMetrics; + +import com.facebook.react.common.MapBuilder; + +/** + * Helps generate constants map for {@link UIManagerModule} by collecting and merging constants from + * registered view managers. + */ +/* package */ class UIManagerModuleConstantsHelper { + + private static final String CUSTOM_BUBBLING_EVENT_TYPES_KEY = "customBubblingEventTypes"; + private static final String CUSTOM_DIRECT_EVENT_TYPES_KEY = "customDirectEventTypes"; + + /** + * Generates map of constants that is then exposed by {@link UIManagerModule}. The constants map + * contains the following predefined fields for 'customBubblingEventTypes' and + * 'customDirectEventTypes'. Provided list of {@param viewManagers} is then used to populate + * content of those predefined fields using + * {@link ViewManager#getExportedCustomBubblingEventTypeConstants} and + * {@link ViewManager#getExportedCustomDirectEventTypeConstants} respectively. Each view manager + * is in addition allowed to expose viewmanager-specific constants that are placed under the key + * that corresponds to the view manager's name (see {@link ViewManager#getName}). Constants are + * merged into the map of {@link UIManagerModule} base constants that is stored in + * {@link UIManagerModuleConstants}. + * TODO(6845124): Create a test for this + */ + /* package */ static Map createConstants( + DisplayMetrics displayMetrics, + List viewManagers) { + Map constants = UIManagerModuleConstants.getConstants(displayMetrics); + Map bubblingEventTypesConstants = UIManagerModuleConstants.getBubblingEventTypeConstants(); + Map directEventTypesConstants = UIManagerModuleConstants.getDirectEventTypeConstants(); + + for (ViewManager viewManager : viewManagers) { + Map viewManagerBubblingEvents = viewManager.getExportedCustomBubblingEventTypeConstants(); + if (viewManagerBubblingEvents != null) { + recursiveMerge(bubblingEventTypesConstants, viewManagerBubblingEvents); + } + Map viewManagerDirectEvents = viewManager.getExportedCustomDirectEventTypeConstants(); + if (viewManagerDirectEvents != null) { + recursiveMerge(directEventTypesConstants, viewManagerDirectEvents); + } + Map viewManagerConstants = MapBuilder.newHashMap(); + Map customViewConstants = viewManager.getExportedViewConstants(); + if (customViewConstants != null) { + viewManagerConstants.put("Constants", customViewConstants); + } + Map viewManagerCommands = viewManager.getCommandsMap(); + if (viewManagerCommands != null) { + viewManagerConstants.put("Commands", viewManagerCommands); + } + Map viewManagerNativeProps = viewManager.getNativeProps(); + if (!viewManagerNativeProps.isEmpty()) { + Map nativeProps = new HashMap<>(); + for (Map.Entry entry : viewManagerNativeProps.entrySet()) { + nativeProps.put(entry.getKey(), entry.getValue().toString()); + } + viewManagerConstants.put("NativeProps", nativeProps); + } + if (!viewManagerConstants.isEmpty()) { + constants.put(viewManager.getName(), viewManagerConstants); + } + } + + constants.put(CUSTOM_BUBBLING_EVENT_TYPES_KEY, bubblingEventTypesConstants); + constants.put(CUSTOM_DIRECT_EVENT_TYPES_KEY, directEventTypesConstants); + + return constants; + } + + /** + * Merges {@param source} map into {@param dest} map recursively + */ + private static void recursiveMerge(Map dest, Map source) { + for (Object key : source.keySet()) { + Object sourceValue = source.get(key); + Object destValue = dest.get(key); + if (destValue != null && (sourceValue instanceof Map) && (destValue instanceof Map)) { + recursiveMerge((Map) destValue, (Map) sourceValue); + } else { + dest.put(key, sourceValue); + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIProp.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIProp.java new file mode 100644 index 000000000..ef10ca1ea --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIProp.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation which is used to mark native UI properties that are exposed to + * JS. {@link ViewManager#getNativeProps} traverses the fields of its + * subclasses and extracts the {@code UIProp} annotation data to generate the + * {@code NativeProps} map. Example: + * + * {@code + * @UIProp(UIProp.Type.BOOLEAN) public static final String PROP_FOO = "foo"; + * @UIProp(UIProp.Type.STRING) public static final String PROP_BAR = "bar"; + * } + */ +@Target(ElementType.FIELD) +@Retention(RUNTIME) +public @interface UIProp { + Type value(); + + public static enum Type { + BOOLEAN("boolean"), + NUMBER("number"), + STRING("String"), + MAP("Map"), + ARRAY("Array"); + + private final String mType; + + Type(String type) { + mType = type; + } + + @Override + public String toString() { + return mType; + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java new file mode 100644 index 000000000..60891ae2d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java @@ -0,0 +1,631 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; + +import java.util.ArrayList; + +import com.facebook.react.animation.Animation; +import com.facebook.react.animation.AnimationRegistry; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.systrace.Systrace; +import com.facebook.systrace.SystraceMessage; + +/** + * This class acts as a buffer for command executed on {@link NativeViewHierarchyManager} or on + * {@link AnimationRegistry}. It expose similar methods as mentioned classes but instead of + * executing commands immediately it enqueues those operations in a queue that is then flushed from + * {@link UIManagerModule} once JS batch of ui operations is finished. This is to make sure that we + * execute all the JS operation coming from a single batch a single loop of the main (UI) android + * looper. + * + * TODO(7135923): Pooling of operation objects + * TODO(5694019): Consider a better data structure for operations queue to save on allocations + */ +public class UIViewOperationQueue { + + private final int[] mMeasureBuffer = new int[4]; + + /** + * A mutation or animation operation on the view hierarchy. + */ + private interface UIOperation { + + void execute(); + } + + /** + * A spec for an operation on the native View hierarchy. + */ + private abstract class ViewOperation implements UIOperation { + + public int mTag; + + public ViewOperation(int tag) { + mTag = tag; + } + } + + private final class RemoveRootViewOperation extends ViewOperation { + + public RemoveRootViewOperation(int tag) { + super(tag); + } + + @Override + public void execute() { + mNativeViewHierarchyManager.removeRootView(mTag); + } + } + + private final class UpdatePropertiesOperation extends ViewOperation { + + private final CatalystStylesDiffMap mProps; + + private UpdatePropertiesOperation(int tag, CatalystStylesDiffMap props) { + super(tag); + mProps = props; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.updateProperties(mTag, mProps); + } + } + + /** + * Operation for updating native view's position and size. The operation is not created directly + * by a {@link UIManagerModule} call from JS. Instead it gets inflated using computed position + * and size values by CSSNode hierarchy. + */ + private final class UpdateLayoutOperation extends ViewOperation { + + private final int mParentTag, mX, mY, mWidth, mHeight; + + public UpdateLayoutOperation( + int parentTag, + int tag, + int x, + int y, + int width, + int height) { + super(tag); + mParentTag = parentTag; + mX = x; + mY = y; + mWidth = width; + mHeight = height; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.updateLayout(mParentTag, mTag, mX, mY, mWidth, mHeight); + } + } + + private final class CreateViewOperation extends ViewOperation { + + private final int mRootViewTagForContext; + private final String mClassName; + private final @Nullable CatalystStylesDiffMap mInitialProps; + + public CreateViewOperation( + int rootViewTagForContext, + int tag, + String className, + @Nullable CatalystStylesDiffMap initialProps) { + super(tag); + mRootViewTagForContext = rootViewTagForContext; + mClassName = className; + mInitialProps = initialProps; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.createView( + mRootViewTagForContext, + mTag, + mClassName, + mInitialProps); + } + } + + private final class ManageChildrenOperation extends ViewOperation { + + private final @Nullable int[] mIndicesToRemove; + private final @Nullable ViewAtIndex[] mViewsToAdd; + private final @Nullable int[] mTagsToDelete; + + public ManageChildrenOperation( + int tag, + @Nullable int[] indicesToRemove, + @Nullable ViewAtIndex[] viewsToAdd, + @Nullable int[] tagsToDelete) { + super(tag); + mIndicesToRemove = indicesToRemove; + mViewsToAdd = viewsToAdd; + mTagsToDelete = tagsToDelete; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.manageChildren( + mTag, + mIndicesToRemove, + mViewsToAdd, + mTagsToDelete); + } + } + + private final class UpdateViewExtraData extends ViewOperation { + + private final Object mExtraData; + + public UpdateViewExtraData(int tag, Object extraData) { + super(tag); + mExtraData = extraData; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.updateViewExtraData(mTag, mExtraData); + } + } + + private final class ChangeJSResponderOperation extends ViewOperation { + + private final boolean mBlockNativeResponder; + private final boolean mClearResponder; + + public ChangeJSResponderOperation( + int tag, + boolean clearResponder, + boolean blockNativeResponder) { + super(tag); + mClearResponder = clearResponder; + mBlockNativeResponder = blockNativeResponder; + } + + @Override + public void execute() { + if (!mClearResponder) { + mNativeViewHierarchyManager.setJSResponder(mTag, mBlockNativeResponder); + } else { + mNativeViewHierarchyManager.clearJSResponder(); + } + } + } + + private final class DispatchCommandOperation extends ViewOperation { + + private final int mCommand; + private final @Nullable ReadableArray mArgs; + + public DispatchCommandOperation(int tag, int command, @Nullable ReadableArray args) { + super(tag); + mCommand = command; + mArgs = args; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.dispatchCommand(mTag, mCommand, mArgs); + } + } + + private final class ShowPopupMenuOperation extends ViewOperation { + + private final ReadableArray mItems; + private final Callback mSuccess; + + public ShowPopupMenuOperation( + int tag, + ReadableArray items, + Callback success) { + super(tag); + mItems = items; + mSuccess = success; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.showPopupMenu(mTag, mItems, mSuccess); + } + } + + /** + * A spec for animation operations (add/remove) + */ + private static abstract class AnimationOperation implements UIViewOperationQueue.UIOperation { + + protected final int mAnimationID; + + public AnimationOperation(int animationID) { + mAnimationID = animationID; + } + } + + private class RegisterAnimationOperation extends AnimationOperation { + + private final Animation mAnimation; + + private RegisterAnimationOperation(Animation animation) { + super(animation.getAnimationID()); + mAnimation = animation; + } + + @Override + public void execute() { + mAnimationRegistry.registerAnimation(mAnimation); + } + } + + private class AddAnimationOperation extends AnimationOperation { + private final int mReactTag; + private final Callback mSuccessCallback; + + private AddAnimationOperation(int reactTag, int animationID, Callback successCallback) { + super(animationID); + mReactTag = reactTag; + mSuccessCallback = successCallback; + } + + @Override + public void execute() { + Animation animation = mAnimationRegistry.getAnimation(mAnimationID); + if (animation != null) { + mNativeViewHierarchyManager.startAnimationForNativeView( + mReactTag, + animation, + mSuccessCallback); + } else { + // node or animation not found + // TODO(5712813): cleanup callback in JS callbacks table in case of an error + throw new IllegalViewOperationException("Animation with id " + mAnimationID + + " was not found"); + } + } + } + + private final class RemoveAnimationOperation extends AnimationOperation { + + private RemoveAnimationOperation(int animationID) { + super(animationID); + } + + @Override + public void execute() { + Animation animation = mAnimationRegistry.getAnimation(mAnimationID); + if (animation != null) { + animation.cancel(); + } + } + } + + private final class MeasureOperation implements UIOperation { + + private final int mReactTag; + private final Callback mCallback; + + private MeasureOperation( + final int reactTag, + final Callback callback) { + super(); + mReactTag = reactTag; + mCallback = callback; + } + + @Override + public void execute() { + try { + mNativeViewHierarchyManager.measure(mReactTag, mMeasureBuffer); + } catch (NoSuchNativeViewException e) { + // Invoke with no args to signal failure and to allow JS to clean up the callback + // handle. + mCallback.invoke(); + return; + } + + float x = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]); + float y = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]); + float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]); + float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]); + mCallback.invoke(0, 0, width, height, x, y); + } + } + + private ArrayList mOperations = new ArrayList<>(); + + private final class FindTargetForTouchOperation implements UIOperation { + + private final int mReactTag; + private final float mTargetX; + private final float mTargetY; + private final Callback mCallback; + + private FindTargetForTouchOperation( + final int reactTag, + final float targetX, + final float targetY, + final Callback callback) { + super(); + mReactTag = reactTag; + mTargetX = targetX; + mTargetY = targetY; + mCallback = callback; + } + + @Override + public void execute() { + try { + mNativeViewHierarchyManager.measure( + mReactTag, + mMeasureBuffer); + } catch (IllegalViewOperationException e) { + mCallback.invoke(); + return; + } + + // Because React coordinates are relative to root container, and measure() operates + // on screen coordinates, we need to offset values using root container location. + final float containerX = (float) mMeasureBuffer[0]; + final float containerY = (float) mMeasureBuffer[1]; + + final int touchTargetReactTag = mNativeViewHierarchyManager.findTargetTagForTouch( + mReactTag, + PixelUtil.toPixelFromDIP(mTargetX) + containerX, + PixelUtil.toPixelFromDIP(mTargetY) + containerY); + + try { + mNativeViewHierarchyManager.measure( + touchTargetReactTag, + mMeasureBuffer); + } catch (IllegalViewOperationException e) { + mCallback.invoke(); + return; + } + + float x = PixelUtil.toDIPFromPixel(mMeasureBuffer[0] - containerX); + float y = PixelUtil.toDIPFromPixel(mMeasureBuffer[1] - containerY); + float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]); + float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]); + mCallback.invoke(touchTargetReactTag, x, y, width, height); + } + } + + private final class SendAccessibilityEvent extends ViewOperation { + + private final int mEventType; + + private SendAccessibilityEvent(int tag, int eventType) { + super(tag); + mEventType = eventType; + } + + @Override + public void execute() { + mNativeViewHierarchyManager.sendAccessibilityEvent(mTag, mEventType); + } + } + + private final UIManagerModule mUIManagerModule; + private final NativeViewHierarchyManager mNativeViewHierarchyManager; + private final AnimationRegistry mAnimationRegistry; + + private final Object mDispatchRunnablesLock = new Object(); + private final DispatchUIFrameCallback mDispatchUIFrameCallback; + + @GuardedBy("mDispatchRunnablesLock") + private final ArrayList mDispatchUIRunnables = new ArrayList<>(); + + /* package */ UIViewOperationQueue( + ReactApplicationContext reactContext, + UIManagerModule uiManagerModule, + NativeViewHierarchyManager nativeViewHierarchyManager, + AnimationRegistry animationRegistry) { + mUIManagerModule = uiManagerModule; + mNativeViewHierarchyManager = nativeViewHierarchyManager; + mAnimationRegistry = animationRegistry; + mDispatchUIFrameCallback = new DispatchUIFrameCallback(reactContext); + } + + public boolean isEmpty() { + return mOperations.isEmpty(); + } + + public void enqueueRemoveRootView(int rootViewTag) { + mOperations.add(new RemoveRootViewOperation(rootViewTag)); + } + + public void enqueueSetJSResponder(int reactTag, boolean blockNativeResponder) { + mOperations.add( + new ChangeJSResponderOperation(reactTag, false /*clearResponder*/, blockNativeResponder)); + } + + public void enqueueClearJSResponder() { + // Tag is 0 because JSResponderHandler doesn't need one in order to clear the responder. + mOperations.add(new ChangeJSResponderOperation(0, true /*clearResponder*/, false)); + } + + public void enqueueDispatchCommand( + int reactTag, + int commandId, + ReadableArray commandArgs) { + mOperations.add(new DispatchCommandOperation(reactTag, commandId, commandArgs)); + } + + public void enqueueUpdateExtraData(int reactTag, Object extraData) { + mOperations.add(new UpdateViewExtraData(reactTag, extraData)); + } + + public void enqueueShowPopupMenu( + int reactTag, + ReadableArray items, + Callback error, + Callback success) { + mOperations.add(new ShowPopupMenuOperation(reactTag, items, success)); + } + + public void enqueueCreateView( + int rootViewTagForContext, + int viewReactTag, + String viewClassName, + @Nullable CatalystStylesDiffMap initialProps) { + mOperations.add( + new CreateViewOperation( + rootViewTagForContext, + viewReactTag, + viewClassName, + initialProps)); + } + + public void enqueueUpdateProperties(int reactTag, String className, CatalystStylesDiffMap props) { + mOperations.add(new UpdatePropertiesOperation(reactTag, props)); + } + + public void enqueueUpdateLayout( + int parentTag, + int reactTag, + int x, + int y, + int width, + int height) { + mOperations.add( + new UpdateLayoutOperation(parentTag, reactTag, x, y, width, height)); + } + + public void enqueueManageChildren( + int reactTag, + @Nullable int[] indicesToRemove, + @Nullable ViewAtIndex[] viewsToAdd, + @Nullable int[] tagsToDelete) { + mOperations.add( + new ManageChildrenOperation(reactTag, indicesToRemove, viewsToAdd, tagsToDelete)); + } + + public void enqueueRegisterAnimation(Animation animation) { + mOperations.add(new RegisterAnimationOperation(animation)); + } + + public void enqueueAddAnimation( + final int reactTag, + final int animationID, + final Callback onSuccess) { + mOperations.add(new AddAnimationOperation(reactTag, animationID, onSuccess)); + } + + public void enqueueRemoveAnimation(int animationID) { + mOperations.add(new RemoveAnimationOperation(animationID)); + } + + public void enqueueMeasure( + final int reactTag, + final Callback callback) { + mOperations.add( + new MeasureOperation(reactTag, callback)); + } + + public void enqueueFindTargetForTouch( + final int reactTag, + final float targetX, + final float targetY, + final Callback callback) { + mOperations.add( + new FindTargetForTouchOperation(reactTag, targetX, targetY, callback)); + } + + public void enqueueSendAccessibilityEvent(int tag, int eventType) { + mOperations.add(new SendAccessibilityEvent(tag, eventType)); + } + + /* package */ void dispatchViewUpdates(final int batchId) { + // Store the current operation queues to dispatch and create new empty ones to continue + // receiving new operations + final ArrayList operations = mOperations.isEmpty() ? null : mOperations; + if (operations != null) { + mOperations = new ArrayList<>(); + } + + mUIManagerModule.notifyOnViewHierarchyUpdateEnqueued(); + + synchronized (mDispatchRunnablesLock) { + mDispatchUIRunnables.add( + new Runnable() { + @Override + public void run() { + SystraceMessage.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "DispatchUI") + .arg("BatchId", batchId) + .flush(); + try { + if (operations != null) { + for (int i = 0; i < operations.size(); i++) { + operations.get(i).execute(); + } + } + mUIManagerModule.notifyOnViewHierarchyUpdateFinished(); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + }); + } + } + + /* package */ void resumeFrameCallback() { + ReactChoreographer.getInstance() + .postFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, mDispatchUIFrameCallback); + } + + /* package */ void pauseFrameCallback() { + + ReactChoreographer.getInstance() + .removeFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, mDispatchUIFrameCallback); + } + + /** + * Choreographer FrameCallback responsible for actually dispatching view updates on the UI thread + * that were enqueued via {@link #dispatchViewUpdates(int)}. The reason we don't just enqueue + * directly to the UI thread from that method is to make sure our Runnables actually run before + * the next traversals happen: + * + * ViewRootImpl#scheduleTraversals (which is called from invalidate, requestLayout, etc) calls + * Looper#postSyncBarrier which keeps any UI thread looper messages from being processed until + * that barrier is removed during the next traversal. That means, depending on when we get updates + * from JS and what else is happening on the UI thread, we can sometimes try to post this runnable + * after ViewRootImpl has posted a barrier. + * + * Using a Choreographer callback (which runs immediately before traversals), we guarantee we run + * before the next traversal. + */ + private class DispatchUIFrameCallback extends GuardedChoreographerFrameCallback { + + private DispatchUIFrameCallback(ReactContext reactContext) { + super(reactContext); + } + + @Override + public void doFrameGuarded(long frameTimeNanos) { + synchronized (mDispatchRunnablesLock) { + for (int i = 0; i < mDispatchUIRunnables.size(); i++) { + mDispatchUIRunnables.get(i).run(); + } + mDispatchUIRunnables.clear(); + } + + ReactChoreographer.getInstance().postFrameCallback( + ReactChoreographer.CallbackType.DISPATCH_UI, this); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewAtIndex.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewAtIndex.java new file mode 100644 index 000000000..6ceef2632 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewAtIndex.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.Comparator; + +/** + * Data structure that couples view tag to it's index in parent view. Used for managing children + * operation. + */ +/* package */ class ViewAtIndex { + public static Comparator COMPARATOR = new Comparator() { + @Override + public int compare(ViewAtIndex lhs, ViewAtIndex rhs) { + return lhs.mIndex - rhs.mIndex; + } + }; + + public final int mTag; + public final int mIndex; + + public ViewAtIndex(int tag, int index) { + mTag = tag; + mIndex = index; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java new file mode 100644 index 000000000..5ee3cc36a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +/** + * Default property values for Views to be shared between Views and ShadowViews. + */ +public class ViewDefaults { + + public static final float FONT_SIZE_SP = 14.0f; + public static final int LINE_HEIGHT = 0; + public static final int NUMBER_OF_LINES = Integer.MAX_VALUE; +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java new file mode 100644 index 000000000..eb0b3ee63 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.view.View; +import android.view.ViewGroup; + +/** + * Class providing children management API for view managers of classes extending ViewGroup. + */ +public abstract class ViewGroupManager + extends ViewManager { + + @Override + public ReactShadowNode createCSSNodeInstance() { + return new ReactShadowNode(); + } + + @Override + public void updateView(T root, CatalystStylesDiffMap props) { + BaseViewPropertyApplicator.applyCommonViewProperties(root, props); + } + + @Override + public void updateExtraData(T root, Object extraData) { + } + + public void addView(T parent, View child, int index) { + parent.addView(child, index); + } + + public int getChildCount(T parent) { + return parent.getChildCount(); + } + + public View getChildAt(T parent, int index) { + return parent.getChildAt(index); + } + + public void removeView(T parent, View child) { + parent.removeView(child); + } + + /** + * Returns whether this View type needs to handle laying out its own children instead of + * deferring to the standard css-layout algorithm. + * Returns true for the layout to *not* be automatically invoked. Instead onLayout will be + * invoked as normal and it is the View instance's responsibility to properly call layout on its + * children. + * Returns false for the default behavior of automatically laying out children without going + * through the ViewGroup's onLayout method. In that case, onLayout for this View type must *not* + * call layout on its children. + */ + public boolean needsCustomLayoutForChildren() { + return false; + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java new file mode 100644 index 000000000..eaf442d23 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import javax.annotation.Nullable; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import android.view.View; + +import com.facebook.csslayout.CSSNode; +import com.facebook.react.touch.CatalystInterceptingViewGroup; +import com.facebook.react.touch.JSResponderHandler; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; + +/** + * Class responsible for knowing how to create and update catalyst Views of a given type. It is also + * responsible for creating and updating CSSNode subclasses used for calculating position and size + * for the corresponding native view. + */ +public abstract class ViewManager { + + private static final Map> CLASS_PROP_CACHE = new HashMap<>(); + + /** + * Creates a view and installs event emitters on it. + */ + public final T createView( + ThemedReactContext reactContext, + JSResponderHandler jsResponderHandler) { + T view = createViewInstance(reactContext); + addEventEmitters(reactContext, view); + if (view instanceof CatalystInterceptingViewGroup) { + ((CatalystInterceptingViewGroup) view).setOnInterceptTouchEventListener(jsResponderHandler); + } + return view; + } + + /** + * @return the name of this view manager. This will be the name used to reference this view + * manager from JavaScript in createReactNativeComponentClass. + */ + public abstract String getName(); + + /** + * This method should return a subclass of {@link CSSNode} which will be then used for measuring + * position and size of the view. In mose of the cases this should just return an instance of + * {@link CSSNode} + */ + public abstract C createCSSNodeInstance(); + + /** + * Subclasses should return a new View instance of the proper type. + * @param reactContext + */ + protected abstract T createViewInstance(ThemedReactContext reactContext); + + /** + * Called when view is detached from view hierarchy and allows for some additional cleanup by + * the {@link ViewManager} subclass. + */ + public void onDropViewInstance(ThemedReactContext reactContext, T view) { + } + + /** + * Subclasses can override this method to install custom event emitters on the given View. You + * might want to override this method if your view needs to emit events besides basic touch events + * to JS (e.g. scroll events). + */ + protected void addEventEmitters(ThemedReactContext reactContext, T view) { + } + + /** + * Subclass should use this method to populate native view with updated style properties. In case + * when a certain property is present in {@param props} map but the value is null, this property + * should be reset to the default value + */ + public abstract void updateView(T root, CatalystStylesDiffMap props); + + /** + * Subclasses can implement this method to receive an optional extra data enqueued from the + * corresponding instance of {@link ReactShadowNode} in + * {@link ReactShadowNode#onCollectExtraUpdates}. + * + * Since css layout step and ui updates can be executed in separate thread apart of setting + * x/y/width/height this is the recommended and thread-safe way of passing extra data from css + * node to the native view counterpart. + * + * TODO(7247021): Replace updateExtraData with generic update props mechanism after D2086999 + */ + public abstract void updateExtraData(T root, Object extraData); + + /** + * Subclasses may use this method to receive events/commands directly from JS through the + * {@link UIManager}. Good example of such a command would be {@code scrollTo} request with + * coordinates for a {@link ScrollView} or {@code goBack} request for a {@link WebView} instance. + * + * @param root View instance that should receive the command + * @param commandId code of the command + * @param args optional arguments for the command + */ + public void receiveCommand(T root, int commandId, @Nullable ReadableArray args) { + } + + /** + * Subclasses of {@link ViewManager} that expect to receive commands through + * {@link UIManagerModule#dispatchViewManagerCommand} should override this method returning the + * map between names of the commands and IDs that are then used in {@link #receiveCommand} method + * whenever the command is dispatched for this particular {@link ViewManager}. + * + * As an example we may consider {@link ReactWebViewManager} that expose the following commands: + * goBack, goForward, reload. In this case the map returned from {@link #getCommandsMap} from + * {@link ReactWebViewManager} will look as follows: + * { + * "goBack": 1, + * "goForward": 2, + * "reload": 3, + * } + * + * Now assuming that "reload" command is dispatched through {@link UIManagerModule} we trigger + * {@link ReactWebViewManager#receiveCommand} passing "3" as {@code commandId} argument. + * + * @return map of string to int mapping of the expected commands + */ + public @Nullable Map getCommandsMap() { + return null; + } + + /** + * Returns a map of config data passed to JS that defines eligible events that can be placed on + * native views. This should return bubbling directly-dispatched event types and specify what + * names should be used to subscribe to either form (bubbling/capturing). + * + * Returned map should be of the form: + * { + * "onTwirl": { + * "phasedRegistrationNames": { + * "bubbled": "onTwirl", + * "captured": "onTwirlCaptured" + * } + * } + * } + */ + public @Nullable Map getExportedCustomBubblingEventTypeConstants() { + return null; + } + + /** + * Returns a map of config data passed to JS that defines eligible events that can be placed on + * native views. This should return non-bubbling directly-dispatched event types. + * + * Returned map should be of the form: + * { + * "onTwirl": { + * "registrationName": "onTwirl" + * } + * } + */ + public @Nullable Map getExportedCustomDirectEventTypeConstants() { + return null; + } + + /** + * Returns a map of view-specific constants that are injected to JavaScript. These constants are + * made accessible via UIManager..Constants. + */ + public @Nullable Map getExportedViewConstants() { + return null; + } + + public Map getNativeProps() { + Map nativeProps = new HashMap<>(); + Class cls = getClass(); + while (cls.getSuperclass() != null) { + Map props = getNativePropsForClass(cls); + for (Map.Entry entry : props.entrySet()) { + nativeProps.put(entry.getKey(), entry.getValue()); + } + cls = cls.getSuperclass(); + } + return nativeProps; + } + + private Map getNativePropsForClass(Class cls) { + Map props = CLASS_PROP_CACHE.get(cls); + if (props != null) { + return props; + } + props = new HashMap<>(); + for (Field f : cls.getDeclaredFields()) { + UIProp annotation = f.getAnnotation(UIProp.class); + if (annotation != null) { + UIProp.Type type = annotation.value(); + try { + String name = (String) f.get(this); + props.put(name, type); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "UIProp " + cls.getName() + "." + f.getName() + " must be public."); + } + } + } + CLASS_PROP_CACHE.put(cls, props); + return props; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java new file mode 100644 index 000000000..2dffc8c26 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Class that stores the mapping between native view name used in JS and the corresponding instance + * of {@link ViewManager}. + */ +/* package */ class ViewManagerRegistry { + + private final Map mViewManagers = new HashMap<>(); + + public ViewManagerRegistry(List viewManagerList) { + for (ViewManager viewManager : viewManagerList) { + mViewManagers.put(viewManager.getName(), viewManager); + } + } + + /* package */ ViewManager get(String className) { + ViewManager viewManager = mViewManagers.get(className); + if (viewManager != null) { + return viewManager; + } else { + throw new IllegalViewOperationException("No ViewManager defined for class " + className); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java new file mode 100644 index 000000000..525a1d892 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.Arrays; +import java.util.HashSet; + +import com.facebook.csslayout.Spacing; +import com.facebook.react.common.SetBuilder; + +/** + * Keys for props that need to be shared across multiple classes. + */ +public class ViewProps { + + public static final String VIEW_CLASS_NAME = "RCTView"; + + // Layout only (only affect positions of children, causes no drawing) + // !!! Keep in sync with LAYOUT_ONLY_PROPS below + public static final String ALIGN_ITEMS = "alignItems"; + public static final String ALIGN_SELF = "alignSelf"; + public static final String BOTTOM = "bottom"; + public static final String COLLAPSABLE = "collapsable"; + public static final String FLEX = "flex"; + public static final String FLEX_DIRECTION = "flexDirection"; + public static final String FLEX_WRAP = "flexWrap"; + public static final String HEIGHT = "height"; + public static final String JUSTIFY_CONTENT = "justifyContent"; + public static final String LEFT = "left"; + public static final String[] MARGINS = { + "margin", "marginVertical", "marginHorizontal", "marginLeft", "marginRight", "marginTop", + "marginBottom" + }; + public static final String[] PADDINGS = { + "padding", "paddingVertical", "paddingHorizontal", "paddingLeft", "paddingRight", + "paddingTop", "paddingBottom" + }; + public static final String POSITION = "position"; + public static final String RIGHT = "right"; + public static final String TOP = "top"; + public static final String WIDTH = "width"; + + // Props that affect more than just layout + public static final String ENABLED = "enabled"; + public static final String BACKGROUND_COLOR = "backgroundColor"; + public static final String COLOR = "color"; + public static final String FONT_SIZE = "fontSize"; + public static final String FONT_WEIGHT = "fontWeight"; + public static final String FONT_STYLE = "fontStyle"; + public static final String FONT_FAMILY = "fontFamily"; + public static final String LINE_HEIGHT = "lineHeight"; + public static final String NEEDS_OFFSCREEN_ALPHA_COMPOSITING = "needsOffscreenAlphaCompositing"; + public static final String NUMBER_OF_LINES = "numberOfLines"; + public static final String ON = "on"; + public static final String RESIZE_MODE = "resizeMode"; + public static final String TEXT_ALIGN = "textAlign"; + public static final String BORDER_WIDTH = "borderWidth"; + public static final String BORDER_LEFT_WIDTH = "borderLeftWidth"; + public static final String BORDER_TOP_WIDTH = "borderTopWidth"; + public static final String BORDER_RIGHT_WIDTH = "borderRightWidth"; + public static final String BORDER_BOTTOM_WIDTH = "borderBottomWidth"; + public static final int[] BORDER_SPACING_TYPES = { + Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM + }; + public static final String[] BORDER_WIDTHS = { + BORDER_WIDTH, BORDER_LEFT_WIDTH, BORDER_RIGHT_WIDTH, BORDER_TOP_WIDTH, BORDER_BOTTOM_WIDTH, + }; + public static final int[] PADDING_MARGIN_SPACING_TYPES = { + Spacing.ALL, Spacing.VERTICAL, Spacing.HORIZONTAL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, + Spacing.BOTTOM + }; + + private static final HashSet LAYOUT_ONLY_PROPS = createLayoutOnlyPropsMap(); + + private static HashSet createLayoutOnlyPropsMap() { + HashSet layoutOnlyProps = SetBuilder.newHashSet(); + layoutOnlyProps.addAll( + Arrays.asList( + ALIGN_SELF, + ALIGN_ITEMS, + BOTTOM, + COLLAPSABLE, + FLEX, + FLEX_DIRECTION, + FLEX_WRAP, + HEIGHT, + JUSTIFY_CONTENT, + LEFT, + POSITION, + RIGHT, + TOP, + WIDTH)); + for (int i = 0; i < MARGINS.length; i++) { + layoutOnlyProps.add(MARGINS[i]); + } + for (int i = 0; i < PADDINGS.length; i++) { + layoutOnlyProps.add(PADDINGS[i]); + } + return layoutOnlyProps; + } + + public static boolean isLayoutOnly(String prop) { + return LAYOUT_ONLY_PROPS.contains(prop); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/DebugComponentOwnershipModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/DebugComponentOwnershipModule.java new file mode 100644 index 000000000..859c74c99 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/DebugComponentOwnershipModule.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.catalyst.uimanager.debug; + +import javax.annotation.Nullable; + +import android.util.SparseArray; + +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.JSApplicationCausedNativeException; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; + +/** + * Native module that can asynchronously request the owners hierarchy of a react tag. + * + * Example returned owner hierarchy: ['RootView', 'Dialog', 'TitleView', 'Text'] + */ +public class DebugComponentOwnershipModule extends ReactContextBaseJavaModule { + + public interface RCTDebugComponentOwnership extends JavaScriptModule { + + void getOwnerHierarchy(int requestID, int tag); + } + + /** + * Callback for when we receive the ownership hierarchy in native code. + * + * NB: {@link #onOwnerHierarchyLoaded} will be called on the native modules thread! + */ + public static interface OwnerHierarchyCallback { + + void onOwnerHierarchyLoaded(int tag, @Nullable ReadableArray owners); + } + + private final SparseArray mRequestIdToCallback = new SparseArray<>(); + + private @Nullable RCTDebugComponentOwnership mRCTDebugComponentOwnership; + private int mNextRequestId = 0; + + public DebugComponentOwnershipModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public void initialize() { + super.initialize(); + mRCTDebugComponentOwnership = getReactApplicationContext(). + getJSModule(RCTDebugComponentOwnership.class); + } + + @Override + public void onCatalystInstanceDestroy() { + super.onCatalystInstanceDestroy(); + mRCTDebugComponentOwnership = null; + } + + @ReactMethod + public synchronized void receiveOwnershipHierarchy( + int requestId, + int tag, + @Nullable ReadableArray owners) { + OwnerHierarchyCallback callback = mRequestIdToCallback.get(requestId); + if (callback == null) { + throw new JSApplicationCausedNativeException( + "Got receiveOwnershipHierarchy for invalid request id: " + requestId); + } + mRequestIdToCallback.delete(requestId); + callback.onOwnerHierarchyLoaded(tag, owners); + } + + /** + * Request to receive the component hierarchy for a particular tag. + * + * Example returned owner hierarchy: ['RootView', 'Dialog', 'TitleView', 'Text'] + * + * NB: The callback provided will be invoked on the native modules thread! + */ + public synchronized void loadComponentOwnerHierarchy(int tag, OwnerHierarchyCallback callback) { + int requestId = mNextRequestId; + mNextRequestId++; + mRequestIdToCallback.put(requestId, callback); + Assertions.assertNotNull(mRCTDebugComponentOwnership).getOwnerHierarchy(requestId, tag); + } + + @Override + public String getName() { + return "DebugComponentOwnershipModule"; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/NotThreadSafeUiManagerDebugListener.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/NotThreadSafeUiManagerDebugListener.java new file mode 100644 index 000000000..1f4a5690b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/debug/NotThreadSafeUiManagerDebugListener.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager.debug; + +import com.facebook.react.uimanager.UIManagerModule; + +/** + * A listener that is notified about {@link UIManagerModule} events. This listener should only be + * used for debug purposes and should not affect application state. + * + * NB: while onViewHierarchyUpdateFinished will always be called from the UI thread, there are no + * guarantees what thread onViewHierarchyUpdateEnqueued is called on. + */ +public interface NotThreadSafeUiManagerDebugListener { + + /** + * Called when {@link UIManagerModule} enqueues a UI batch to be dispatched to the main thread. + */ + void onViewHierarchyUpdateEnqueued(); + + /** + * Called from the main thread after a UI batch has been applied to all root views. + */ + void onViewHierarchyUpdateFinished(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java new file mode 100644 index 000000000..505fea794 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager.events; + +/** + * A UI event that can be dispatched to JS. + */ +public abstract class Event { + + private final int mViewTag; + private final long mTimestampMs; + + protected Event(int viewTag, long timestampMs) { + mViewTag = viewTag; + mTimestampMs = timestampMs; + } + + /** + * @return the view id for the view that generated this event + */ + public final int getViewTag() { + return mViewTag; + } + + /** + * @return the time at which the event happened in the {@link android.os.SystemClock#uptimeMillis} + * base. + */ + public final long getTimestampMs() { + return mTimestampMs; + } + + /** + * @return false if this Event can *never* be coalesced + */ + public boolean canCoalesce() { + return true; + } + + /** + * Given two events, coalesce them into a single event that will be sent to JS instead of two + * separate events. By default, just chooses the one the is more recent. + * + * Two events will only ever try to be coalesced if they have the same event name, view id, and + * coalescing key. + */ + public T coalesce(T otherEvent) { + return (T) (getTimestampMs() > otherEvent.getTimestampMs() ? this : otherEvent); + } + + /** + * @return a key used to determine which other events of this type this event can be coalesced + * with. For example, touch move events should only be coalesced within a single gesture so a + * coalescing key there would be the unique gesture id. + */ + public short getCoalescingKey() { + return 0; + } + + /** + * Called when the EventDispatcher is done with an event, either because it was dispatched or + * because it was coalesced with another Event. + */ + public void dispose() { + } + + /** + * @return the name of this event as registered in JS + */ + public abstract String getEventName(); + + /** + * Dispatch this event to JS using the given event emitter. + */ + public abstract void dispatch(RCTEventEmitter rctEventEmitter); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java new file mode 100644 index 000000000..aa3315c16 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java @@ -0,0 +1,302 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager.events; + +import javax.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Map; + +import android.util.LongSparseArray; +import android.view.Choreographer; + +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.ReactChoreographer; +import com.facebook.systrace.Systrace; + +/** + * Class responsible for dispatching UI events to JS. The main purpose of this class is to act as an + * intermediary between UI code generating events and JS, making sure we don't send more events than + * JS can process. + * + * To use it, create a subclass of {@link Event} and call {@link #dispatchEvent(Event)} whenever + * there's a UI event to dispatch. + * + * This class works by installing a Choreographer frame callback on the main thread. This callback + * then enqueues a runnable on the JS thread (if one is not already pending) that is responsible for + * actually dispatch events to JS. This implementation depends on the properties that + * 1) FrameCallbacks run after UI events have been processed in Choreographer.java + * 2) when we enqueue a runnable on the JS queue thread, it won't be called until after any + * previously enqueued JS jobs have finished processing + * + * If JS is taking a long time processing events, then the UI events generated on the UI thread can + * be coalesced into fewer events so that when the runnable runs, we don't overload JS with a ton + * of events and make it get even farther behind. + * + * Ideally, we don't need this and JS is fast enough to process all the events each frame, but bad + * things happen, including load on CPUs from the system, and we should handle this case well. + * + * == Event Cookies == + * + * An event cookie is made up of the event type id, view tag, and a custom coalescing key. Only + * Events that have the same cookie can be coalesced. + * + * Event Cookie Composition: + * VIEW_TAG_MASK = 0x00000000ffffffff + * EVENT_TYPE_ID_MASK = 0x0000ffff00000000 + * COALESCING_KEY_MASK = 0xffff000000000000 + */ +public class EventDispatcher implements LifecycleEventListener { + + private static final Comparator EVENT_COMPARATOR = new Comparator() { + @Override + public int compare(Event lhs, Event rhs) { + if (lhs == null && rhs == null) { + return 0; + } + if (lhs == null) { + return -1; + } + if (rhs == null) { + return 1; + } + + long diff = lhs.getTimestampMs() - rhs.getTimestampMs(); + if (diff == 0) { + return 0; + } else if (diff < 0) { + return -1; + } else { + return 1; + } + } + }; + + private final Object mEventsStagingLock = new Object(); + private final Object mEventsToDispatchLock = new Object(); + private final ReactApplicationContext mReactContext; + private final LongSparseArray mEventCookieToLastEventIdx = new LongSparseArray<>(); + private final Map mEventNameToEventId = MapBuilder.newHashMap(); + private final DispatchEventsRunnable mDispatchEventsRunnable = new DispatchEventsRunnable(); + private final ArrayList mEventStaging = new ArrayList<>(); + + private Event[] mEventsToDispatch = new Event[16]; + private int mEventsToDispatchSize = 0; + private @Nullable RCTEventEmitter mRCTEventEmitter; + private volatile @Nullable ScheduleDispatchFrameCallback mCurrentFrameCallback; + private short mNextEventTypeId = 0; + private volatile boolean mHasDispatchScheduled = false; + + public EventDispatcher(ReactApplicationContext reactContext) { + mReactContext = reactContext; + mReactContext.addLifecycleEventListener(this); + } + + /** + * Sends the given Event to JS, coalescing eligible events if JS is backed up. + */ + public void dispatchEvent(Event event) { + synchronized (mEventsStagingLock) { + mEventStaging.add(event); + } + } + + @Override + public void onHostResume() { + UiThreadUtil.assertOnUiThread(); + Assertions.assumeCondition(mCurrentFrameCallback == null); + + if (mRCTEventEmitter == null) { + mRCTEventEmitter = mReactContext.getJSModule(RCTEventEmitter.class); + } + + mCurrentFrameCallback = new ScheduleDispatchFrameCallback(); + ReactChoreographer.getInstance() + .postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, mCurrentFrameCallback); + } + + @Override + public void onHostPause() { + clearFrameCallback(); + } + + @Override + public void onHostDestroy() { + clearFrameCallback(); + } + + public void onCatalystInstanceDestroyed() { + clearFrameCallback(); + } + + private void clearFrameCallback() { + UiThreadUtil.assertOnUiThread(); + if (mCurrentFrameCallback != null) { + mCurrentFrameCallback.stop(); + mCurrentFrameCallback = null; + } + } + + /** + * We use a staging data structure so that all UI events generated in a single frame are + * dispatched at once. Otherwise, a JS runnable enqueued in a previous frame could run while the + * UI thread is in the process of adding UI events and we might incorrectly send one event this + * frame and another from this frame during the next. + */ + private void moveStagedEventsToDispatchQueue() { + synchronized (mEventsStagingLock) { + synchronized (mEventsToDispatchLock) { + for (int i = 0; i < mEventStaging.size(); i++) { + Event event = mEventStaging.get(i); + + if (!event.canCoalesce()) { + addEventToEventsToDispatch(event); + continue; + } + + long eventCookie = getEventCookie( + event.getViewTag(), + event.getEventName(), + event.getCoalescingKey()); + + Event eventToAdd = null; + Event eventToDispose = null; + Integer lastEventIdx = mEventCookieToLastEventIdx.get(eventCookie); + + if (lastEventIdx == null) { + eventToAdd = event; + mEventCookieToLastEventIdx.put(eventCookie, mEventsToDispatchSize); + } else { + Event lastEvent = mEventsToDispatch[lastEventIdx]; + Event coalescedEvent = event.coalesce(lastEvent); + if (coalescedEvent != lastEvent) { + eventToAdd = coalescedEvent; + mEventCookieToLastEventIdx.put(eventCookie, mEventsToDispatchSize); + eventToDispose = lastEvent; + mEventsToDispatch[lastEventIdx] = null; + } else { + eventToDispose = event; + } + } + + if (eventToAdd != null) { + addEventToEventsToDispatch(eventToAdd); + } + if (eventToDispose != null) { + eventToDispose.dispose(); + } + } + } + mEventStaging.clear(); + } + } + + private long getEventCookie(int viewTag, String eventName, short coalescingKey) { + short eventTypeId; + Short eventIdObj = mEventNameToEventId.get(eventName); + if (eventIdObj != null) { + eventTypeId = eventIdObj; + } else { + eventTypeId = mNextEventTypeId++; + mEventNameToEventId.put(eventName, eventTypeId); + } + return getEventCookie(viewTag, eventTypeId, coalescingKey); + } + + private static long getEventCookie(int viewTag, short eventTypeId, short coalescingKey) { + return viewTag | + (((long) eventTypeId) & 0xffff) << 32 | + (((long) coalescingKey) & 0xffff) << 48; + } + + private class ScheduleDispatchFrameCallback implements Choreographer.FrameCallback { + + private boolean mShouldStop = false; + + @Override + public void doFrame(long frameTimeNanos) { + UiThreadUtil.assertOnUiThread(); + + if (mShouldStop) { + return; + } + + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ScheduleDispatchFrameCallback"); + try { + moveStagedEventsToDispatchQueue(); + + if (!mHasDispatchScheduled) { + mHasDispatchScheduled = true; + mReactContext.runOnJSQueueThread(mDispatchEventsRunnable); + } + + ReactChoreographer.getInstance() + .postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + + public void stop() { + mShouldStop = true; + } + } + + private class DispatchEventsRunnable implements Runnable { + + @Override + public void run() { + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "DispatchEventsRunnable"); + try { + mHasDispatchScheduled = false; + Assertions.assertNotNull(mRCTEventEmitter); + synchronized (mEventsToDispatchLock) { + // We avoid allocating an array and iterator, and "sorting" if we don't need to. + // This occurs when the size of mEventsToDispatch is zero or one. + if (mEventsToDispatchSize > 1) { + Arrays.sort(mEventsToDispatch, 0, mEventsToDispatchSize, EVENT_COMPARATOR); + } + for (int eventIdx = 0; eventIdx < mEventsToDispatchSize; eventIdx++) { + Event event = mEventsToDispatch[eventIdx]; + // Event can be null if it has been coalesced into another event. + if (event == null) { + continue; + } + event.dispatch(mRCTEventEmitter); + event.dispose(); + } + clearEventsToDispatch(); + mEventCookieToLastEventIdx.clear(); + } + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + } + + private void addEventToEventsToDispatch(Event event) { + if (mEventsToDispatchSize == mEventsToDispatch.length) { + mEventsToDispatch = Arrays.copyOf(mEventsToDispatch, 2 * mEventsToDispatch.length); + } + mEventsToDispatch[mEventsToDispatchSize++] = event; + } + + private void clearEventsToDispatch() { + Arrays.fill(mEventsToDispatch, 0, mEventsToDispatchSize, null); + mEventsToDispatchSize = 0; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/NativeGestureUtil.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/NativeGestureUtil.java new file mode 100644 index 000000000..6ef3011b2 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/NativeGestureUtil.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager.events; + +import android.view.MotionEvent; +import android.view.View; + +import com.facebook.react.uimanager.RootViewUtil; + +/** + * Utilities for native Views that interpret native gestures (e.g. ScrollView, ViewPager, etc.). + */ +public class NativeGestureUtil { + + /** + * Helper method that should be called when a native view starts a native gesture (e.g. a native + * ScrollView takes control of a gesture stream and starts scrolling). This will handle + * dispatching the appropriate events to JS to make sure the gesture in JS is canceled. + * + * @param view the View starting the native gesture + * @param event the MotionEvent that caused the gesture to be started + */ + public static void notifyNativeGestureStarted(View view, MotionEvent event) { + RootViewUtil.getRootView(view).onChildStartedNativeGesture(event); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTEventEmitter.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTEventEmitter.java new file mode 100644 index 000000000..4fa6f3677 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTEventEmitter.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager.events; + +import javax.annotation.Nullable; + +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; + +public interface RCTEventEmitter extends JavaScriptModule { + public void receiveEvent(int targetTag, String eventName, @Nullable WritableMap event); + public void receiveTouches( + String eventName, + WritableArray touches, + WritableArray changedIndices); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java new file mode 100644 index 000000000..62e52373f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager.events; + +import android.view.MotionEvent; + +/** + * An event representing the start, end or movement of a touch. Corresponds to a single + * {@link android.view.MotionEvent}. + * + * TouchEvent coalescing can happen for move events if two move events have the same target view and + * coalescing key. See {@link TouchEventCoalescingKeyHelper} for more information about how these + * coalescing keys are determined. + */ +public class TouchEvent extends Event { + + private final MotionEvent mMotionEvent; + private final TouchEventType mTouchEventType; + private final short mCoalescingKey; + + public TouchEvent(int viewTag, TouchEventType touchEventType, MotionEvent motionEventToCopy) { + super(viewTag, motionEventToCopy.getEventTime()); + mTouchEventType = touchEventType; + mMotionEvent = MotionEvent.obtain(motionEventToCopy); + + short coalescingKey = 0; + int action = (mMotionEvent.getAction() & MotionEvent.ACTION_MASK); + switch (action) { + case MotionEvent.ACTION_DOWN: + TouchEventCoalescingKeyHelper.addCoalescingKey(mMotionEvent.getDownTime()); + break; + case MotionEvent.ACTION_UP: + TouchEventCoalescingKeyHelper.removeCoalescingKey(mMotionEvent.getDownTime()); + break; + case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_POINTER_UP: + TouchEventCoalescingKeyHelper.incrementCoalescingKey(mMotionEvent.getDownTime()); + break; + case MotionEvent.ACTION_MOVE: + coalescingKey = TouchEventCoalescingKeyHelper.getCoalescingKey(mMotionEvent.getDownTime()); + break; + case MotionEvent.ACTION_CANCEL: + TouchEventCoalescingKeyHelper.removeCoalescingKey(mMotionEvent.getDownTime()); + break; + default: + throw new RuntimeException("Unhandled MotionEvent action: " + action); + } + mCoalescingKey = coalescingKey; + } + + @Override + public String getEventName() { + return mTouchEventType.getJSEventName(); + } + + @Override + public boolean canCoalesce() { + // We can coalesce move events but not start/end events. Coalescing move events should probably + // append historical move data like MotionEvent batching does. This is left as an exercise for + // the reader. + switch (mTouchEventType) { + case START: + case END: + case CANCEL: + return false; + case MOVE: + return true; + default: + throw new RuntimeException("Unknown touch event type: " + mTouchEventType); + } + } + + @Override + public short getCoalescingKey() { + return mCoalescingKey; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + TouchesHelper.sendTouchEvent( + rctEventEmitter, + mTouchEventType, + getViewTag(), + mMotionEvent); + } + + @Override + public void dispose() { + mMotionEvent.recycle(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper.java new file mode 100644 index 000000000..e5783e3c5 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager.events; + +import android.util.SparseIntArray; + +/** + * Utility for determining coalescing keys for TouchEvents. To preserve proper ordering of events, + * move events should only be coalesced if there has been no up/down event between them (this + * basically only applies to multitouch since for single touches an up would signal the end of the + * gesture). To illustrate to kind of coalescing we want, imagine we are coalescing the following + * touch stream: + * + * (U = finger up, D = finger down, M = move) + * D MMMMM D MMMMMMMMMMMMMM U MMMMM D MMMMMM U U + * + * We want to make sure to coalesce this as + * + * D M D M U M D U U + * + * and *not* + * + * D D U M D U U + * + * To accomplish this, this class provides a way to initialize a coalescing key for a gesture and + * then increment it for every pointer up/down that occurs during that single gesture. + * + * We identify a single gesture based on {@link android.view.MotionEvent#getDownTime()} which will + * stay constant for a given set of related touches on a single view. + * + * NB: even though down time is a long, we cast as an int using the least significant bits as the + * identifier. In practice, we will not be coalescing over a time range where the most significant + * bits of that time range matter. This would require a gesture that lasts Integer.MAX_VALUE * 2 ms, + * or ~48 days. + * + * NB: we assume two gestures cannot begin at the same time. + * + * NB: this class should only be used from the UI thread. + */ +public class TouchEventCoalescingKeyHelper { + + private static final SparseIntArray sDownTimeToCoalescingKey = new SparseIntArray(); + + /** + * Starts tracking a new coalescing key corresponding to the gesture with this down time. + */ + public static void addCoalescingKey(long downTime) { + sDownTimeToCoalescingKey.put((int) downTime, 0); + } + + /** + * Increments the coalescing key corresponding to the gesture with this down time. + */ + public static void incrementCoalescingKey(long downTime) { + int currentValue = sDownTimeToCoalescingKey.get((int) downTime, -1); + if (currentValue == -1) { + throw new RuntimeException("Tried to increment non-existent cookie"); + } + sDownTimeToCoalescingKey.put((int) downTime, currentValue + 1); + } + + /** + * Gets the coalescing key corresponding to the gesture with this down time. + */ + public static short getCoalescingKey(long downTime) { + int currentValue = sDownTimeToCoalescingKey.get((int) downTime, -1); + if (currentValue == -1) { + throw new RuntimeException("Tried to get non-existent cookie"); + } + return ((short) (0xffff & currentValue)); + } + + /** + * Stops tracking a new coalescing key corresponding to the gesture with this down time. + */ + public static void removeCoalescingKey(long downTime) { + sDownTimeToCoalescingKey.delete((int) downTime); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventType.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventType.java new file mode 100644 index 000000000..36f329661 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventType.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager.events; + +/** + * Touch event types that JS module RCTEventEmitter can understand + */ +public enum TouchEventType { + START("topTouchStart"), + END("topTouchEnd"), + MOVE("topTouchMove"), + CANCEL("topTouchCancel"); + + private final String mJSEventName; + + TouchEventType(String jsEventName) { + mJSEventName = jsEventName; + } + + public String getJSEventName() { + return mJSEventName; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java new file mode 100644 index 000000000..56c6ff0ad --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager.events; + +import android.view.MotionEvent; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.PixelUtil; + +/** + * Class responsible for generating catalyst touch events based on android {@link MotionEvent}. + */ +/*package*/ class TouchesHelper { + + private static final String PAGE_X_KEY = "pageX"; + private static final String PAGE_Y_KEY = "pageY"; + private static final String TARGET_KEY = "target"; + private static final String TIMESTAMP_KEY = "timeStamp"; + private static final String POINTER_IDENTIFIER_KEY = "identifier"; + + // TODO(7351435): remove when we standardize touchEvent payload, since iOS uses locationXYZ but + // Android uses pageXYZ. As a temporary solution, Android currently sends both. + private static final String LOCATION_X_KEY = "locationX"; + private static final String LOCATION_Y_KEY = "locationY"; + + /** + * Creates catalyst pointers array in format that is expected by RCTEventEmitter JS module from + * given {@param event} instance. This method use {@param reactTarget} parameter to set as a + * target view id associated with current gesture. + */ + private static WritableArray createsPointersArray(int reactTarget, MotionEvent event) { + WritableArray touches = Arguments.createArray(); + + // Calculate raw-to-relative offset as getRawX() and getRawY() can only return values for the + // pointer at index 0. We use those value to calculate "raw" coordinates for other pointers + float offsetX = event.getRawX() - event.getX(); + float offsetY = event.getRawY() - event.getY(); + + for (int index = 0; index < event.getPointerCount(); index++) { + WritableMap touch = Arguments.createMap(); + touch.putDouble(PAGE_X_KEY, PixelUtil.toDIPFromPixel(event.getX(index) + offsetX)); + touch.putDouble(PAGE_Y_KEY, PixelUtil.toDIPFromPixel(event.getY(index) + offsetY)); + touch.putDouble(LOCATION_X_KEY, PixelUtil.toDIPFromPixel(event.getX(index))); + touch.putDouble(LOCATION_Y_KEY, PixelUtil.toDIPFromPixel(event.getY(index))); + touch.putInt(TARGET_KEY, reactTarget); + touch.putDouble(TIMESTAMP_KEY, event.getEventTime()); + touch.putDouble(POINTER_IDENTIFIER_KEY, event.getPointerId(index)); + touches.pushMap(touch); + } + + return touches; + } + + /** + * Generate and send touch event to RCTEventEmitter JS module associated with the given + * {@param context}. Touch event can encode multiple concurrent touches (pointers). + * + * @param rctEventEmitter Event emitter used to execute JS module call + * @param type type of the touch event (see {@link TouchEventType}) + * @param reactTarget target view react id associated with this gesture + * @param androidMotionEvent native touch event to read pointers count and coordinates from + */ + public static void sendTouchEvent( + RCTEventEmitter rctEventEmitter, + TouchEventType type, + int reactTarget, + MotionEvent androidMotionEvent) { + + WritableArray pointers = createsPointersArray(reactTarget, androidMotionEvent); + + // For START and END events send only index of the pointer that is associated with that event + // For MOVE and CANCEL events 'changedIndices' array should contain all the pointers indices + WritableArray changedIndices = Arguments.createArray(); + if (type == TouchEventType.MOVE || type == TouchEventType.CANCEL) { + for (int i = 0; i < androidMotionEvent.getPointerCount(); i++) { + changedIndices.pushInt(i); + } + } else if (type == TouchEventType.START || type == TouchEventType.END) { + changedIndices.pushInt(androidMotionEvent.getActionIndex()); + } else { + throw new RuntimeException("Unknown touch type: " + type); + } + + rctEventEmitter.receiveTouches( + type.getJSEventName(), + pointers, + changedIndices); + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java new file mode 100644 index 000000000..9d0d32405 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayout.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.drawer; + +import android.support.v4.widget.DrawerLayout; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; + +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.events.NativeGestureUtil; + +/** + * Wrapper view for {@link DrawerLayout}. It manages the properties that can be set on the drawer + * and contains some ReactNative-specific functionality. + */ +/* package */ class ReactDrawerLayout extends DrawerLayout { + + public static final int DEFAULT_DRAWER_WIDTH = LayoutParams.MATCH_PARENT; + private int mDrawerPosition = Gravity.START; + private int mDrawerWidth = DEFAULT_DRAWER_WIDTH; + + public ReactDrawerLayout(ReactContext reactContext) { + super(reactContext); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (super.onInterceptTouchEvent(ev)) { + NativeGestureUtil.notifyNativeGestureStarted(this, ev); + return true; + } + return false; + } + + /* package */ void openDrawer() { + openDrawer(mDrawerPosition); + } + + /* package */ void closeDrawer() { + closeDrawer(mDrawerPosition); + } + + /* package */ void setDrawerPosition(int drawerPosition) { + mDrawerPosition = drawerPosition; + setDrawerProperties(); + } + + /* package */ void setDrawerWidth(int drawerWidth) { + mDrawerWidth = (int) PixelUtil.toPixelFromDIP((float) drawerWidth); + setDrawerProperties(); + } + + // Sets the properties of the drawer, after the navigationView has been set. + /* package */ void setDrawerProperties() { + if (this.getChildCount() == 2) { + View drawerView = this.getChildAt(1); + LayoutParams layoutParams = (LayoutParams) drawerView.getLayoutParams(); + layoutParams.gravity = mDrawerPosition; + layoutParams.width = mDrawerWidth; + drawerView.setLayoutParams(layoutParams); + drawerView.setClickable(true); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.java new file mode 100644 index 000000000..eb07905a7 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.java @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.drawer; + +import javax.annotation.Nullable; + +import java.util.Map; + +import android.os.SystemClock; +import android.support.v4.widget.DrawerLayout; +import android.view.Gravity; +import android.view.View; + +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.UIProp; +import com.facebook.react.uimanager.ViewGroupManager; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.drawer.events.DrawerClosedEvent; +import com.facebook.react.views.drawer.events.DrawerOpenedEvent; +import com.facebook.react.views.drawer.events.DrawerSlideEvent; +import com.facebook.react.views.drawer.events.DrawerStateChangedEvent; + +/** + * View Manager for {@link ReactDrawerLayout} components. + */ +public class ReactDrawerLayoutManager extends ViewGroupManager { + + private static final String REACT_CLASS = "AndroidDrawerLayout"; + + public static final int OPEN_DRAWER = 1; + public static final int CLOSE_DRAWER = 2; + + @UIProp(UIProp.Type.NUMBER) + public static final String PROP_DRAWER_POSITION = "drawerPosition"; + @UIProp(UIProp.Type.NUMBER) + public static final String PROP_DRAWER_WIDTH = "drawerWidth"; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + protected void addEventEmitters(ThemedReactContext reactContext, ReactDrawerLayout view) { + view.setDrawerListener( + new DrawerEventEmitter( + view, + reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher())); + } + + @Override + protected ReactDrawerLayout createViewInstance(ThemedReactContext context) { + return new ReactDrawerLayout(context); + } + + @Override + public void updateView(ReactDrawerLayout view, CatalystStylesDiffMap props) { + super.updateView(view, props); + + if (props.hasKey(PROP_DRAWER_POSITION)) { + int drawerPosition = props.getInt(PROP_DRAWER_POSITION, -1); + if (Gravity.START == drawerPosition || Gravity.END == drawerPosition) { + view.setDrawerPosition(drawerPosition); + } else { + throw new JSApplicationIllegalArgumentException("Unknown drawerPosition " + drawerPosition); + } + } + + if (props.hasKey(PROP_DRAWER_WIDTH)) { + view.setDrawerWidth(props.getInt(PROP_DRAWER_WIDTH, ReactDrawerLayout.DEFAULT_DRAWER_WIDTH)); + } + } + + @Override + public boolean needsCustomLayoutForChildren() { + // Return true, since DrawerLayout will lay out it's own children. + return true; + } + + @Override + public @Nullable Map getCommandsMap() { + return MapBuilder.of("openDrawer", OPEN_DRAWER, "closeDrawer", CLOSE_DRAWER); + } + + @Override + public void receiveCommand( + ReactDrawerLayout root, + int commandId, + @Nullable ReadableArray args) { + switch (commandId) { + case OPEN_DRAWER: + root.openDrawer(); + break; + case CLOSE_DRAWER: + root.closeDrawer(); + break; + } + } + + @Override + public @Nullable Map getExportedViewConstants() { + return MapBuilder.of( + "DrawerPosition", + MapBuilder.of("Left", Gravity.START, "Right", Gravity.END)); + } + + @Override + public @Nullable Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.of( + DrawerSlideEvent.EVENT_NAME, MapBuilder.of("registrationName", "onDrawerSlide"), + DrawerOpenedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onDrawerOpen"), + DrawerClosedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onDrawerClose"), + DrawerStateChangedEvent.EVENT_NAME, MapBuilder.of( + "registrationName", "onDrawerStateChanged")); + } + + /** + * This method is overridden because of two reasons: + * 1. A drawer must have exactly two children + * 2. The second child that is added, is the navigationView, which gets panned from the side. + */ + @Override + public void addView(ReactDrawerLayout parent, View child, int index) { + if (getChildCount(parent) >= 2) { + throw new + JSApplicationIllegalArgumentException("The Drawer cannot have more than two children"); + } + if (index != 0 && index != 1) { + throw new JSApplicationIllegalArgumentException( + "The only valid indices for drawer's child are 0 or 1. Got " + index + " instead."); + } + parent.addView(child, index); + parent.setDrawerProperties(); + } + + public static class DrawerEventEmitter implements DrawerLayout.DrawerListener { + + private final DrawerLayout mDrawerLayout; + private final EventDispatcher mEventDispatcher; + + public DrawerEventEmitter(DrawerLayout drawerLayout, EventDispatcher eventDispatcher) { + mDrawerLayout = drawerLayout; + mEventDispatcher = eventDispatcher; + } + + @Override + public void onDrawerSlide(View view, float v) { + mEventDispatcher.dispatchEvent( + new DrawerSlideEvent(mDrawerLayout.getId(), SystemClock.uptimeMillis(), v)); + } + + @Override + public void onDrawerOpened(View view) { + mEventDispatcher.dispatchEvent( + new DrawerOpenedEvent(mDrawerLayout.getId(), SystemClock.uptimeMillis())); + } + + @Override + public void onDrawerClosed(View view) { + mEventDispatcher.dispatchEvent( + new DrawerClosedEvent(mDrawerLayout.getId(), SystemClock.uptimeMillis())); + } + + @Override + public void onDrawerStateChanged(int i) { + mEventDispatcher.dispatchEvent( + new DrawerStateChangedEvent(mDrawerLayout.getId(), SystemClock.uptimeMillis(), i)); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerClosedEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerClosedEvent.java new file mode 100644 index 000000000..83bc9f50f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerClosedEvent.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.drawer.events; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +public class DrawerClosedEvent extends Event { + + public static final String EVENT_NAME = "topDrawerClosed"; + + public DrawerClosedEvent(int viewId, long timestampMs) { + super(viewId, timestampMs); + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + // All events for a given view can be coalesced. + return 0; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), Arguments.createMap()); + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerOpenedEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerOpenedEvent.java new file mode 100644 index 000000000..916c301c9 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerOpenedEvent.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.drawer.events; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +public class DrawerOpenedEvent extends Event { + + public static final String EVENT_NAME = "topDrawerOpened"; + + public DrawerOpenedEvent(int viewId, long timestampMs) { + super(viewId, timestampMs); + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + // All events for a given view can be coalesced. + return 0; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), Arguments.createMap()); + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerSlideEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerSlideEvent.java new file mode 100644 index 000000000..b35bbc8d0 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerSlideEvent.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.drawer.events; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by a DrawerLayout as it is being moved open/closed. + */ +public class DrawerSlideEvent extends Event { + + public static final String EVENT_NAME = "topDrawerSlide"; + + private final float mOffset; + + public DrawerSlideEvent(int viewId, long timestampMs, float offset) { + super(viewId, timestampMs); + mOffset = offset; + } + + public float getOffset() { + return mOffset; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + // All slide events for a given view can be coalesced. + return 0; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putDouble("offset", getOffset()); + return eventData; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerStateChangedEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerStateChangedEvent.java new file mode 100644 index 000000000..dc6c9cd9c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/drawer/events/DrawerStateChangedEvent.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.drawer.events; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +public class DrawerStateChangedEvent extends Event { + + public static final String EVENT_NAME = "topDrawerStateChanged"; + + private final int mDrawerState; + + public DrawerStateChangedEvent(int viewId, long timestampMs, int drawerState) { + super(viewId, timestampMs); + mDrawerState = drawerState; + } + + public int getDrawerState() { + return mDrawerState; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + // All events for a given view can be coalesced. + return 0; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putDouble("drawerState", getDrawerState()); + return eventData; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.java new file mode 100644 index 000000000..fd7a6f67f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.image; + +import javax.annotation.Nullable; + +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.drawee.drawable.ScalingUtils; + +/** + * Converts JS resize modes into Android-specific scale type. + */ +public class ImageResizeMode { + + /** + * Converts JS resize modes into {@code ScalingUtils.ScaleType}. + * See {@code ImageResizeMode.js}. + */ + public static ScalingUtils.ScaleType toScaleType(@Nullable String resizeModeValue) { + if ("contain".equals(resizeModeValue)) { + return ScalingUtils.ScaleType.CENTER_INSIDE; + } + if ("cover".equals(resizeModeValue)) { + return ScalingUtils.ScaleType.CENTER_CROP; + } + if ("stretch".equals(resizeModeValue)) { + return ScalingUtils.ScaleType.FIT_XY; + } + if (resizeModeValue == null) { + // Use the default. Never use null. + return defaultValue(); + } + throw new JSApplicationIllegalArgumentException( + "Invalid resize mode: '" + resizeModeValue + "'"); + } + + /** + * This is the default as per web and iOS. + * We want to be consistent across platforms. + */ + public static ScalingUtils.ScaleType defaultValue() { + return ScalingUtils.ScaleType.CENTER_CROP; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java new file mode 100644 index 000000000..b7cabec78 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.image; + +import javax.annotation.Nullable; + +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.drawee.controller.AbstractDraweeControllerBuilder; +import com.facebook.react.uimanager.CSSColorUtil; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.SimpleViewManager; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIProp; +import com.facebook.react.uimanager.ViewProps; + +public class ReactImageManager extends SimpleViewManager { + + public static final String REACT_CLASS = "RCTImageView"; + + @Override + public String getName() { + return REACT_CLASS; + } + + // In JS this is Image.props.source.uri + @UIProp(UIProp.Type.STRING) + public static final String PROP_SRC = "src"; + @UIProp(UIProp.Type.NUMBER) + public static final String PROP_BORDER_RADIUS = "borderRadius"; + @UIProp(UIProp.Type.STRING) + public static final String PROP_RESIZE_MODE = ViewProps.RESIZE_MODE; + private static final String PROP_TINT_COLOR = "tintColor"; + + private final @Nullable AbstractDraweeControllerBuilder mDraweeControllerBuilder; + private final @Nullable Object mCallerContext; + + public ReactImageManager( + AbstractDraweeControllerBuilder draweeControllerBuilder, + Object callerContext) { + mDraweeControllerBuilder = draweeControllerBuilder; + mCallerContext = callerContext; + } + + public ReactImageManager() { + mDraweeControllerBuilder = null; + mCallerContext = null; + } + + @Override + public ReactImageView createViewInstance(ThemedReactContext context) { + return new ReactImageView( + context, + mDraweeControllerBuilder == null ? + Fresco.newDraweeControllerBuilder() : mDraweeControllerBuilder, + mCallerContext); + } + + @Override + public void updateView(final ReactImageView view, final CatalystStylesDiffMap props) { + super.updateView(view, props); + + if (props.hasKey(PROP_RESIZE_MODE)) { + view.setScaleType(ImageResizeMode.toScaleType(props.getString(PROP_RESIZE_MODE))); + } + if (props.hasKey(PROP_SRC)) { + view.setSource(props.getString(PROP_SRC)); + } + if (props.hasKey(PROP_BORDER_RADIUS)) { + view.setBorderRadius(props.getFloat(PROP_BORDER_RADIUS, 0.0f)); + } + if (props.hasKey(PROP_TINT_COLOR)) { + String tintColorString = props.getString(PROP_TINT_COLOR); + if (tintColorString == null) { + view.clearColorFilter(); + } else { + view.setColorFilter(CSSColorUtil.getColor(tintColorString)); + } + } + view.maybeUpdateView(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java new file mode 100644 index 000000000..c8ba6c061 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.image; + +import javax.annotation.Nullable; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.net.Uri; + +import com.facebook.drawee.controller.AbstractDraweeControllerBuilder; +import com.facebook.drawee.controller.ControllerListener; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.common.util.UriUtil; +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder; +import com.facebook.drawee.drawable.ScalingUtils; +import com.facebook.drawee.generic.GenericDraweeHierarchy; +import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; +import com.facebook.drawee.generic.RoundingParams; +import com.facebook.drawee.interfaces.DraweeController; +import com.facebook.drawee.view.GenericDraweeView; +import com.facebook.imagepipeline.common.ResizeOptions; +import com.facebook.imagepipeline.request.BasePostprocessor; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; +import com.facebook.imagepipeline.request.Postprocessor; + +/** + * Wrapper class around Fresco's GenericDraweeView, enabling persisting props across multiple view + * update and consistent processing of both static and network images. + */ +public class ReactImageView extends GenericDraweeView { + + private static final int REMOTE_IMAGE_FADE_DURATION_MS = 300; + public static final String TAG = ReactImageView.class.getSimpleName(); + + /* + * Implementation note re rounded corners: + * + * Fresco's built-in rounded corners only work for 'cover' resize mode - + * this is a limitation in Android itself. Fresco has a workaround for this, but + * it requires knowing the background color. + * + * So for the other modes, we use a postprocessor. + * Because the postprocessor uses a modified bitmap, that would just get cropped in + * 'cover' mode, so we fall back to Fresco's normal implementation. + */ + private static final Matrix sMatrix = new Matrix(); + private static final Matrix sInverse = new Matrix(); + + private class RoundedCornerPostprocessor extends BasePostprocessor { + + float getRadius(Bitmap source) { + ScalingUtils.getTransform( + sMatrix, + new Rect(0, 0, source.getWidth(), source.getHeight()), + source.getWidth(), + source.getHeight(), + 0.0f, + 0.0f, + mScaleType); + sMatrix.invert(sInverse); + return sInverse.mapRadius(mBorderRadius); + } + + @Override + public void process(Bitmap output, Bitmap source) { + output.setHasAlpha(true); + if (mBorderRadius < 0.01f) { + super.process(output, source); + return; + } + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setShader(new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); + Canvas canvas = new Canvas(output); + float radius = getRadius(source); + canvas.drawRoundRect( + new RectF(0, 0, source.getWidth(), source.getHeight()), + radius, + radius, + paint); + } + } + + private @Nullable Uri mUri; + private float mBorderRadius; + private ScalingUtils.ScaleType mScaleType; + private boolean mIsDirty; + private boolean mIsLocalImage; + private final AbstractDraweeControllerBuilder mDraweeControllerBuilder; + private final RoundedCornerPostprocessor mRoundedCornerPostprocessor; + private final @Nullable Object mCallerContext; + private @Nullable ControllerListener mControllerListener; + private int mImageFadeDuration = -1; + + // We can't specify rounding in XML, so have to do so here + private static GenericDraweeHierarchy buildHierarchy(Context context) { + return new GenericDraweeHierarchyBuilder(context.getResources()) + .setRoundingParams(RoundingParams.fromCornersRadius(0)) + .build(); + } + + public ReactImageView( + Context context, + AbstractDraweeControllerBuilder draweeControllerBuilder, + @Nullable Object callerContext) { + super(context, buildHierarchy(context)); + mScaleType = ImageResizeMode.defaultValue(); + mDraweeControllerBuilder = draweeControllerBuilder; + mRoundedCornerPostprocessor = new RoundedCornerPostprocessor(); + mCallerContext = callerContext; + } + + public void setBorderRadius(float borderRadius) { + mBorderRadius = PixelUtil.toPixelFromDIP(borderRadius); + mIsDirty = true; + } + + public void setScaleType(ScalingUtils.ScaleType scaleType) { + mScaleType = scaleType; + mIsDirty = true; + } + + public void setSource(@Nullable String source) { + mUri = null; + if (source != null) { + try { + mUri = Uri.parse(source); + // Verify scheme is set, so that relative uri (used by static resources) are not handled. + if (mUri.getScheme() == null) { + mUri = null; + } + } catch (Exception e) { + // ignore malformed uri, then attempt to extract resource ID. + } + if (mUri == null) { + mUri = getResourceDrawableUri(getContext(), source); + mIsLocalImage = true; + } else { + mIsLocalImage = false; + } + } + mIsDirty = true; + } + + public void maybeUpdateView() { + if (!mIsDirty) { + return; + } + + boolean doResize = shouldResize(mUri); + if (doResize && (getWidth() <= 0 || getHeight() <=0)) { + // If need a resize and the size is not yet set, wait until the layout pass provides one + return; + } + + GenericDraweeHierarchy hierarchy = getHierarchy(); + hierarchy.setActualImageScaleType(mScaleType); + + boolean usePostprocessorScaling = + mScaleType != ScalingUtils.ScaleType.CENTER_CROP && + mScaleType != ScalingUtils.ScaleType.FOCUS_CROP; + float hierarchyRadius = usePostprocessorScaling ? 0 : mBorderRadius; + + RoundingParams roundingParams = hierarchy.getRoundingParams(); + roundingParams.setCornersRadius(hierarchyRadius); + hierarchy.setRoundingParams(roundingParams); + hierarchy.setFadeDuration(mImageFadeDuration >= 0 + ? mImageFadeDuration + : mIsLocalImage ? 0 : REMOTE_IMAGE_FADE_DURATION_MS); + + Postprocessor postprocessor = usePostprocessorScaling ? mRoundedCornerPostprocessor : null; + + ResizeOptions resizeOptions = doResize ? new ResizeOptions(getWidth(), getHeight()) : null; + + ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(mUri) + .setPostprocessor(postprocessor) + .setResizeOptions(resizeOptions) + .build(); + + DraweeController draweeController = mDraweeControllerBuilder + .reset() + .setCallerContext(mCallerContext) + .setOldController(getController()) + .setImageRequest(imageRequest) + .setControllerListener(mControllerListener) + .build(); + setController(draweeController); + mIsDirty = false; + } + + // VisibleForTesting + public void setControllerListener(ControllerListener controllerListener) { + mControllerListener = controllerListener; + mIsDirty = true; + maybeUpdateView(); + } + + // VisibleForTesting + public void setImageFadeDuration(int imageFadeDuration) { + mImageFadeDuration = imageFadeDuration; + mIsDirty = true; + maybeUpdateView(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (w > 0 && h > 0) { + maybeUpdateView(); + } + } + + /** + * ReactImageViews only render a single image. + */ + @Override + public boolean hasOverlappingRendering() { + return false; + } + + private static boolean shouldResize(@Nullable Uri uri) { + // Resizing is inferior to scaling. See http://frescolib.org/docs/resizing-rotating.html#_ + // We resize here only for images likely to be from the device's camera, where the app developer + // has no control over the original size + return uri != null && (UriUtil.isLocalContentUri(uri) || UriUtil.isLocalFileUri(uri)); + } + + private static @Nullable Uri getResourceDrawableUri(Context context, @Nullable String name) { + if (name == null || name.isEmpty()) { + return null; + } + name = name.toLowerCase().replace("-", "_"); + int resId = context.getResources().getIdentifier( + name, + "drawable", + context.getPackageName()); + return new Uri.Builder() + .scheme(UriUtil.LOCAL_RESOURCE_SCHEME) + .path(String.valueOf(resId)) + .build(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarShadowNode.java new file mode 100644 index 000000000..b6d9f315f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarShadowNode.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.progressbar; + +import javax.annotation.Nullable; + +import java.util.HashSet; +import java.util.Set; + +import android.util.SparseIntArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; + +import com.facebook.csslayout.CSSNode; +import com.facebook.csslayout.MeasureOutput; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.ReactShadowNode; +import com.facebook.infer.annotation.Assertions; + +/** + * Node responsible for holding the style of the ProgressBar, see under + * {@link android.R.attr.progressBarStyle} for possible styles. ReactProgressBarViewManager + * manages how this style is applied to the ProgressBar. + */ +public class ProgressBarShadowNode extends ReactShadowNode implements CSSNode.MeasureFunction { + + private @Nullable String style; + + private final SparseIntArray mHeight = new SparseIntArray(); + private final SparseIntArray mWidth = new SparseIntArray(); + private final Set mMeasured = new HashSet<>(); + + public ProgressBarShadowNode() { + setMeasureFunction(this); + } + + public @Nullable String getStyle() { + return style; + } + + public void setStyle(String style) { + this.style = style; + } + + @Override + public void measure(CSSNode node, float width, MeasureOutput measureOutput) { + final int style = ReactProgressBarViewManager.getStyleFromString(getStyle()); + if (!mMeasured.contains(style)) { + ProgressBar progressBar = new ProgressBar(getThemedContext(), null, style); + final int spec = View.MeasureSpec.makeMeasureSpec( + ViewGroup.LayoutParams.WRAP_CONTENT, + View.MeasureSpec.UNSPECIFIED); + progressBar.measure(spec, spec); + mHeight.put(style, progressBar.getMeasuredHeight()); + mWidth.put(style, progressBar.getMeasuredWidth()); + mMeasured.add(style); + } + + measureOutput.height = mHeight.get(style); + measureOutput.width = mWidth.get(style); + } + + @Override + public void updateProperties(CatalystStylesDiffMap styles) { + super.updateProperties(styles); + + if (styles.hasKey(ReactProgressBarViewManager.PROP_STYLE)) { + String style = styles.getString(ReactProgressBarViewManager.PROP_STYLE); + Assertions.assertNotNull( + style, + "style property should always be set for the progress bar component"); + // TODO(7255944): Validate progressbar style attribute + setStyle(style); + } else { + setStyle(ReactProgressBarViewManager.DEFAULT_STYLE); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java new file mode 100644 index 000000000..296e9d400 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.progressbar; + +import javax.annotation.Nullable; + +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ProgressBar; + +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.uimanager.BaseViewPropertyApplicator; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIProp; +import com.facebook.react.uimanager.ViewManager; + +/** + * Manages instances of ProgressBar. ProgressBar is wrapped in a FrameLayout because the style of + * the ProgressBar can only be set in the constructor; whenever the style of a ProgressBar changes, + * we have to drop the existing ProgressBar (if there is one) and create a new one with the style + * given. + */ +public class ReactProgressBarViewManager extends ViewManager { + + @UIProp(UIProp.Type.STRING) public static final String PROP_STYLE = "styleAttr"; + + /* package */ static final String REACT_CLASS = "AndroidProgressBar"; + /* package */ static final String DEFAULT_STYLE = "Large"; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + protected FrameLayout createViewInstance(ThemedReactContext context) { + return new FrameLayout(context); + } + + @Override + public void updateView(FrameLayout view, CatalystStylesDiffMap props) { + BaseViewPropertyApplicator.applyCommonViewProperties(view, props); + if (props.hasKey(PROP_STYLE)) { + final int style = getStyleFromString(props.getString(PROP_STYLE)); + view.removeAllViews(); + view.addView( + new ProgressBar(view.getContext(), null, style), + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + } + } + + @Override + public ProgressBarShadowNode createCSSNodeInstance() { + return new ProgressBarShadowNode(); + } + + @Override + public void updateExtraData(FrameLayout root, Object extraData) { + // do nothing + } + + /* package */ static int getStyleFromString(@Nullable String styleStr) { + if (styleStr == null) { + throw new JSApplicationIllegalArgumentException( + "ProgressBar needs to have a style, null received"); + } else if (styleStr.equals("Horizontal")) { + return android.R.attr.progressBarStyleHorizontal; + } else if (styleStr.equals("Small")) { + return android.R.attr.progressBarStyleSmall; + } else if (styleStr.equals("Large")) { + return android.R.attr.progressBarStyleLarge; + } else if (styleStr.equals("Inverse")) { + return android.R.attr.progressBarStyleInverse; + } else if (styleStr.equals("SmallInverse")) { + return android.R.attr.progressBarStyleSmallInverse; + } else if (styleStr.equals("LargeInverse")) { + return android.R.attr.progressBarStyleLargeInverse; + } else { + throw new JSApplicationIllegalArgumentException("Unknown ProgressBar style: " + styleStr); + } + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/OnScrollDispatchHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/OnScrollDispatchHelper.java new file mode 100644 index 000000000..a9078c420 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/OnScrollDispatchHelper.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.scroll; + +import android.os.SystemClock; + +/** + * Android has a bug where onScrollChanged is called twice per frame with the same params during + * flings. We hack around that here by trying to detect that duplicate call and not dispatch it. See + * https://code.google.com/p/android/issues/detail?id=39473 + */ +public class OnScrollDispatchHelper { + + private static final int MIN_EVENT_SEPARATION_MS = 10; + + private int mPrevX = Integer.MIN_VALUE; + private int mPrevY = Integer.MIN_VALUE; + private long mLastScrollEventTimeMs = -(MIN_EVENT_SEPARATION_MS + 1); + + /** + * Call from a ScrollView in onScrollChanged, returns true if this onScrollChanged is legit (not a + * duplicate) and should be dispatched. + */ + public boolean onScrollChanged(int x, int y) { + long eventTime = SystemClock.uptimeMillis(); + boolean shouldDispatch = + eventTime - mLastScrollEventTimeMs > MIN_EVENT_SEPARATION_MS || + mPrevX != x || + mPrevY != y; + + mLastScrollEventTimeMs = eventTime; + mPrevX = x; + mPrevY = y; + + return shouldDispatch; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java new file mode 100644 index 000000000..ec528cedf --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.scroll; + +import android.content.Context; +import android.view.MotionEvent; +import android.widget.HorizontalScrollView; + +import com.facebook.react.uimanager.MeasureSpecAssertions; +import com.facebook.react.uimanager.events.NativeGestureUtil; + +/** + * Similar to {@link ReactScrollView} but only supports horizontal scrolling. + */ +public class ReactHorizontalScrollView extends HorizontalScrollView { + + private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper(); + + public ReactHorizontalScrollView(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec); + + setMeasuredDimension( + MeasureSpec.getSize(widthMeasureSpec), + MeasureSpec.getSize(heightMeasureSpec)); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // Call with the present values in order to re-layout if necessary + scrollTo(getScrollX(), getScrollY()); + } + + @Override + protected void onScrollChanged(int x, int y, int oldX, int oldY) { + super.onScrollChanged(x, y, oldX, oldY); + + if (mOnScrollDispatchHelper.onScrollChanged(x, y)) { + ReactScrollViewHelper.emitScrollEvent(this, x, y); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (super.onInterceptTouchEvent(ev)) { + NativeGestureUtil.notifyNativeGestureStarted(this, ev); + return true; + } + + return false; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java new file mode 100644 index 000000000..aa8c784cc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.scroll; + +import javax.annotation.Nullable; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.ViewGroupManager; + +/** + * View manager for {@link ReactHorizontalScrollView} components. + * + *

    Note that {@link ReactScrollView} and {@link ReactHorizontalScrollView} are exposed to JS + * as a single ScrollView component, configured via the {@code horizontal} boolean property. + */ +public class ReactHorizontalScrollViewManager + extends ViewGroupManager + implements ReactScrollViewCommandHelper.ScrollCommandHandler { + + private static final String REACT_CLASS = "AndroidHorizontalScrollView"; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + public ReactHorizontalScrollView createViewInstance(ThemedReactContext context) { + return new ReactHorizontalScrollView(context); + } + + @Override + public void receiveCommand( + ReactHorizontalScrollView scrollView, + int commandId, + @Nullable ReadableArray args) { + ReactScrollViewCommandHelper.receiveCommand(this, scrollView, commandId, args); + } + + @Override + public void scrollTo( + ReactHorizontalScrollView scrollView, + ReactScrollViewCommandHelper.ScrollToCommandData data) { + scrollView.smoothScrollTo(data.mDestX, data.mDestY); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java new file mode 100644 index 000000000..cc8098efc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.scroll; + +import javax.annotation.Nullable; + +import android.content.Context; +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ScrollView; + +import com.facebook.react.uimanager.MeasureSpecAssertions; +import com.facebook.react.uimanager.events.NativeGestureUtil; +import com.facebook.react.views.view.ReactClippingViewGroup; +import com.facebook.react.views.view.ReactClippingViewGroupHelper; +import com.facebook.infer.annotation.Assertions; + +/** + * A simple subclass of ScrollView that doesn't dispatch measure and layout to its children and has + * a scroll listener to send scroll events to JS. + * + *

    ReactScrollView only supports vertical scrolling. For horizontal scrolling, + * use {@link ReactHorizontalScrollView}. + */ +public class ReactScrollView extends ScrollView implements ReactClippingViewGroup { + + private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper(); + + private boolean mRemoveClippedSubviews; + private @Nullable Rect mClippingRect; + + public ReactScrollView(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec); + + setMeasuredDimension( + MeasureSpec.getSize(widthMeasureSpec), + MeasureSpec.getSize(heightMeasureSpec)); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // Call with the present values in order to re-layout if necessary + scrollTo(getScrollX(), getScrollY()); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (mRemoveClippedSubviews) { + updateClippingRect(); + } + } + + @Override + protected void onScrollChanged(int x, int y, int oldX, int oldY) { + super.onScrollChanged(x, y, oldX, oldY); + + if (mOnScrollDispatchHelper.onScrollChanged(x, y)) { + if (mRemoveClippedSubviews) { + updateClippingRect(); + } + + ReactScrollViewHelper.emitScrollEvent(this, x, y); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (super.onInterceptTouchEvent(ev)) { + NativeGestureUtil.notifyNativeGestureStarted(this, ev); + return true; + } + + return false; + } + + @Override + public void setRemoveClippedSubviews(boolean removeClippedSubviews) { + if (removeClippedSubviews && mClippingRect == null) { + mClippingRect = new Rect(); + } + mRemoveClippedSubviews = removeClippedSubviews; + updateClippingRect(); + } + + @Override + public boolean getRemoveClippedSubviews() { + return mRemoveClippedSubviews; + } + + @Override + public void updateClippingRect() { + if (!mRemoveClippedSubviews) { + return; + } + + Assertions.assertNotNull(mClippingRect); + + ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); + View contentView = getChildAt(0); + if (contentView instanceof ReactClippingViewGroup) { + ((ReactClippingViewGroup) contentView).updateClippingRect(); + } + } + + @Override + public void getClippingRect(Rect outClippingRect) { + outClippingRect.set(Assertions.assertNotNull(mClippingRect)); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java new file mode 100644 index 000000000..840fde9dd --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.scroll; + +import javax.annotation.Nullable; + +import java.util.Map; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.common.MapBuilder; + +/** + * Helper for view managers to handle commands like 'scrollTo'. + * Shared by {@link ReactScrollViewManager} and {@link ReactHorizontalScrollViewManager}. + */ +public class ReactScrollViewCommandHelper { + + public static final int COMMAND_SCROLL_TO = 1; + + public interface ScrollCommandHandler { + void scrollTo(T scrollView, ScrollToCommandData data); + } + + public static class ScrollToCommandData { + + public final int mDestX, mDestY; + + ScrollToCommandData(int destX, int destY) { + mDestX = destX; + mDestY = destY; + } + } + + public static Map getCommandsMap() { + return MapBuilder.of("scrollTo", COMMAND_SCROLL_TO); + } + + public static void receiveCommand( + ScrollCommandHandler viewManager, + T scrollView, + int commandType, + @Nullable ReadableArray args) { + Assertions.assertNotNull(viewManager); + Assertions.assertNotNull(scrollView); + Assertions.assertNotNull(args); + switch (commandType) { + case COMMAND_SCROLL_TO: + int destX = Math.round(PixelUtil.toPixelFromDIP(args.getInt(0))); + int destY = Math.round(PixelUtil.toPixelFromDIP(args.getInt(1))); + viewManager.scrollTo(scrollView, new ScrollToCommandData(destX, destY)); + return; + default: + throw new IllegalArgumentException(String.format( + "Unsupported command %d received by %s.", + commandType, + viewManager.getClass().getSimpleName())); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java new file mode 100644 index 000000000..c0b72def6 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.scroll; + +import android.os.SystemClock; +import android.view.View; +import android.view.ViewGroup; + +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.uimanager.UIManagerModule; + +/** + * Helper class that deals with emitting Scroll Events. + */ +public class ReactScrollViewHelper { + + /** + * Shared by {@link ReactScrollView} and {@link ReactHorizontalScrollView}. + */ + /* package */ static void emitScrollEvent(ViewGroup scrollView, int scrollX, int scrollY) { + View contentView = scrollView.getChildAt(0); + ReactContext reactContext = (ReactContext) scrollView.getContext(); + reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent( + new ScrollEvent( + scrollView.getId(), + SystemClock.uptimeMillis(), + scrollX, + scrollY, + contentView.getWidth(), + contentView.getHeight(), + scrollView.getWidth(), + scrollView.getHeight())); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java new file mode 100644 index 000000000..185394151 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.scroll; + +import javax.annotation.Nullable; + +import java.util.Map; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIProp; +import com.facebook.react.uimanager.ViewGroupManager; +import com.facebook.react.views.view.ReactClippingViewGroupHelper; + +/** + * View manager for {@link ReactScrollView} components. + * + *

    Note that {@link ReactScrollView} and {@link ReactHorizontalScrollView} are exposed to JS + * as a single ScrollView component, configured via the {@code horizontal} boolean property. + */ +public class ReactScrollViewManager + extends ViewGroupManager + implements ReactScrollViewCommandHelper.ScrollCommandHandler { + + private static final String REACT_CLASS = "RCTScrollView"; + + @UIProp(UIProp.Type.BOOLEAN) public static final String PROP_SHOWS_VERTICAL_SCROLL_INDICATOR = + "showsVerticalScrollIndicator"; + @UIProp(UIProp.Type.BOOLEAN) public static final String PROP_SHOWS_HORIZONTAL_SCROLL_INDICATOR = + "showsHorizontalScrollIndicator"; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + public ReactScrollView createViewInstance(ThemedReactContext context) { + return new ReactScrollView(context); + } + + @Override + public void updateView(ReactScrollView scrollView, CatalystStylesDiffMap props) { + super.updateView(scrollView, props); + if (props.hasKey(PROP_SHOWS_VERTICAL_SCROLL_INDICATOR)) { + scrollView.setVerticalScrollBarEnabled( + props.getBoolean(PROP_SHOWS_VERTICAL_SCROLL_INDICATOR, true)); + } + + if (props.hasKey(PROP_SHOWS_HORIZONTAL_SCROLL_INDICATOR)) { + scrollView.setHorizontalScrollBarEnabled( + props.getBoolean(PROP_SHOWS_HORIZONTAL_SCROLL_INDICATOR, true)); + } + + ReactClippingViewGroupHelper.applyRemoveClippedSubviewsProperty(scrollView, props); + } + + @Override + public @Nullable Map getCommandsMap() { + return ReactScrollViewCommandHelper.getCommandsMap(); + } + + @Override + public void receiveCommand( + ReactScrollView scrollView, + int commandId, + @Nullable ReadableArray args) { + ReactScrollViewCommandHelper.receiveCommand(this, scrollView, commandId, args); + } + + @Override + public void scrollTo( + ReactScrollView scrollView, + ReactScrollViewCommandHelper.ScrollToCommandData data) { + scrollView.smoothScrollTo(data.mDestX, data.mDestY); + } + + @Override + public @Nullable Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.builder() + .put(ScrollEvent.EVENT_NAME, MapBuilder.of("registrationName", "onScroll")) + .put("topScrollBeginDrag", MapBuilder.of("registrationName", "onScrollBeginDrag")) + .put("topScrollEndDrag", MapBuilder.of("registrationName", "onScrollEndDrag")) + .put("topScrollAnimationEnd", MapBuilder.of("registrationName", "onScrollAnimationEnd")) + .put("topMomentumScrollBegin", MapBuilder.of("registrationName", "onMomentumScrollBegin")) + .put("topMomentumScrollEnd", MapBuilder.of("registrationName", "onMomentumScrollEnd")) + .build(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java new file mode 100644 index 000000000..af4961936 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.scroll; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * A event dispatched from a ScrollView scrolling. + */ +public class ScrollEvent extends Event { + + public static final String EVENT_NAME = "topScroll"; + + private final int mScrollX; + private final int mScrollY; + private final int mContentWidth; + private final int mContentHeight; + private final int mScrollViewWidth; + private final int mScrollViewHeight; + + public ScrollEvent( + int viewTag, + long timestampMs, + int scrollX, + int scrollY, + int contentWidth, + int contentHeight, + int scrollViewWidth, + int scrollViewHeight) { + super(viewTag, timestampMs); + mScrollX = scrollX; + mScrollY = scrollY; + mContentWidth = contentWidth; + mContentHeight = contentHeight; + mScrollViewWidth = scrollViewWidth; + mScrollViewHeight = scrollViewHeight; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + // All scroll events for a given view can be coalesced + return 0; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap contentOffset = Arguments.createMap(); + contentOffset.putDouble("x", PixelUtil.toDIPFromPixel(mScrollX)); + contentOffset.putDouble("y", PixelUtil.toDIPFromPixel(mScrollY)); + + WritableMap contentSize = Arguments.createMap(); + contentSize.putDouble("width", PixelUtil.toDIPFromPixel(mContentWidth)); + contentSize.putDouble("height", PixelUtil.toDIPFromPixel(mContentHeight)); + + WritableMap layoutMeasurement = Arguments.createMap(); + layoutMeasurement.putDouble("width", PixelUtil.toDIPFromPixel(mScrollViewWidth)); + layoutMeasurement.putDouble("height", PixelUtil.toDIPFromPixel(mScrollViewHeight)); + + WritableMap event = Arguments.createMap(); + event.putMap("contentOffset", contentOffset); + event.putMap("contentSize", contentSize); + event.putMap("layoutMeasurement", layoutMeasurement); + + event.putInt("target", getViewTag()); + event.putBoolean("responderIgnoreScroll", true); + return event; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java new file mode 100644 index 000000000..79b39058b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.switchviewview; + +import android.content.Context; +import android.support.v7.widget.SwitchCompat; +import android.widget.Switch; + +/** + * Switch that has its value controlled by JS. Whenever the value of the switch changes, we do not + * allow any other changes to that switch until JS sets a value explicitly. This stops the Switch + * from changing its value multiple times, when those changes have not been processed by JS first. + */ +/*package*/ class ReactSwitch extends SwitchCompat { + + private boolean mAllowChange; + + public ReactSwitch(Context context) { + super(context); + mAllowChange = true; + } + + @Override + public void setChecked(boolean checked) { + if (mAllowChange) { + mAllowChange = false; + super.setChecked(checked); + } + } + + /*package*/ void setOn(boolean on) { + // If the switch has a different value than the value sent by JS, we must change it. + if (isChecked() != on) { + super.setChecked(on); + } + mAllowChange = true; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchEvent.java new file mode 100644 index 000000000..4b9251dc6 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchEvent.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.switchviewview; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by a ReactSwitchManager once a switch is fully switched on/off + */ +/*package*/ class ReactSwitchEvent extends Event { + + public static final String EVENT_NAME = "topChange"; + + private final boolean mIsChecked; + + public ReactSwitchEvent(int viewId, long timestampMs, boolean isChecked) { + super(viewId, timestampMs); + mIsChecked = isChecked; + } + + public boolean getIsChecked() { + return mIsChecked; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + // All switch events for a given view can be coalesced. + return 0; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + eventData.putBoolean("value", getIsChecked()); + return eventData; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java new file mode 100644 index 000000000..9f62f5d0d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// switchview because switch is a keyword +package com.facebook.react.views.switchviewview; + +import android.os.SystemClock; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; + +import com.facebook.csslayout.CSSNode; +import com.facebook.csslayout.MeasureOutput; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.ReactShadowNode; +import com.facebook.react.uimanager.SimpleViewManager; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIProp; +import com.facebook.react.uimanager.ViewProps; + +/** + * View manager for {@link ReactSwitch} components. + */ +public class ReactSwitchManager extends SimpleViewManager { + + private static final String REACT_CLASS = "AndroidSwitch"; + @UIProp(UIProp.Type.BOOLEAN) public static final String PROP_ENABLED = ViewProps.ENABLED; + @UIProp(UIProp.Type.BOOLEAN) public static final String PROP_ON = ViewProps.ON; + + private static class ReactSwitchShadowNode extends ReactShadowNode implements + CSSNode.MeasureFunction { + + private int mWidth; + private int mHeight; + private boolean mMeasured; + + private ReactSwitchShadowNode() { + setMeasureFunction(this); + } + + @Override + public void measure(CSSNode node, float width, MeasureOutput measureOutput) { + if (!mMeasured) { + // Create a switch with the default config and measure it; since we don't (currently) + // support setting custom switch text, this is fine, as all switches will measure the same + // on a specific device/theme/locale combination. + ReactSwitch reactSwitch = new ReactSwitch(getThemedContext()); + final int spec = View.MeasureSpec.makeMeasureSpec( + ViewGroup.LayoutParams.WRAP_CONTENT, + View.MeasureSpec.UNSPECIFIED); + reactSwitch.measure(spec, spec); + mWidth = reactSwitch.getMeasuredWidth(); + mHeight = reactSwitch.getMeasuredHeight(); + mMeasured = true; + } + measureOutput.width = mWidth; + measureOutput.height = mHeight; + } + } + + private static final CompoundButton.OnCheckedChangeListener ON_CHECKED_CHANGE_LISTENER = + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + ReactContext reactContext = (ReactContext) buttonView.getContext(); + reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent( + new ReactSwitchEvent( + buttonView.getId(), + SystemClock.uptimeMillis(), + isChecked)); + } + }; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + public ReactShadowNode createCSSNodeInstance() { + return new ReactSwitchShadowNode(); + } + + @Override + protected ReactSwitch createViewInstance(ThemedReactContext context) { + ReactSwitch view = new ReactSwitch(context); + view.setShowText(false); + return view; + } + + @Override + public void updateView(ReactSwitch view, CatalystStylesDiffMap props) { + super.updateView(view, props); + if (props.hasKey(PROP_ENABLED)) { + view.setEnabled(props.getBoolean(PROP_ENABLED, true)); + } + if (props.hasKey(PROP_ON)) { + // we set the checked change listener to null and then restore it so that we don't fire an + // onChange event to JS when JS itself is updating the value of the switch + view.setOnCheckedChangeListener(null); + view.setOn(props.getBoolean(PROP_ON, false)); + view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER); + } + } + + @Override + protected void addEventEmitters(final ThemedReactContext reactContext, final ReactSwitch view) { + view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java new file mode 100644 index 000000000..eac5ed6db --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.text; + +import javax.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +import android.graphics.Paint; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.text.style.MetricAffectingSpan; + +public class CustomStyleSpan extends MetricAffectingSpan { + + // Typeface caching is a bit weird: once a Typeface is created, it cannot be changed, so we need + // to cache each font family and each style that they have. Typeface does cache this already in + // Typeface.create(Typeface, style) post API 16, but for that you already need a Typeface. + // Therefore, here we cache one style for each font family, and let Typeface cache all styles for + // that font family. Of course this is not ideal, and especially after adding Typeface loading + // from assets, we will need to have our own caching mechanism for all Typeface creation types. + // TODO: t6866343 add better Typeface caching + private static final Map sTypefaceCache = new HashMap(); + + private final int mStyle; + private final int mWeight; + private final @Nullable String mFontFamily; + + public CustomStyleSpan(int fontStyle, int fontWeight, @Nullable String fontFamily) { + mStyle = fontStyle; + mWeight = fontWeight; + mFontFamily = fontFamily; + } + + @Override + public void updateDrawState(TextPaint ds) { + apply(ds, mStyle, mWeight, mFontFamily); + } + + @Override + public void updateMeasureState(TextPaint paint) { + apply(paint, mStyle, mWeight, mFontFamily); + } + + /** + * Returns {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. + */ + public int getStyle() { + return (mStyle == ReactTextShadowNode.UNSET ? 0 : mStyle); + } + + /** + * Returns {@link Typeface#NORMAL} or {@link Typeface#BOLD}. + */ + public int getWeight() { + return (mWeight == ReactTextShadowNode.UNSET ? 0 : mWeight); + } + + /** + * Returns the font family set for this StyleSpan. + */ + public @Nullable String getFontFamily() { + return mFontFamily; + } + + private static void apply(Paint paint, int style, int weight, @Nullable String family) { + int oldStyle; + Typeface typeface = paint.getTypeface(); + if (typeface == null) { + oldStyle = 0; + } else { + oldStyle = typeface.getStyle(); + } + + int want = 0; + if ((weight == Typeface.BOLD) || + ((oldStyle & Typeface.BOLD) != 0 && weight == ReactTextShadowNode.UNSET)) { + want |= Typeface.BOLD; + } + + if ((style == Typeface.ITALIC) || + ((oldStyle & Typeface.ITALIC) != 0 && style == ReactTextShadowNode.UNSET)) { + want |= Typeface.ITALIC; + } + + if (family != null) { + typeface = getOrCreateTypeface(family, want); + } + + if (typeface != null) { + paint.setTypeface(Typeface.create(typeface, want)); + } else { + paint.setTypeface(Typeface.defaultFromStyle(want)); + } + } + + private static Typeface getOrCreateTypeface(String family, int style) { + if (sTypefaceCache.get(family) != null) { + return sTypefaceCache.get(family); + } + + Typeface typeface = Typeface.create(family, style); + sTypefaceCache.put(family, typeface); + return typeface; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/DefaultStyleValuesUtil.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/DefaultStyleValuesUtil.java new file mode 100644 index 000000000..78aa65a3d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/DefaultStyleValuesUtil.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.text; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; + +/** + * Utility class that access default values from style + */ +public final class DefaultStyleValuesUtil { + + private DefaultStyleValuesUtil() { + throw new AssertionError("Never invoke this for an Utility class!"); + } + + /** + * Utility method that returns the default text hint color as define by the theme + * + * @param context The Context + * @return The ColorStateList for the hint text as defined in the style + */ + public static ColorStateList getDefaultTextColorHint(Context context) { + Resources.Theme theme = context.getTheme(); + TypedArray textAppearances = null; + try { + textAppearances = theme.obtainStyledAttributes(new int[]{android.R.attr.textColorHint}); + ColorStateList textColorHint = textAppearances.getColorStateList(0); + return textColorHint; + } finally { + if (textAppearances != null) { + textAppearances.recycle(); + } + } + } + + /** + * Utility method that returns the default text color as define by the theme + * + * @param context The Context + * @return The ColorStateList for the text as defined in the style + */ + public static ColorStateList getDefaultTextColor(Context context) { + Resources.Theme theme = context.getTheme(); + TypedArray textAppearances = null; + try { + textAppearances = theme.obtainStyledAttributes(new int[]{android.R.attr.textColor}); + ColorStateList textColor = textAppearances.getColorStateList(0); + return textColor; + } finally { + if (textAppearances != null) { + textAppearances.recycle(); + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java new file mode 100644 index 000000000..1dec2e726 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactRawTextManager.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.text; + +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.uimanager.ThemedReactContext; + +/** + * Manages raw text nodes. Since they are used only as a virtual nodes any type of native view + * operation will throw an {@link IllegalStateException} + */ +public class ReactRawTextManager extends ReactTextViewManager { + + @VisibleForTesting + public static final String REACT_CLASS = "RCTRawText"; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + public ReactTextView createViewInstance(ThemedReactContext context) { + throw new IllegalStateException("RKRawText doesn't map into a native view"); + } + + @Override + public void updateView(ReactTextView view, CatalystStylesDiffMap props) { + throw new IllegalStateException("RKRawText doesn't map into a native view"); + } + + @Override + public void updateExtraData(ReactTextView view, Object extraData) { + } + + @Override + public ReactTextShadowNode createCSSNodeInstance() { + return new ReactTextShadowNode(true); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTagSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTagSpan.java new file mode 100644 index 000000000..9bdc7c03f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTagSpan.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.text; + +/** + * Instances of this class are used to place reactTag information of nested text react nodes + * into spannable text rendered by single {@link TextView} + */ +public class ReactTagSpan { + + private final int mReactTag; + + public ReactTagSpan(int reactTag) { + mReactTag = reactTag; + } + + public int getReactTag() { + return mReactTag; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java new file mode 100644 index 000000000..dcd997b6c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java @@ -0,0 +1,394 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.text; + +import javax.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +import android.graphics.Typeface; +import android.text.BoringLayout; +import android.text.Layout; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import android.widget.TextView; + +import com.facebook.csslayout.CSSConstants; +import com.facebook.csslayout.CSSNode; +import com.facebook.csslayout.MeasureOutput; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.uimanager.CSSColorUtil; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.IllegalViewOperationException; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactShadowNode; +import com.facebook.react.uimanager.UIViewOperationQueue; +import com.facebook.react.uimanager.ViewDefaults; +import com.facebook.react.uimanager.ViewProps; + +/** + * {@link ReactShadowNode} class for spannable text view. + * + * This node calculates {@link Spannable} based on subnodes of the same type and passes the + * resulting object down to textview's shadowview and actual native {@link TextView} instance. + * It is important to keep in mind that {@link Spannable} is calculated only on layout step, so if + * there are any text properties that may/should affect the result of {@link Spannable} they should + * be set in a corresponding {@link ReactTextShadowNode}. Resulting {@link Spannable} object is then + * then passed as "computedDataFromMeasure" down to shadow and native view. + * + * TODO(7255858): Rename *CSSNode to *ShadowView (or sth similar) as it's no longer is used + * solely for layouting + */ +public class ReactTextShadowNode extends ReactShadowNode { + + public static final String PROP_TEXT = "text"; + public static final int UNSET = -1; + + private static final TextPaint sTextPaintInstance = new TextPaint(); + + static { + sTextPaintInstance.setFlags(TextPaint.ANTI_ALIAS_FLAG); + } + + private static class SetSpanOperation { + protected int start, end; + protected Object what; + SetSpanOperation(int start, int end, Object what) { + this.start = start; + this.end = end; + this.what = what; + } + public void execute(SpannableStringBuilder sb) { + sb.setSpan(what, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + } + } + + private static final void buildSpannedFromTextCSSNode( + ReactTextShadowNode textCSSNode, + SpannableStringBuilder sb, + List ops) { + int start = sb.length(); + if (textCSSNode.mText != null) { + sb.append(textCSSNode.mText); + } + for (int i = 0, length = textCSSNode.getChildCount(); i < length; i++) { + CSSNode child = textCSSNode.getChildAt(i); + if (child instanceof ReactTextShadowNode) { + buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops); + } else { + throw new IllegalViewOperationException("Unexpected view type nested under text node: " + + child.getClass()); + } + ((ReactTextShadowNode) child).markUpdateSeen(); + } + int end = sb.length(); + if (end > start) { + if (textCSSNode.mIsColorSet) { + ops.add(new SetSpanOperation(start, end, new ForegroundColorSpan(textCSSNode.mColor))); + } + if (textCSSNode.mIsBackgroundColorSet) { + ops.add( + new SetSpanOperation( + start, + end, + new BackgroundColorSpan(textCSSNode.mBackgroundColor))); + } + if (textCSSNode.mFontSize != UNSET) { + ops.add(new SetSpanOperation(start, end, new AbsoluteSizeSpan(textCSSNode.mFontSize))); + } + if (textCSSNode.mFontStyle != UNSET || + textCSSNode.mFontWeight != UNSET || + textCSSNode.mFontFamily != null) { + ops.add(new SetSpanOperation( + start, + end, + new CustomStyleSpan( + textCSSNode.mFontStyle, + textCSSNode.mFontWeight, + textCSSNode.mFontFamily))); + } + ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textCSSNode.getReactTag()))); + } + } + + protected static final Spanned fromTextCSSNode(ReactTextShadowNode textCSSNode) { + SpannableStringBuilder sb = new SpannableStringBuilder(); + // TODO(5837930): Investigate whether it's worth optimizing this part and do it if so + + // The {@link SpannableStringBuilder} implementation require setSpan operation to be called + // up-to-bottom, otherwise all the spannables that are withing the region for which one may set + // a new spannable will be wiped out + List ops = new ArrayList(); + buildSpannedFromTextCSSNode(textCSSNode, sb, ops); + if (textCSSNode.mFontSize == -1) { + sb.setSpan( + new AbsoluteSizeSpan((int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))), + 0, + sb.length(), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + } + for (int i = ops.size() - 1; i >= 0; i--) { + SetSpanOperation op = ops.get(i); + op.execute(sb); + } + return sb; + } + + private static final CSSNode.MeasureFunction TEXT_MEASURE_FUNCTION = + new CSSNode.MeasureFunction() { + @Override + public void measure(CSSNode node, float width, MeasureOutput measureOutput) { + // TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic) + ReactTextShadowNode reactCSSNode = (ReactTextShadowNode) node; + TextPaint textPaint = sTextPaintInstance; + Layout layout; + Spanned text = Assertions.assertNotNull( + reactCSSNode.mPreparedSpannedText, + "Spannable element has not been prepared in onBeforeLayout"); + BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint); + float desiredWidth = boring == null ? + Layout.getDesiredWidth(text, textPaint) : Float.NaN; + + if (boring == null && + (CSSConstants.isUndefined(width) || + (!CSSConstants.isUndefined(desiredWidth) && desiredWidth <= width))) { + // Is used when the width is not known and the text is not boring, ie. if it contains + // unicode characters. + layout = new StaticLayout( + text, + textPaint, + (int) Math.ceil(desiredWidth), + Layout.Alignment.ALIGN_NORMAL, + 1, + 0, + true); + } else if (boring != null && (CSSConstants.isUndefined(width) || boring.width <= width)) { + // Is used for single-line, boring text when the width is either unknown or bigger + // than the width of the text. + layout = BoringLayout.make( + text, + textPaint, + boring.width, + Layout.Alignment.ALIGN_NORMAL, + 1, + 0, + boring, + true); + } else { + // Is used for multiline, boring text and the width is known. + layout = new StaticLayout( + text, + textPaint, + (int) width, + Layout.Alignment.ALIGN_NORMAL, + 1, + 0, + true); + } + + measureOutput.height = layout.getHeight(); + measureOutput.width = layout.getWidth(); + if (reactCSSNode.mNumberOfLines != UNSET && + reactCSSNode.mNumberOfLines < layout.getLineCount()) { + measureOutput.height = layout.getLineBottom(reactCSSNode.mNumberOfLines - 1); + } + if (reactCSSNode.mLineHeight != UNSET) { + int lines = reactCSSNode.mNumberOfLines != UNSET + ? Math.min(reactCSSNode.mNumberOfLines, layout.getLineCount()) + : layout.getLineCount(); + float lineHeight = PixelUtil.toPixelFromSP(reactCSSNode.mLineHeight); + measureOutput.height = lineHeight * lines; + } + } + }; + + /** + * Return -1 if the input string is not a valid numeric fontWeight (100, 200, ..., 900), otherwise + * return the weight. + */ + private static int parseNumericFontWeight(String fontWeightString) { + // This should be much faster than using regex to verify input and Integer.parseInt + return fontWeightString.length() == 3 && fontWeightString.endsWith("00") + && fontWeightString.charAt(0) <= '9' && fontWeightString.charAt(0) >= '1' ? + 100 * (fontWeightString.charAt(0) - '0') : -1; + } + + private int mLineHeight = UNSET; + private int mNumberOfLines = UNSET; + private boolean mIsColorSet = false; + private int mColor; + private boolean mIsBackgroundColorSet = false; + private int mBackgroundColor; + private int mFontSize = UNSET; + /** + * mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. + * mFontWeight can be {@link Typeface#NORMAL} or {@link Typeface#BOLD}. + */ + private int mFontStyle = UNSET; + private int mFontWeight = UNSET; + /** + * NB: If a font family is used that does not have a style in a certain Android version (ie. + * monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text + * nodes. To retain that style, you have to add it to those nodes explicitly. + * Example, Android 4.4: + * Bold Text + * Bold Text + * Bold Text + * + * Not Bold Text + * Not Bold Text + * Not Bold Text + * + * Not Bold Text + * Bold Text + * Bold Text + */ + private @Nullable String mFontFamily = null; + private @Nullable String mText = null; + + private @Nullable Spanned mPreparedSpannedText; + private final boolean mIsVirtual; + + @Override + public void onBeforeLayout() { + if (mIsVirtual) { + return; + } + mPreparedSpannedText = fromTextCSSNode(this); + markUpdated(); + } + + @Override + protected void markUpdated() { + super.markUpdated(); + // We mark virtual anchor node as dirty as updated text needs to be re-measured + if (!mIsVirtual) { + super.dirty(); + } + } + + @Override + public void updateProperties(CatalystStylesDiffMap styles) { + super.updateProperties(styles); + + if (styles.hasKey(PROP_TEXT)) { + mText = styles.getString(PROP_TEXT); + markUpdated(); + } + if (styles.hasKey(ViewProps.NUMBER_OF_LINES)) { + mNumberOfLines = styles.getInt(ViewProps.NUMBER_OF_LINES, UNSET); + markUpdated(); + } + if (styles.hasKey(ViewProps.LINE_HEIGHT)) { + mLineHeight = styles.getInt(ViewProps.LINE_HEIGHT, UNSET); + markUpdated(); + } + if (styles.hasKey(ViewProps.FONT_SIZE)) { + if (styles.isNull(ViewProps.FONT_SIZE)) { + mFontSize = UNSET; + } else { + mFontSize = (int) Math.ceil(PixelUtil.toPixelFromSP( + styles.getFloat(ViewProps.FONT_SIZE, ViewDefaults.FONT_SIZE_SP))); + } + markUpdated(); + } + if (styles.hasKey(ViewProps.COLOR)) { + String colorString = styles.getString(ViewProps.COLOR); + if (colorString == null) { + mIsColorSet = false; + } else { + mColor = CSSColorUtil.getColor(colorString); + mIsColorSet = true; + } + markUpdated(); + } + if (styles.hasKey(ViewProps.BACKGROUND_COLOR)) { + String colorString = styles.getString(ViewProps.BACKGROUND_COLOR); + if (colorString == null) { + mIsBackgroundColorSet = false; + } else { + mBackgroundColor = CSSColorUtil.getColor(colorString); + mIsBackgroundColorSet = true; + } + markUpdated(); + } + + if (styles.hasKey(ViewProps.FONT_FAMILY)) { + mFontFamily = styles.getString(ViewProps.FONT_FAMILY); + markUpdated(); + } + + if (styles.hasKey(ViewProps.FONT_WEIGHT)) { + String fontWeightString = styles.getString(ViewProps.FONT_WEIGHT); + int fontWeightNumeric = fontWeightString != null ? + parseNumericFontWeight(fontWeightString) : -1; + int fontWeight = UNSET; + if (fontWeightNumeric >= 500 || "bold".equals(fontWeightString)) { + fontWeight = Typeface.BOLD; + } else if ("normal".equals(fontWeightString) || + (fontWeightNumeric != -1 && fontWeightNumeric < 500)) { + fontWeight = Typeface.NORMAL; + } + if (fontWeight != mFontWeight) { + mFontWeight = fontWeight; + markUpdated(); + } + } + + if (styles.hasKey(ViewProps.FONT_STYLE)) { + String fontStyleString = styles.getString(ViewProps.FONT_STYLE); + int fontStyle = UNSET; + if ("italic".equals(fontStyleString)) { + fontStyle = Typeface.ITALIC; + } else if ("normal".equals(fontStyleString)) { + fontStyle = Typeface.NORMAL; + } + if (fontStyle != mFontStyle) { + mFontStyle = fontStyle; + markUpdated(); + } + } + } + + @Override + public boolean isVirtualAnchor() { + return !mIsVirtual; + } + + @Override + public boolean isVirtual() { + return mIsVirtual; + } + + @Override + public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { + if (mIsVirtual) { + return; + } + super.onCollectExtraUpdates(uiViewOperationQueue); + if (mPreparedSpannedText != null) { + uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), mPreparedSpannedText); + } + } + + public ReactTextShadowNode(boolean isVirtual) { + mIsVirtual = isVirtual; + if (!isVirtual) { + setMeasureFunction(TEXT_MEASURE_FUNCTION); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java new file mode 100644 index 000000000..36d3923fe --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.text; + +import android.content.Context; +import android.text.Layout; +import android.text.Spanned; +import android.widget.TextView; + +import com.facebook.react.uimanager.ReactCompoundView; + +public class ReactTextView extends TextView implements ReactCompoundView { + + public ReactTextView(Context context) { + super(context); + } + + @Override + public int reactTagForTouch(float touchX, float touchY) { + Spanned text = (Spanned) getText(); + int target = getId(); + + int x = (int) touchX; + int y = (int) touchY; + + x -= getTotalPaddingLeft(); + y -= getTotalPaddingTop(); + + x += getScrollX(); + y += getScrollY(); + + Layout layout = getLayout(); + int line = layout.getLineForVertical(y); + + int lineStartX = (int) layout.getLineLeft(line); + int lineEndX = (int) layout.getLineRight(line); + + // TODO(5966918): Consider extending touchable area for text spans by some DP constant + if (x >= lineStartX && x <= lineEndX) { + int index = layout.getOffsetForHorizontal(line, x); + + // We choose the most inner span (shortest) containing character at the given index + // if no such span can be found we will send the textview's react id as a touch handler + // In case when there are more than one spans with same length we choose the last one + // from the spans[] array, since it correspond to the most inner react element + ReactTagSpan[] spans = text.getSpans(index, index, ReactTagSpan.class); + + if (spans != null) { + int targetSpanTextLength = text.length(); + for (int i = 0; i < spans.length; i++) { + int spanStart = text.getSpanStart(spans[i]); + int spanEnd = text.getSpanEnd(spans[i]); + if (spanEnd > index && (spanEnd - spanStart) <= targetSpanTextLength) { + target = spans[i].getReactTag(); + targetSpanTextLength = (spanEnd - spanStart); + } + } + } + } + + return target; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java new file mode 100644 index 000000000..e78b15cef --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.text; + +import android.text.Spannable; +import android.text.Spanned; +import android.text.TextUtils; +import android.view.Gravity; +import android.widget.TextView; + +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.uimanager.BaseViewPropertyApplicator; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIProp; +import com.facebook.react.uimanager.ViewDefaults; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.common.annotations.VisibleForTesting; + +/** + * Manages instances of spannable {@link TextView}. + * + * This is a "shadowing" view manager, which means that the {@link NativeViewHierarchyManager} will + * not manage children of native {@link TextView} instances returned by this manager. Instead we use + * @{link ReactTextShadowNode} hierarchy to calculate a {@link Spannable} text representing the + * whole text subtree. + */ +public class ReactTextViewManager extends ViewManager { + + @VisibleForTesting + public static final String REACT_CLASS = "RCTText"; + + @UIProp(UIProp.Type.NUMBER) + public static final String PROP_NUMBER_OF_LINES = ViewProps.NUMBER_OF_LINES; + @UIProp(UIProp.Type.STRING) + public static final String PROP_TEXT_ALIGN = ViewProps.TEXT_ALIGN; + @UIProp(UIProp.Type.NUMBER) + public static final String PROP_LINE_HEIGHT = ViewProps.LINE_HEIGHT; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + public ReactTextView createViewInstance(ThemedReactContext context) { + return new ReactTextView(context); + } + + @Override + public void updateView(ReactTextView view, CatalystStylesDiffMap props) { + BaseViewPropertyApplicator.applyCommonViewProperties(view, props); + // maxLines can only be set in master view (block), doesn't really make sense to set in a span + if (props.hasKey(PROP_NUMBER_OF_LINES)) { + view.setMaxLines(props.getInt(PROP_NUMBER_OF_LINES, ViewDefaults.NUMBER_OF_LINES)); + view.setEllipsize(TextUtils.TruncateAt.END); + } + // same with textAlign + if (props.hasKey(PROP_TEXT_ALIGN)) { + final String textAlign = props.getString(PROP_TEXT_ALIGN); + if (textAlign == null || "auto".equals(textAlign)) { + view.setGravity(Gravity.NO_GRAVITY); + } else if ("left".equals(textAlign)) { + view.setGravity(Gravity.LEFT); + } else if ("right".equals(textAlign)) { + view.setGravity(Gravity.RIGHT); + } else if ("center".equals(textAlign)) { + view.setGravity(Gravity.CENTER_HORIZONTAL); + } else { + throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign); + } + } + // same for lineSpacing + if (props.hasKey(PROP_LINE_HEIGHT)) { + if (props.isNull(PROP_LINE_HEIGHT)) { + view.setLineSpacing(0, 1); + } else { + float lineHeight = + PixelUtil.toPixelFromSP(props.getInt(PROP_LINE_HEIGHT, ViewDefaults.LINE_HEIGHT)); + view.setLineSpacing(lineHeight, 0); + } + } + } + + @Override + public void updateExtraData(ReactTextView view, Object extraData) { + view.setText((Spanned) extraData); + } + + @Override + public ReactTextShadowNode createCSSNodeInstance() { + return new ReactTextShadowNode(false); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextViewManager.java new file mode 100644 index 000000000..b11af8c57 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactVirtualTextViewManager.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.text; + +import com.facebook.react.common.annotations.VisibleForTesting; + +/** + * Manages raw text nodes. Since they are used only as a virtual nodes any type of native view + * operation will throw an {@link IllegalStateException} + */ +public class ReactVirtualTextViewManager extends ReactRawTextManager { + + @VisibleForTesting + public static final String REACT_CLASS = "RCTVirtualText"; + + @Override + public String getName() { + return REACT_CLASS; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java new file mode 100644 index 000000000..863d99607 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -0,0 +1,275 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.textinput; + +import javax.annotation.Nullable; + +import java.util.ArrayList; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Editable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +import com.facebook.infer.annotation.Assertions; + +/** + * A wrapper around the EditText that lets us better control what happens when an EditText gets + * focused or blurred, and when to display the soft keyboard and when not to. + * + * ReactEditTexts have setFocusableInTouchMode set to false automatically because touches on the + * EditText are managed on the JS side. This also removes the nasty side effect that EditTexts + * have, which is that focus is always maintained on one of the EditTexts. + * + * The wrapper stops the EditText from triggering *TextChanged events, in the case where JS + * has called this explicitly. This is the default behavior on other platforms as well. + * VisibleForTesting from {@link TextInputEventsTestCase}. + */ +public class ReactEditText extends EditText { + + private final InputMethodManager mInputMethodManager; + // This flag is set to true when we set the text of the EditText explicitly. In that case, no + // *TextChanged events should be triggered. This is less expensive than removing the text + // listeners and adding them back again after the text change is completed. + private boolean mIsSettingTextFromJS; + // This component is controlled, so we want it to get focused only when JS ask it to do so. + // Whenever android requests focus (which it does for random reasons), it will be ignored. + private boolean mIsJSSettingFocus; + private int mDefaultGravityHorizontal; + private int mDefaultGravityVertical; + private int mNativeEventCount; + private @Nullable ArrayList mListeners; + private @Nullable TextWatcherDelegator mTextWatcherDelegator; + + public ReactEditText(Context context) { + super(context); + setFocusableInTouchMode(false); + + mInputMethodManager = (InputMethodManager) + Assertions.assertNotNull(getContext().getSystemService(Context.INPUT_METHOD_SERVICE)); + mDefaultGravityHorizontal = + getGravity() & (Gravity.HORIZONTAL_GRAVITY_MASK | Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK); + mDefaultGravityVertical = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; + mNativeEventCount = 0; + mIsSettingTextFromJS = false; + mIsJSSettingFocus = false; + mListeners = null; + mTextWatcherDelegator = null; + } + + // After the text changes inside an EditText, TextView checks if a layout() has been requested. + // If it has, it will not scroll the text to the end of the new text inserted, but wait for the + // next layout() to be called. However, we do not perform a layout() after a requestLayout(), so + // we need to override isLayoutRequested to force EditText to scroll to the end of the new text + // immediately. + // TODO: t6408636 verify if we should schedule a layout after a View does a requestLayout() + @Override + public boolean isLayoutRequested() { + return false; + } + + // Consume 'Enter' key events: TextView tries to give focus to the next TextInput, but it can't + // since we only allow JS to change focus, which in turn causes TextView to crash. + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + hideSoftKeyboard(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + public void clearFocus() { + setFocusableInTouchMode(false); + super.clearFocus(); + hideSoftKeyboard(); + } + + @Override + public boolean requestFocus(int direction, Rect previouslyFocusedRect) { + if (!mIsJSSettingFocus) { + return false; + } + setFocusableInTouchMode(true); + boolean focused = super.requestFocus(direction, previouslyFocusedRect); + showSoftKeyboard(); + return focused; + } + + @Override + public void addTextChangedListener(TextWatcher watcher) { + if (mListeners == null) { + mListeners = new ArrayList<>(); + super.addTextChangedListener(getTextWatcherDelegator()); + } + + mListeners.add(watcher); + } + + @Override + public void removeTextChangedListener(TextWatcher watcher) { + if (mListeners != null) { + mListeners.remove(watcher); + + if (mListeners.isEmpty()) { + mListeners = null; + super.removeTextChangedListener(getTextWatcherDelegator()); + } + } + } + + /* package */ void requestFocusFromJS() { + mIsJSSettingFocus = true; + requestFocus(); + mIsJSSettingFocus = false; + } + + /* package */ void clearFocusFromJS() { + clearFocus(); + } + + // VisibleForTesting from {@link TextInputEventsTestCase}. + public int incrementAndGetEventCounter() { + return ++mNativeEventCount; + } + + // VisibleForTesting from {@link TextInputEventsTestCase}. + public void maybeSetText(ReactTextUpdate reactTextUpdate) { + // Only set the text if it is up to date. + if (reactTextUpdate.getJsEventCounter() < mNativeEventCount) { + return; + } + + // The current text gets replaced with the text received from JS. However, the spans on the + // current text need to be adapted to the new text. Since TextView#setText() will remove or + // reset some of these spans even if they are set directly, SpannableStringBuilder#replace() is + // used instead (this is also used by the the keyboard implementation underneath the covers). + SpannableStringBuilder spannableStringBuilder = + new SpannableStringBuilder(reactTextUpdate.getText()); + manageSpans(spannableStringBuilder); + mIsSettingTextFromJS = true; + getText().replace(0, length(), spannableStringBuilder); + mIsSettingTextFromJS = false; + } + + /** + * Remove and/or add {@link Spanned.SPAN_EXCLUSIVE_EXCLUSIVE} spans, since they should only exist + * as long as the text they cover is the same. All other spans will remain the same, since they + * will adapt to the new text, hence why {@link SpannableStringBuilder#replace} never removes + * them. + */ + private void manageSpans(SpannableStringBuilder spannableStringBuilder) { + Object[] spans = getText().getSpans(0, length(), Object.class); + for (int spanIdx = 0; spanIdx < spans.length; spanIdx++) { + if ((getText().getSpanFlags(spans[spanIdx]) & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) != + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) { + continue; + } + Object span = spans[spanIdx]; + final int spanStart = getText().getSpanStart(spans[spanIdx]); + final int spanEnd = getText().getSpanEnd(spans[spanIdx]); + final int spanFlags = getText().getSpanFlags(spans[spanIdx]); + + // Make sure the span is removed from existing text, otherwise the spans we set will be + // ignored or it will cover text that has changed. + getText().removeSpan(spans[spanIdx]); + if (sameTextForSpan(getText(), spannableStringBuilder, spanStart, spanEnd)) { + spannableStringBuilder.setSpan(span, spanStart, spanEnd, spanFlags); + } + } + } + + private static boolean sameTextForSpan( + final Editable oldText, + final SpannableStringBuilder newText, + final int start, + final int end) { + if (start > newText.length() || end > newText.length()) { + return false; + } + for (int charIdx = start; charIdx < end; charIdx++) { + if (oldText.charAt(charIdx) != newText.charAt(charIdx)) { + return false; + } + } + return true; + } + + private boolean showSoftKeyboard() { + return mInputMethodManager.showSoftInput(this, 0); + } + + private void hideSoftKeyboard() { + mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + } + + private TextWatcherDelegator getTextWatcherDelegator() { + if (mTextWatcherDelegator == null) { + mTextWatcherDelegator = new TextWatcherDelegator(); + } + return mTextWatcherDelegator; + } + + /* package */ void setGravityHorizontal(int gravityHorizontal) { + if (gravityHorizontal == 0) { + gravityHorizontal = mDefaultGravityHorizontal; + } + setGravity( + (getGravity() & ~Gravity.HORIZONTAL_GRAVITY_MASK & + ~Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) | gravityHorizontal); + } + + /* package */ void setGravityVertical(int gravityVertical) { + if (gravityVertical == 0) { + gravityVertical = mDefaultGravityVertical; + } + setGravity((getGravity() & ~Gravity.VERTICAL_GRAVITY_MASK) | gravityVertical); + } + + /** + * This class will redirect *TextChanged calls to the listeners only in the case where the text + * is changed by the user, and not explicitly set by JS. + */ + private class TextWatcherDelegator implements TextWatcher { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + if (!mIsSettingTextFromJS && mListeners != null) { + for (TextWatcher listener : mListeners) { + listener.beforeTextChanged(s, start, count, after); + } + } + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (!mIsSettingTextFromJS && mListeners != null) { + for (TextWatcher listener : mListeners) { + listener.onTextChanged(s, start, before, count); + } + } + } + + @Override + public void afterTextChanged(Editable s) { + if (!mIsSettingTextFromJS && mListeners != null) { + for (android.text.TextWatcher listener : mListeners) { + listener.afterTextChanged(s); + } + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java new file mode 100644 index 000000000..f7363441b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextChangedEvent.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.textinput; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by EditText native view when text changes. + */ +/* package */ class ReactTextChangedEvent extends Event { + + public static final String EVENT_NAME = "topChange"; + + private String mText; + private int mContentWidth; + private int mContentHeight; + private int mEventCount; + + public ReactTextChangedEvent( + int viewId, + long timestampMs, + String text, + int contentSizeWidth, + int contentSizeHeight, + int eventCount) { + super(viewId, timestampMs); + mText = text; + mContentWidth = contentSizeWidth; + mContentHeight = contentSizeHeight; + mEventCount = eventCount; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putString("text", mText); + + WritableMap contentSize = Arguments.createMap(); + contentSize.putDouble("width", mContentWidth); + contentSize.putDouble("height", mContentHeight); + eventData.putMap("contentSize", contentSize); + eventData.putInt("eventCount", mEventCount); + + eventData.putInt("target", getViewTag()); + return eventData; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.java new file mode 100644 index 000000000..2b77c3140 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputBlurEvent.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.textinput; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by EditText native view when it loses focus. + */ +/* package */ class ReactTextInputBlurEvent extends Event { + + private static final String EVENT_NAME = "topBlur"; + + public ReactTextInputBlurEvent( + int viewId, + long timestampMs) { + super(viewId, timestampMs); + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + return eventData; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEndEditingEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEndEditingEvent.java new file mode 100644 index 000000000..ff99d68f7 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEndEditingEvent.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.textinput; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by EditText native view when text editing ends, + * because of the user leaving the text input. + */ +class ReactTextInputEndEditingEvent extends Event { + + private static final String EVENT_NAME = "topEndEditing"; + + private String mText; + + public ReactTextInputEndEditingEvent( + int viewId, + long timestampMs, + String text) { + super(viewId, timestampMs); + mText = text; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + eventData.putString("text", mText); + return eventData; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEvent.java new file mode 100644 index 000000000..f2cbc8b91 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputEvent.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.textinput; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by EditText native view when text changes. + */ +/* package */ class ReactTextInputEvent extends Event { + + public static final String EVENT_NAME = "topTextInput"; + + private String mText; + private String mPreviousText; + private int mRangeStart; + private int mRangeEnd; + + public ReactTextInputEvent( + int viewId, + long timestampMs, + String text, + String previousText, + int rangeStart, + int rangeEnd) { + super(viewId, timestampMs); + mText = text; + mPreviousText = previousText; + mRangeStart = rangeStart; + mRangeEnd = rangeEnd; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + // We don't want to miss any textinput event, as event data is incremental. + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + WritableMap range = Arguments.createMap(); + range.putDouble("start", mRangeStart); + range.putDouble("end", mRangeEnd); + + eventData.putString("text", mText); + eventData.putString("previousText", mPreviousText); + eventData.putMap("range", range); + + eventData.putInt("target", getViewTag()); + return eventData; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.java new file mode 100644 index 000000000..e593e851e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputFocusEvent.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.textinput; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by EditText native view when it receives focus. + */ +/* package */ class ReactTextInputFocusEvent extends Event { + + private static final String EVENT_NAME = "topFocus"; + + public ReactTextInputFocusEvent( + int viewId, + long timestampMs) { + super(viewId, timestampMs); + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + return eventData; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java new file mode 100644 index 000000000..d2f4251eb --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -0,0 +1,445 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.textinput; + +import javax.annotation.Nullable; + +import java.util.Map; + +import android.graphics.PorterDuff; +import android.os.SystemClock; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.TextView; + +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.JSApplicationCausedNativeException; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.BaseViewPropertyApplicator; +import com.facebook.react.uimanager.CSSColorUtil; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.UIProp; +import com.facebook.react.uimanager.ViewDefaults; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.text.DefaultStyleValuesUtil; + +/** + * Manages instances of TextInput. + */ +public class ReactTextInputManager extends ViewManager { + + /* package */ static final String REACT_CLASS = "AndroidTextInput"; + + private static final int FOCUS_TEXT_INPUT = 1; + private static final int BLUR_TEXT_INPUT = 2; + + @UIProp(UIProp.Type.NUMBER) + public static final String PROP_FONT_SIZE = ViewProps.FONT_SIZE; + @UIProp(UIProp.Type.BOOLEAN) + public static final String PROP_TEXT_INPUT_AUTO_CORRECT = "autoCorrect"; + @UIProp(UIProp.Type.NUMBER) + public static final String PROP_TEXT_INPUT_AUTO_CAPITALIZE = "autoCapitalize"; + @UIProp(UIProp.Type.NUMBER) + public static final String PROP_TEXT_ALIGN = "textAlign"; + @UIProp(UIProp.Type.NUMBER) + public static final String PROP_TEXT_ALIGN_VERTICAL = "textAlignVertical"; + @UIProp(UIProp.Type.STRING) + public static final String PROP_TEXT_INPUT_HINT = "placeholder"; + @UIProp(UIProp.Type.STRING) + public static final String PROP_TEXT_INPUT_HINT_COLOR = "placeholderTextColor"; + @UIProp(UIProp.Type.NUMBER) + public static final String PROP_TEXT_INPUT_NUMLINES = ViewProps.NUMBER_OF_LINES; + @UIProp(UIProp.Type.BOOLEAN) + public static final String PROP_TEXT_INPUT_MULTILINE = "multiline"; + @UIProp(UIProp.Type.STRING) + public static final String PROP_TEXT_INPUT_KEYBOARD_TYPE = "keyboardType"; + @UIProp(UIProp.Type.BOOLEAN) + public static final String PROP_TEXT_INPUT_PASSWORD = "password"; + @UIProp(UIProp.Type.BOOLEAN) + public static final String PROP_TEXT_INPUT_EDITABLE = "editable"; + @UIProp(UIProp.Type.STRING) + public static final String PROP_TEXT_INPUT_UNDERLINE_COLOR = "underlineColorAndroid"; + + private static final String KEYBOARD_TYPE_EMAIL_ADDRESS = "email-address"; + private static final String KEYBOARD_TYPE_NUMERIC = "numeric"; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + public ReactEditText createViewInstance(ThemedReactContext context) { + ReactEditText editText = new ReactEditText(context); + int inputType = editText.getInputType(); + editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE)); + editText.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))); + return editText; + } + + @Override + public ReactTextInputShadowNode createCSSNodeInstance() { + return new ReactTextInputShadowNode(); + } + + @Nullable + @Override + public Map getExportedCustomBubblingEventTypeConstants() { + return MapBuilder.builder() + .put( + "topSubmitEditing", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of( + "bubbled", "onSubmitEditing", "captured", "onSubmitEditingCapture"))) + .put( + "topEndEditing", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onEndEditing", "captured", "onEndEditingCapture"))) + .put( + "topTextInput", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onTextInput", "captured", "onTextInputCapture"))) + .put( + "topFocus", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onFocus", "captured", "onFocusCapture"))) + .put( + "topBlur", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onBlur", "captured", "onBlurCapture"))) + .build(); + } + + @Override + public @Nullable Map getCommandsMap() { + return MapBuilder.of("focusTextInput", FOCUS_TEXT_INPUT, "blurTextInput", BLUR_TEXT_INPUT); + } + + @Override + public void receiveCommand( + ReactEditText reactEditText, + int commandId, + @Nullable ReadableArray args) { + switch (commandId) { + case FOCUS_TEXT_INPUT: + reactEditText.requestFocusFromJS(); + break; + case BLUR_TEXT_INPUT: + reactEditText.clearFocusFromJS(); + break; + } + } + + @Override + public void updateExtraData(ReactEditText view, Object extraData) { + if (extraData instanceof float[]) { + float[] padding = (float[]) extraData; + + view.setPadding( + (int) Math.ceil(padding[0]), + (int) Math.ceil(padding[1]), + (int) Math.ceil(padding[2]), + (int) Math.ceil(padding[3])); + } else if (extraData instanceof ReactTextUpdate) { + view.maybeSetText((ReactTextUpdate) extraData); + } + } + + @Override + public void updateView(ReactEditText view, CatalystStylesDiffMap props) { + BaseViewPropertyApplicator.applyCommonViewProperties(view, props); + + if (props.hasKey(PROP_FONT_SIZE)) { + float textSize = props.getFloat(PROP_FONT_SIZE, ViewDefaults.FONT_SIZE_SP); + view.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + (int) Math.ceil(PixelUtil.toPixelFromSP(textSize))); + } + + //Prevents flickering color while waiting for JS update. + if (props.hasKey(ViewProps.COLOR)) { + final String colorStr = props.getString(ViewProps.COLOR); + if (colorStr != null) { + final int color = CSSColorUtil.getColor(colorStr); + view.setTextColor(color); + } else { + view.setTextColor(DefaultStyleValuesUtil.getDefaultTextColor(view.getContext())); + } + } + + if (props.hasKey(PROP_TEXT_INPUT_HINT)) { + view.setHint(props.getString(PROP_TEXT_INPUT_HINT)); + } + + if (props.hasKey(PROP_TEXT_INPUT_HINT_COLOR)) { + final String colorStr = props.getString(PROP_TEXT_INPUT_HINT_COLOR); + if (colorStr != null) { + final int color = CSSColorUtil.getColor(colorStr); + view.setHintTextColor(color); + } else { + view.setHintTextColor(DefaultStyleValuesUtil.getDefaultTextColorHint(view.getContext())); + // We need to invalidate in order to force EditText to update hint color. + // see updateTextColors() method in TextView.java + view.invalidate(); + } + } + + if (props.hasKey(PROP_TEXT_INPUT_UNDERLINE_COLOR)) { + String colorStr = props.getString(PROP_TEXT_INPUT_UNDERLINE_COLOR); + if (colorStr != null) { + int color = CSSColorUtil.getColor(colorStr); + view.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN); + } else { + view.getBackground().clearColorFilter(); + } + } + + if (props.hasKey(PROP_TEXT_ALIGN)) { + int gravityHorizontal = props.getInt(PROP_TEXT_ALIGN, 0); + view.setGravityHorizontal(gravityHorizontal); + } + + if (props.hasKey(PROP_TEXT_ALIGN_VERTICAL)) { + int gravityVertical = props.getInt(PROP_TEXT_ALIGN_VERTICAL, 0); + view.setGravityVertical(gravityVertical); + } + + if (props.hasKey(PROP_TEXT_INPUT_EDITABLE)) { + if (props.getBoolean(PROP_TEXT_INPUT_EDITABLE, true)) { + view.setEnabled(true); + } else { + view.setEnabled(false); + } + } + + // newInputType will collect all content attributes that have to be set in the InputText. + int newInputType = view.getInputType(); + + if (props.hasKey(PROP_TEXT_INPUT_AUTO_CORRECT)) { + // clear auto correct flags + newInputType + &= ~(InputType.TYPE_TEXT_FLAG_AUTO_CORRECT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + if (props.getBoolean(PROP_TEXT_INPUT_AUTO_CORRECT, false)) { + newInputType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; + } else if (!props.isNull(PROP_TEXT_INPUT_AUTO_CORRECT)) { + newInputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; + } + } + + if (props.hasKey(PROP_TEXT_INPUT_MULTILINE)) { + if (props.getBoolean(PROP_TEXT_INPUT_MULTILINE, false)) { + newInputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE; + } else { + newInputType &= ~InputType.TYPE_TEXT_FLAG_MULTI_LINE; + } + } + + if (props.hasKey(PROP_TEXT_INPUT_KEYBOARD_TYPE)) { + // reset keyboard type defaults + newInputType = newInputType & + ~InputType.TYPE_CLASS_NUMBER & + ~InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; + + String keyboardType = props.getString(PROP_TEXT_INPUT_KEYBOARD_TYPE); + if (KEYBOARD_TYPE_NUMERIC.equalsIgnoreCase(keyboardType)) { + newInputType |= InputType.TYPE_CLASS_NUMBER; + } else if (KEYBOARD_TYPE_EMAIL_ADDRESS.equalsIgnoreCase(keyboardType)) { + newInputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; + } + } + + if (props.hasKey(PROP_TEXT_INPUT_PASSWORD)) { + if (props.getBoolean(PROP_TEXT_INPUT_PASSWORD, false)) { + newInputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD; + } else { + newInputType &= ~InputType.TYPE_TEXT_VARIATION_PASSWORD; + } + } + + if (props.hasKey(PROP_TEXT_INPUT_AUTO_CAPITALIZE)) { + // clear auto capitalization flags + newInputType &= ~( + InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | + InputType.TYPE_TEXT_FLAG_CAP_WORDS | + InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS); + int autoCapitalize = props.getInt(PROP_TEXT_INPUT_AUTO_CAPITALIZE, InputType.TYPE_CLASS_TEXT); + + switch (autoCapitalize) { + case InputType.TYPE_TEXT_FLAG_CAP_SENTENCES: + case InputType.TYPE_TEXT_FLAG_CAP_WORDS: + case InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS: + case InputType.TYPE_CLASS_TEXT: + newInputType |= autoCapitalize; + break; + default: + throw new + JSApplicationCausedNativeException("Invalid autoCapitalize value: " + autoCapitalize); + } + } + + if (view.getInputType() != newInputType) { + view.setInputType(newInputType); + } + + if (props.hasKey(PROP_TEXT_INPUT_NUMLINES)) { + view.setLines(props.getInt(PROP_TEXT_INPUT_NUMLINES, 1)); + } + } + + private class ReactTextInputTextWatcher implements TextWatcher { + + private EventDispatcher mEventDispatcher; + private ReactEditText mEditText; + private String mPreviousText; + + public ReactTextInputTextWatcher( + final ReactContext reactContext, + final ReactEditText editText) { + mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + mEditText = editText; + mPreviousText = null; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Incoming charSequence gets mutated before onTextChanged() is invoked + mPreviousText = s.toString(); + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Rearranging the text (i.e. changing between singleline and multiline attributes) can + // also trigger onTextChanged, call the event in JS only when the text actually changed + if (count > 0 || before > 0) { + Assertions.assertNotNull(mPreviousText); + + int contentWidth = mEditText.getWidth(); + int contentHeight = mEditText.getHeight(); + + // Use instead size of text content within EditText when available + if (mEditText.getLayout() != null) { + contentWidth = mEditText.getCompoundPaddingLeft() + mEditText.getLayout().getWidth() + + mEditText.getCompoundPaddingRight(); + contentHeight = mEditText.getCompoundPaddingTop() + mEditText.getLayout().getHeight() + + mEditText.getCompoundPaddingTop(); + } + + // The event that contains the event counter and updates it must be sent first. + // TODO: t7936714 merge these events + mEventDispatcher.dispatchEvent( + new ReactTextChangedEvent( + mEditText.getId(), + SystemClock.uptimeMillis(), + s.toString(), + (int) PixelUtil.toDIPFromPixel(contentWidth), + (int) PixelUtil.toDIPFromPixel(contentHeight), + mEditText.incrementAndGetEventCounter())); + + mEventDispatcher.dispatchEvent( + new ReactTextInputEvent( + mEditText.getId(), + SystemClock.uptimeMillis(), + count > 0 ? s.toString().substring(start, start + count) : "", + before > 0 ? mPreviousText.substring(start, start + before) : "", + start, + count > 0 ? start + count - 1 : start + before)); + } + } + + @Override + public void afterTextChanged(Editable s) { + } + } + + @Override + protected void addEventEmitters( + final ThemedReactContext reactContext, + final ReactEditText editText) { + editText.addTextChangedListener(new ReactTextInputTextWatcher(reactContext, editText)); + editText.setOnFocusChangeListener( + new View.OnFocusChangeListener() { + public void onFocusChange(View v, boolean hasFocus) { + EventDispatcher eventDispatcher = + reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + if (hasFocus) { + eventDispatcher.dispatchEvent( + new ReactTextInputFocusEvent( + editText.getId(), + SystemClock.uptimeMillis())); + } else { + eventDispatcher.dispatchEvent( + new ReactTextInputBlurEvent( + editText.getId(), + SystemClock.uptimeMillis())); + + eventDispatcher.dispatchEvent( + new ReactTextInputEndEditingEvent( + editText.getId(), + SystemClock.uptimeMillis(), + editText.getText().toString())); + } + } + }); + + editText.setOnEditorActionListener( + new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent keyEvent) { + // Any 'Enter' action will do + if ((actionId & EditorInfo.IME_MASK_ACTION) > 0 || + actionId == EditorInfo.IME_NULL) { + EventDispatcher eventDispatcher = + reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + eventDispatcher.dispatchEvent( + new ReactTextInputSubmitEditingEvent( + editText.getId(), + SystemClock.uptimeMillis(), + editText.getText().toString())); + } + return false; + } + }); + } + + @Override + public @Nullable Map getExportedViewConstants() { + return MapBuilder.of( + "TextAlign", + MapBuilder.of( + "start", Gravity.START, + "center", Gravity.CENTER_HORIZONTAL, + "end", Gravity.END), + "TextAlignVertical", + MapBuilder.of( + "top", Gravity.TOP, + "center", Gravity.CENTER_VERTICAL, + "bottom", Gravity.BOTTOM)); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java new file mode 100644 index 000000000..6052b644d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.textinput; + +import javax.annotation.Nullable; + +import android.text.Spanned; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import com.facebook.csslayout.CSSNode; +import com.facebook.csslayout.MeasureOutput; +import com.facebook.csslayout.Spacing; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIViewOperationQueue; +import com.facebook.react.uimanager.ViewDefaults; +import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.views.text.ReactTextShadowNode; + +/* package */ class ReactTextInputShadowNode extends ReactTextShadowNode implements + CSSNode.MeasureFunction { + + public static final String PROP_TEXT_INPUT_MOST_RECENT_EVENT_COUNT = "mostRecentEventCount"; + private static final int MEASURE_SPEC = View.MeasureSpec.makeMeasureSpec( + ViewGroup.LayoutParams.WRAP_CONTENT, + View.MeasureSpec.UNSPECIFIED); + + private @Nullable EditText mEditText; + private int mFontSize; + private @Nullable float[] mComputedPadding; + private int mJsEventCount = UNSET; + private int mNumLines = UNSET; + + public ReactTextInputShadowNode() { + super(false); + mFontSize = (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP)); + setMeasureFunction(this); + } + + @Override + protected void setThemedContext(ThemedReactContext themedContext) { + super.setThemedContext(themedContext); + + // TODO #7120264: cache this stuff better + mEditText = new EditText(getThemedContext()); + // This is needed to fix an android bug since 4.4.3 which will throw an NPE in measure, + // setting the layoutParams fixes it: https://code.google.com/p/android/issues/detail?id=75877 + mEditText.setLayoutParams( + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + setDefaultPadding(Spacing.LEFT, mEditText.getPaddingLeft()); + setDefaultPadding(Spacing.TOP, mEditText.getPaddingTop()); + setDefaultPadding(Spacing.RIGHT, mEditText.getPaddingRight()); + setDefaultPadding(Spacing.BOTTOM, mEditText.getPaddingBottom()); + mComputedPadding = spacingToFloatArray(getStylePadding()); + } + + @Override + public void measure(CSSNode node, float width, MeasureOutput measureOutput) { + // measure() should never be called before setThemedContext() + EditText editText = Assertions.assertNotNull(mEditText); + + measureOutput.width = width; + editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mFontSize); + mComputedPadding = spacingToFloatArray(getStylePadding()); + editText.setPadding( + (int) Math.ceil(getStylePadding().get(Spacing.LEFT)), + (int) Math.ceil(getStylePadding().get(Spacing.TOP)), + (int) Math.ceil(getStylePadding().get(Spacing.RIGHT)), + (int) Math.ceil(getStylePadding().get(Spacing.BOTTOM))); + + if (mNumLines != UNSET) { + editText.setLines(mNumLines); + } + + editText.measure(MEASURE_SPEC, MEASURE_SPEC); + measureOutput.height = editText.getMeasuredHeight(); + } + + @Override + public void onBeforeLayout() { + // We don't have to measure the text within the text input. + return; + } + + @Override + public void updateProperties(CatalystStylesDiffMap styles) { + super.updateProperties(styles); + if (styles.hasKey(ViewProps.FONT_SIZE)) { + float fontSize = styles.getFloat(ViewProps.FONT_SIZE, ViewDefaults.FONT_SIZE_SP); + mFontSize = (int) Math.ceil(PixelUtil.toPixelFromSP(fontSize)); + } + + if (styles.hasKey(PROP_TEXT_INPUT_MOST_RECENT_EVENT_COUNT)) { + mJsEventCount = styles.getInt(PROP_TEXT_INPUT_MOST_RECENT_EVENT_COUNT, 0); + } + + if (styles.hasKey(ReactTextInputManager.PROP_TEXT_INPUT_NUMLINES)) { + mNumLines = styles.getInt(ReactTextInputManager.PROP_TEXT_INPUT_NUMLINES, UNSET); + } + } + + @Override + public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { + super.onCollectExtraUpdates(uiViewOperationQueue); + if (mComputedPadding != null) { + uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), mComputedPadding); + mComputedPadding = null; + } + + if (mJsEventCount != UNSET) { + Spanned preparedSpannedText = fromTextCSSNode(this); + ReactTextUpdate reactTextUpdate = new ReactTextUpdate(preparedSpannedText, mJsEventCount); + uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate); + } + } + + @Override + public void setPadding(int spacingType, float padding) { + super.setPadding(spacingType, padding); + mComputedPadding = spacingToFloatArray(getStylePadding()); + markUpdated(); + } + + private static float[] spacingToFloatArray(Spacing spacing) { + return new float[] { + spacing.get(Spacing.LEFT), + spacing.get(Spacing.TOP), + spacing.get(Spacing.RIGHT), + spacing.get(Spacing.BOTTOM), + }; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputSubmitEditingEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputSubmitEditingEvent.java new file mode 100644 index 000000000..0b378dbb8 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputSubmitEditingEvent.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.textinput; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by EditText native view when the user submits the text. + */ +/* package */ class ReactTextInputSubmitEditingEvent + extends Event { + + private static final String EVENT_NAME = "topSubmitEditing"; + + private String mText; + + public ReactTextInputSubmitEditingEvent( + int viewId, + long timestampMs, + String text) { + super(viewId, timestampMs); + mText = text; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + eventData.putString("text", mText); + return eventData; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextUpdate.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextUpdate.java new file mode 100644 index 000000000..fc6c443c4 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextUpdate.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.textinput; + +import android.text.Spanned; + +/** + * Class that contains the data needed for a Text Input text update. + * VisibleForTesting from {@link TextInputEventsTestCase}. + */ +public class ReactTextUpdate { + + private final Spanned mText; + private final int mJsEventCounter; + + public ReactTextUpdate(Spanned text, int jsEventCounter) { + mText = text; + mJsEventCounter = jsEventCounter; + } + + public Spanned getText() { + return mText; + } + + public int getJsEventCounter() { + return mJsEventCounter; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java new file mode 100644 index 000000000..fb96b4478 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java @@ -0,0 +1,234 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.toolbar; + +import javax.annotation.Nullable; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.os.SystemClock; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import com.facebook.react.R; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.uimanager.CSSColorUtil; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.UIProp; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.toolbar.events.ToolbarClickEvent; +import com.facebook.react.uimanager.ViewGroupManager; + +/** + * Manages instances of Toolbar. + */ +public class ReactToolbarManager extends ViewGroupManager { + + private static final String REACT_CLASS = "ToolbarAndroid"; + + @UIProp(UIProp.Type.STRING) + public static final String PROP_LOGO = "logo"; + @UIProp(UIProp.Type.STRING) + public static final String PROP_NAV_ICON = "navIcon"; + @UIProp(UIProp.Type.STRING) + public static final String PROP_SUBTITLE = "subtitle"; + @UIProp(UIProp.Type.STRING) + public static final String PROP_SUBTITLE_COLOR = "subtitleColor"; + @UIProp(UIProp.Type.STRING) + public static final String PROP_TITLE = "title"; + @UIProp(UIProp.Type.STRING) + public static final String PROP_TITLE_COLOR = "titleColor"; + @UIProp(UIProp.Type.ARRAY) + public static final String PROP_ACTIONS = "actions"; + + private static final String PROP_ACTION_ICON = "icon"; + private static final String PROP_ACTION_SHOW = "show"; + private static final String PROP_ACTION_SHOW_WITH_TEXT = "showWithText"; + private static final String PROP_ACTION_TITLE = "title"; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + protected Toolbar createViewInstance(ThemedReactContext reactContext) { + return new Toolbar(reactContext); + } + + @Override + public void updateView(Toolbar toolbar, CatalystStylesDiffMap props) { + super.updateView(toolbar, props); + + int[] defaultColors = getDefaultColors(toolbar.getContext()); + if (props.hasKey(PROP_SUBTITLE)) { + toolbar.setSubtitle(props.getString(PROP_SUBTITLE)); + } + if (props.hasKey(PROP_SUBTITLE_COLOR)) { + String color = props.getString(PROP_SUBTITLE_COLOR); + if (color != null) { + toolbar.setSubtitleTextColor(CSSColorUtil.getColor(color)); + } else { + toolbar.setSubtitleTextColor(defaultColors[1]); + } + } + if (props.hasKey(PROP_TITLE)) { + toolbar.setTitle(props.getString(PROP_TITLE)); + } + if (props.hasKey(PROP_TITLE_COLOR)) { + String color = props.getString(PROP_TITLE_COLOR); + if (color != null) { + toolbar.setTitleTextColor(CSSColorUtil.getColor(color)); + } else { + toolbar.setTitleTextColor(defaultColors[0]); + } + } + if (props.hasKey(PROP_NAV_ICON)) { + String navIcon = props.getString(PROP_NAV_ICON); + if (navIcon != null) { + toolbar.setNavigationIcon(getDrawableResourceByName(toolbar.getContext(), navIcon)); + } else { + toolbar.setNavigationIcon(null); + } + } + if (props.hasKey(PROP_LOGO)) { + String logo = props.getString(PROP_LOGO); + if (logo != null) { + toolbar.setLogo(getDrawableResourceByName(toolbar.getContext(), logo)); + } else { + toolbar.setLogo(null); + } + } + if (props.hasKey(PROP_ACTIONS)) { + setActions(toolbar, props.getArray(PROP_ACTIONS)); + } + } + + @Override + protected void addEventEmitters(final ThemedReactContext reactContext, final Toolbar view) { + final EventDispatcher mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class) + .getEventDispatcher(); + view.setNavigationOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + mEventDispatcher.dispatchEvent( + new ToolbarClickEvent(view.getId(), SystemClock.uptimeMillis(), -1)); + } + }); + + view.setOnMenuItemClickListener( + new Toolbar.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + mEventDispatcher.dispatchEvent( + new ToolbarClickEvent( + view.getId(), + SystemClock.uptimeMillis(), + menuItem.getOrder())); + return true; + } + }); + } + + @Override + public boolean needsCustomLayoutForChildren() { + return true; + } + + private static void setActions(Toolbar toolbar, @Nullable ReadableArray actions) { + Menu menu = toolbar.getMenu(); + menu.clear(); + if (actions != null) { + for (int i = 0; i < actions.size(); i++) { + ReadableMap action = actions.getMap(i); + MenuItem item = menu.add(Menu.NONE, Menu.NONE, i, action.getString(PROP_ACTION_TITLE)); + String icon = action.hasKey(PROP_ACTION_ICON) ? action.getString(PROP_ACTION_ICON) : null; + if (icon != null) { + item.setIcon(getDrawableResourceByName(toolbar.getContext(), icon)); + } + String show = action.hasKey(PROP_ACTION_SHOW) ? action.getString(PROP_ACTION_SHOW) : null; + if (show != null) { + int showAsAction = MenuItem.SHOW_AS_ACTION_NEVER; + if ("always".equals(show)) { + showAsAction = MenuItem.SHOW_AS_ACTION_ALWAYS; + } else if ("ifRoom".equals(show)) { + showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM; + } + if (action.hasKey(PROP_ACTION_SHOW_WITH_TEXT) && + action.getBoolean(PROP_ACTION_SHOW_WITH_TEXT)) { + showAsAction = showAsAction | MenuItem.SHOW_AS_ACTION_WITH_TEXT; + } + item.setShowAsAction(showAsAction); + } + } + } + } + + private static int[] getDefaultColors(Context context) { + Resources.Theme theme = context.getTheme(); + TypedArray toolbarStyle = null; + TypedArray textAppearances = null; + TypedArray titleTextAppearance = null; + TypedArray subtitleTextAppearance = null; + + try { + toolbarStyle = theme + .obtainStyledAttributes(new int[]{R.attr.toolbarStyle}); + int toolbarStyleResId = toolbarStyle.getResourceId(0, 0); + textAppearances = theme.obtainStyledAttributes( + toolbarStyleResId, new int[]{ + R.attr.titleTextAppearance, + R.attr.subtitleTextAppearance, + }); + int titleTextAppearanceResId = textAppearances.getResourceId(0, 0); + int subtitleTextAppearanceResId = textAppearances.getResourceId(1, 0); + + titleTextAppearance = theme + .obtainStyledAttributes(titleTextAppearanceResId, new int[]{android.R.attr.textColor}); + subtitleTextAppearance = theme + .obtainStyledAttributes(subtitleTextAppearanceResId, new int[]{android.R.attr.textColor}); + + int titleTextColor = titleTextAppearance.getColor(0, Color.BLACK); + int subtitleTextColor = subtitleTextAppearance.getColor(0, Color.BLACK); + + return new int[] {titleTextColor, subtitleTextColor}; + } finally { + recycleQuietly(toolbarStyle); + recycleQuietly(textAppearances); + recycleQuietly(titleTextAppearance); + recycleQuietly(subtitleTextAppearance); + } + } + + private static void recycleQuietly(@Nullable TypedArray style) { + if (style != null) { + style.recycle(); + } + } + + private static int getDrawableResourceByName(Context context, String name) { + name = name.toLowerCase().replace("-", "_"); + return context.getResources().getIdentifier( + name, + "drawable", + context.getPackageName()); + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/events/ToolbarClickEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/events/ToolbarClickEvent.java new file mode 100644 index 000000000..4e6bfa0ff --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/events/ToolbarClickEvent.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.facebook.react.views.toolbar.events; + +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Represents a click on the toolbar. + * Position is meaningful when the click happenned on a menu + */ +public class ToolbarClickEvent extends Event { + + private static final String EVENT_NAME = "topSelect"; + private final int position; + + public ToolbarClickEvent(int viewId, long timestampMs, int position) { + super(viewId, timestampMs); + this.position = position; + } + + public int getPosition() { + return position; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + WritableMap event = new WritableNativeMap(); + event.putInt("position", getPosition()); + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), event); + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java new file mode 100644 index 000000000..a8efbd777 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.view; + +import android.graphics.PixelFormat; + +/** + * Simple utility class for manipulating colors, based on Fresco's + * DrawableUtils (https://github.com/facebook/fresco). + * For a small helper like this, copying is simpler than adding + * a dependency on com.facebook.fresco.drawee. + */ +public class ColorUtil { + + /** + * Multiplies the color with the given alpha. + * @param color color to be multiplied + * @param alpha value between 0 and 255 + * @return multiplied color + */ + public static int multiplyColorAlpha(int color, int alpha) { + if (alpha == 255) { + return color; + } + if (alpha == 0) { + return color & 0x00FFFFFF; + } + alpha = alpha + (alpha >> 7); // make it 0..256 + int colorAlpha = color >>> 24; + int multipliedAlpha = colorAlpha * alpha >> 8; + return (multipliedAlpha << 24) | (color & 0x00FFFFFF); + } + + /** + * Gets the opacity from a color. Inspired by Android ColorDrawable. + * @return opacity expressed by one of PixelFormat constants + */ + public static int getOpacityFromColor(int color) { + int colorAlpha = color >>> 24; + if (colorAlpha == 255) { + return PixelFormat.OPAQUE; + } else if (colorAlpha == 0) { + return PixelFormat.TRANSPARENT; + } else { + return PixelFormat.TRANSLUCENT; + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroup.java new file mode 100644 index 000000000..af73bda94 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroup.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.view; + +import android.graphics.Rect; +import android.view.View; + +import com.facebook.react.uimanager.CatalystStylesDiffMap; + +/** + * Interface that should be implemented by {@link View} subclasses that support + * {@code removeClippedSubviews} property. When this property is set for the {@link ViewGroup} + * subclass it's responsible for detaching it's child views that are clipped by the view boundaries. + * Those view boundaries should be determined based on it's parent clipping area and current view's + * offset in parent and doesn't necessarily reflect the view visible area (in a sense of a value + * that {@link View#getGlobalVisibleRect} may return). In order to determine the clipping rect for + * current view helper method {@link ReactClippingViewGroupHelper#calculateClippingRect} can be used + * that takes into account parent view settings. + */ +public interface ReactClippingViewGroup { + + /** + * Notify view that clipping area may have changed and it should recalculate the list of children + * that shold be attached/detached. This method should be called only when property + * {@code removeClippedSubviews} is set to {@code true} on a view. + * + * CAUTION: Views are responsible for calling {@link #updateClippingRect} on it's children. This + * should happen if child implement {@link ReactClippingViewGroup}, return true from + * {@link #getRemoveClippedSubviews} and clipping rect change of the current view may affect + * clipping rect of this child. + */ + void updateClippingRect(); + + /** + * Get rectangular bounds to which view is currently clipped to. Called only on views that has set + * {@code removeCLippedSubviews} property value to {@code true}. + * + * @param outClippingRect output clipping rect should be written to this object. + */ + void getClippingRect(Rect outClippingRect); + + /** + * Sets property {@code removeClippedSubviews} as a result of property update in JS. Should be + * called only from @{link ViewManager#updateView} method. + * + * Helper method {@link ReactClippingViewGroupHelper#applyRemoveClippedSubviewsProperty} may be + * used by {@link ViewManager} subclass to apply this property based on property update map + * {@link CatalystStylesDiffMap}. + */ + void setRemoveClippedSubviews(boolean removeClippedSubviews); + + /** + * Get the current value of {@code removeClippedSubviews} property. + */ + boolean getRemoveClippedSubviews(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroupHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroupHelper.java new file mode 100644 index 000000000..67a2e9d45 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewGroupHelper.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.view; + +import javax.annotation.concurrent.NotThreadSafe; + +import android.graphics.Rect; +import android.view.View; +import android.view.ViewParent; + +import com.facebook.react.uimanager.CatalystStylesDiffMap; + +/** + * Provides implementation of common tasks for view and it's view manager supporting property + * {@code removeClippedSubviews}. + */ +@NotThreadSafe +public class ReactClippingViewGroupHelper { + + public static final String PROP_REMOVE_CLIPPED_SUBVIEWS = "removeClippedSubviews"; + + private static final Rect sHelperRect = new Rect(); + + /** + * Can be used by view that support {@code removeClippedSubviews} property to calculate area that + * given {@param view} should be clipped to based on the clipping rectangle of it's parent in + * case when parent is also set to clip it's children. + * + * @param view view that we want to calculate clipping rect for + * @param outputRect where the calculated rectangle will be written + */ + public static void calculateClippingRect(View view, Rect outputRect) { + ViewParent parent = view.getParent(); + if (parent == null) { + outputRect.setEmpty(); + return; + } else if (parent instanceof ReactClippingViewGroup) { + ReactClippingViewGroup clippingViewGroup = (ReactClippingViewGroup) parent; + if (clippingViewGroup.getRemoveClippedSubviews()) { + clippingViewGroup.getClippingRect(sHelperRect); + sHelperRect.offset(-view.getLeft(), -view.getTop()); + view.getDrawingRect(outputRect); + if (!outputRect.intersect(sHelperRect)) { + // rectangles does not intersect -> we should write empty rect to output + outputRect.setEmpty(); + } + return; + } + } + view.getDrawingRect(outputRect); + } + + /** + * Can be used by view's manager in {@link ViewManager#updateView} method to update property + * {@code removeClippedSubviews} in the view. + * + * @param view view instance passed to {@link ViewManager#updateView} + * @param props property map passed to {@link ViewManager#updateView} + */ + public static void applyRemoveClippedSubviewsProperty( + ReactClippingViewGroup view, + CatalystStylesDiffMap props) { + if (props.hasKey(PROP_REMOVE_CLIPPED_SUBVIEWS)) { + view.setRemoveClippedSubviews(props.getBoolean(PROP_REMOVE_CLIPPED_SUBVIEWS, false)); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java new file mode 100644 index 000000000..503d01a6c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.view; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.os.Build; +import android.util.TypedValue; + +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.SoftAssertions; +import com.facebook.react.uimanager.CSSColorUtil; + +/** + * Utility class that helps with converting android drawable description used in JS to an actual + * instance of {@link Drawable}. + */ +/* package */ class ReactDrawableHelper { + + private static final TypedValue sResolveOutValue = new TypedValue(); + + public static Drawable createDrawableFromJSDescription( + Context context, + ReadableMap drawableDescriptionDict) { + String type = drawableDescriptionDict.getString("type"); + if ("ThemeAttrAndroid".equals(type)) { + String attr = drawableDescriptionDict.getString("attribute"); + SoftAssertions.assertNotNull(attr); + int attrID = context.getResources().getIdentifier(attr, "attr", "android"); + if (attrID == 0) { + throw new JSApplicationIllegalArgumentException("Attribute " + attr + + " couldn't be found in the resource list"); + } + if (context.getTheme().resolveAttribute(attrID, sResolveOutValue, true)) { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return context.getResources() + .getDrawable(sResolveOutValue.resourceId, context.getTheme()); + } else { + return context.getResources().getDrawable(sResolveOutValue.resourceId); + } + } else { + throw new JSApplicationIllegalArgumentException("Attribute " + attr + + " couldn't be resolved into a drawable"); + } + } else if ("RippleAndroid".equals(type)) { + if (Build.VERSION.SDK_INT < 21) { + throw new JSApplicationIllegalArgumentException("Ripple drawable is not available on " + + "android API <21"); + } + String colorName = drawableDescriptionDict.hasKey("color") ? + drawableDescriptionDict.getString("color") : null; + int color; + if (colorName != null) { + color = CSSColorUtil.getColor(colorName); + } else { + if (context.getTheme().resolveAttribute( + android.R.attr.colorControlHighlight, + sResolveOutValue, + true)) { + color = context.getResources().getColor(sResolveOutValue.resourceId); + } else { + throw new JSApplicationIllegalArgumentException("Attribute colorControlHighlight " + + "couldn't be resolved into a drawable"); + } + } + Drawable mask = null; + if (!drawableDescriptionDict.hasKey("borderless") || + drawableDescriptionDict.isNull("borderless") || + !drawableDescriptionDict.getBoolean("borderless")) { + mask = new ColorDrawable(Color.WHITE); + } + ColorStateList colorStateList = new ColorStateList( + new int[][] {new int[]{}}, + new int[] {color}); + return new RippleDrawable(colorStateList, null, mask); + } else { + throw new JSApplicationIllegalArgumentException( + "Invalid type for android drawable: " + type); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java new file mode 100644 index 000000000..4360c33fc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java @@ -0,0 +1,301 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.view; + +import javax.annotation.Nullable; + +import java.util.Locale; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathEffect; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.csslayout.CSSConstants; +import com.facebook.csslayout.FloatUtil; +import com.facebook.csslayout.Spacing; + +/** + * A subclass of {@link Drawable} used for background of {@link ReactViewGroup}. It supports + * drawing background color and borders (including rounded borders) by providing a react friendly + * API (setter for each of those properties). + * + * The implementation tries to allocate as few objects as possible depending on which properties are + * set. E.g. for views with rounded background/borders we allocate {@code mPathForBorderRadius} and + * {@code mTempRectForBorderRadius}. In case when view have a rectangular borders we allocate + * {@code mBorderWidthResult} and similar. When only background color is set we won't allocate any + * extra/unnecessary objects. + */ +/* package */ class ReactViewBackgroundDrawable extends Drawable { + + private static final int DEFAULT_BORDER_COLOR = Color.BLACK; + + private static enum BorderStyle { + SOLID, + DASHED, + DOTTED; + + public @Nullable PathEffect getPathEffect(float borderWidth) { + switch (this) { + case SOLID: + return null; + + case DASHED: + return new DashPathEffect( + new float[] {borderWidth*3, borderWidth*3, borderWidth*3, borderWidth*3}, 0); + + case DOTTED: + return new DashPathEffect( + new float[] {borderWidth, borderWidth, borderWidth, borderWidth}, 0); + + default: + return null; + } + } + }; + + /* Value at Spacing.ALL index used for rounded borders, whole array used by rectangular borders */ + private @Nullable Spacing mBorderWidth; + private @Nullable Spacing mBorderColor; + private @Nullable BorderStyle mBorderStyle; + + /* Used for rounded border and rounded background */ + private @Nullable PathEffect mPathEffectForBorderStyle; + private @Nullable Path mPathForBorderRadius; + private @Nullable RectF mTempRectForBorderRadius; + private boolean mNeedUpdatePathForBorderRadius = false; + private float mBorderRadius = CSSConstants.UNDEFINED; + + /* Used by all types of background and for drawing borders */ + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private int mColor = Color.TRANSPARENT; + private int mAlpha = 255; + + @Override + public void draw(Canvas canvas) { + if (!CSSConstants.isUndefined(mBorderRadius) && mBorderRadius > 0) { + drawRoundedBackgroundWithBorders(canvas); + } else { + drawRectangularBackgroundWithBorders(canvas); + } + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mNeedUpdatePathForBorderRadius = true; + } + + @Override + public void setAlpha(int alpha) { + if (alpha != mAlpha) { + mAlpha = alpha; + invalidateSelf(); + } + } + + @Override + public int getAlpha() { + return mAlpha; + } + + @Override + public void setColorFilter(ColorFilter cf) { + // do nothing + } + + @Override + public int getOpacity() { + return ColorUtil.getOpacityFromColor(ColorUtil.multiplyColorAlpha(mColor, mAlpha)); + } + + public void setBorderWidth(int position, float width) { + if (mBorderWidth == null) { + mBorderWidth = new Spacing(); + } + if (!FloatUtil.floatsEqual(mBorderWidth.getRaw(position), width)) { + mBorderWidth.set(position, width); + if (position == Spacing.ALL) { + mNeedUpdatePathForBorderRadius = true; + } + invalidateSelf(); + } + } + + public void setBorderColor(int position, float color) { + if (mBorderColor == null) { + mBorderColor = new Spacing(); + mBorderColor.setDefault(Spacing.LEFT, DEFAULT_BORDER_COLOR); + mBorderColor.setDefault(Spacing.TOP, DEFAULT_BORDER_COLOR); + mBorderColor.setDefault(Spacing.RIGHT, DEFAULT_BORDER_COLOR); + mBorderColor.setDefault(Spacing.BOTTOM, DEFAULT_BORDER_COLOR); + } + if (!FloatUtil.floatsEqual(mBorderColor.getRaw(position), color)) { + mBorderColor.set(position, color); + invalidateSelf(); + } + } + + public void setBorderStyle(@Nullable String style) { + BorderStyle borderStyle = style == null + ? null + : BorderStyle.valueOf(style.toUpperCase(Locale.US)); + if (mBorderStyle != borderStyle) { + mBorderStyle = borderStyle; + mNeedUpdatePathForBorderRadius = true; + invalidateSelf(); + } + } + + public void setRadius(float radius) { + if (mBorderRadius != radius) { + mBorderRadius = radius; + invalidateSelf(); + } + } + + public void setColor(int color) { + mColor = color; + invalidateSelf(); + } + + @VisibleForTesting + public int getColor() { + return mColor; + } + + private void drawRoundedBackgroundWithBorders(Canvas canvas) { + updatePath(); + int useColor = ColorUtil.multiplyColorAlpha(mColor, mAlpha); + if ((useColor >>> 24) != 0) { // color is not transparent + mPaint.setColor(useColor); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawPath(mPathForBorderRadius, mPaint); + } + // maybe draw borders? + float fullBorderWidth = getFullBorderWidth(); + if (fullBorderWidth > 0) { + int borderColor = getFullBorderColor(); + mPaint.setColor(ColorUtil.multiplyColorAlpha(borderColor, mAlpha)); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(fullBorderWidth); + mPaint.setPathEffect(mPathEffectForBorderStyle); + canvas.drawPath(mPathForBorderRadius, mPaint); + } + } + + private void updatePath() { + if (!mNeedUpdatePathForBorderRadius) { + return; + } + mNeedUpdatePathForBorderRadius = false; + if (mPathForBorderRadius == null) { + mPathForBorderRadius = new Path(); + mTempRectForBorderRadius = new RectF(); + } + mPathForBorderRadius.reset(); + mTempRectForBorderRadius.set(getBounds()); + float fullBorderWidth = getFullBorderWidth(); + if (fullBorderWidth > 0) { + mTempRectForBorderRadius.inset(fullBorderWidth * 0.5f, fullBorderWidth * 0.5f); + } + mPathForBorderRadius.addRoundRect( + mTempRectForBorderRadius, + mBorderRadius, + mBorderRadius, + Path.Direction.CW); + + mPathEffectForBorderStyle = mBorderStyle != null + ? mBorderStyle.getPathEffect(getFullBorderWidth()) + : null; + } + + /** + * For rounded borders we use default "borderWidth" property. + */ + private float getFullBorderWidth() { + return (mBorderWidth != null && !CSSConstants.isUndefined(mBorderWidth.getRaw(Spacing.ALL))) ? + mBorderWidth.getRaw(Spacing.ALL) : 0f; + } + + /** + * We use this method for getting color for rounded borders only similarly as for + * {@link #getFullBorderWidth}. + */ + private int getFullBorderColor() { + return (mBorderColor != null && !CSSConstants.isUndefined(mBorderColor.getRaw(Spacing.ALL))) ? + (int) mBorderColor.getRaw(Spacing.ALL) : DEFAULT_BORDER_COLOR; + } + + private void drawRectangularBackgroundWithBorders(Canvas canvas) { + int useColor = ColorUtil.multiplyColorAlpha(mColor, mAlpha); + if ((useColor >>> 24) != 0) { // color is not transparent + mPaint.setColor(useColor); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawRect(getBounds(), mPaint); + } + // maybe draw borders? + if (getBorderWidth(Spacing.LEFT) > 0 || getBorderWidth(Spacing.TOP) > 0 || + getBorderWidth(Spacing.RIGHT) > 0 || getBorderWidth(Spacing.BOTTOM) > 0) { + + int borderLeft = getBorderWidth(Spacing.LEFT); + int borderTop = getBorderWidth(Spacing.TOP); + int borderRight = getBorderWidth(Spacing.RIGHT); + int borderBottom = getBorderWidth(Spacing.BOTTOM); + int colorLeft = getBorderColor(Spacing.LEFT); + int colorTop = getBorderColor(Spacing.TOP); + int colorRight = getBorderColor(Spacing.RIGHT); + int colorBottom = getBorderColor(Spacing.BOTTOM); + + int width = getBounds().width(); + int height = getBounds().height(); + + if (borderLeft > 0 && colorLeft != Color.TRANSPARENT) { + mPaint.setColor(colorLeft); + canvas.drawRect(0, borderTop, borderLeft, height - borderBottom, mPaint); + } + + if (borderTop > 0 && colorTop != Color.TRANSPARENT) { + mPaint.setColor(colorTop); + canvas.drawRect(0, 0, width, borderTop, mPaint); + } + + if (borderRight > 0 && colorRight != Color.TRANSPARENT) { + mPaint.setColor(colorRight); + canvas.drawRect( + width - borderRight, + borderTop, + width, + height - borderBottom, + mPaint); + } + + if (borderBottom > 0 && colorBottom != Color.TRANSPARENT) { + mPaint.setColor(colorBottom); + canvas.drawRect(0, height - borderBottom, width, height, mPaint); + } + } + } + + private int getBorderWidth(int position) { + return mBorderWidth != null ? Math.round(mBorderWidth.get(position)) : 0; + } + + private int getBorderColor(int position) { + return mBorderColor != null ? (int) mBorderColor.get(position) : DEFAULT_BORDER_COLOR; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java new file mode 100644 index 000000000..2af6bb254 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -0,0 +1,487 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.view; + +import javax.annotation.Nullable; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.touch.CatalystInterceptingViewGroup; +import com.facebook.react.touch.OnInterceptTouchEventListener; +import com.facebook.react.uimanager.MeasureSpecAssertions; +import com.facebook.react.uimanager.PointerEvents; +import com.facebook.react.uimanager.ReactPointerEventsView; + +/** + * Backing for a React View. Has support for borders, but since borders aren't common, lazy + * initializes most of the storage needed for them. + */ +public class ReactViewGroup extends ViewGroup implements + CatalystInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView { + + private static final int ARRAY_CAPACITY_INCREMENT = 12; + private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT; + private static final LayoutParams sDefaultLayoutParam = new ViewGroup.LayoutParams(0, 0); + /* should only be used in {@link #updateClippingToRect} */ + private static final Rect sHelperRect = new Rect(); + + /** + * This listener will be set for child views when removeClippedSubview property is enabled. When + * children layout is updated, it will call {@link #updateSubviewClipStatus} to notify parent + * view about that fact so that view can be attached/detached if necessary. + * + * TODO(7728005): Attach/detach views in batch - once per frame in case when multiple children + * update their layout. + */ + private static final class ChildrenLayoutChangeListener implements OnLayoutChangeListener { + + private final ReactViewGroup mParent; + + private ChildrenLayoutChangeListener(ReactViewGroup parent) { + mParent = parent; + } + + @Override + public void onLayoutChange( + View v, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + if (mParent.getRemoveClippedSubviews()) { + mParent.updateSubviewClipStatus(v); + } + } + } + + // Following properties are here to support the option {@code removeClippedSubviews}. This is a + // temporary optimization/hack that is mainly applicable to the large list of images. The way + // it's implemented is that we store an additional array of children in view node. We selectively + // remove some of the views (detach) from it while still storing them in that additional array. + // We override all possible add methods for {@link ViewGroup} so that we can controll this process + // whenever the option is set. We also override {@link ViewGroup#getChildAt} and + // {@link ViewGroup#getChildCount} so those methods may return views that are not attached. + // This is risky but allows us to perform a correct cleanup in {@link NativeViewHierarchyManager}. + private boolean mRemoveClippedSubviews = false; + private @Nullable View[] mAllChildren = null; + private int mAllChildrenCount; + private @Nullable Rect mClippingRect; + private PointerEvents mPointerEvents = PointerEvents.AUTO; + private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener; + private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable; + private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener; + private boolean mNeedsOffscreenAlphaCompositing = false; + + public ReactViewGroup(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec); + + setMeasuredDimension( + MeasureSpec.getSize(widthMeasureSpec), + MeasureSpec.getSize(heightMeasureSpec)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + // No-op since UIManagerModule handles actually laying out children. + } + + @Override + public void setBackgroundColor(int color) { + if (color == Color.TRANSPARENT) { + Drawable backgroundDrawble = getBackground(); + if (mReactBackgroundDrawable != null && (backgroundDrawble instanceof LayerDrawable)) { + // extract translucent background portion from layerdrawable + super.setBackground(null); + LayerDrawable layerDrawable = (LayerDrawable) backgroundDrawble; + super.setBackground(layerDrawable.getDrawable(1)); + } else if (backgroundDrawble instanceof ReactViewBackgroundDrawable) { + // mReactBackground is set for background + mReactBackgroundDrawable = null; + super.setBackground(null); + } + } else { + getOrCreateReactViewBackground().setColor(color); + } + } + + @Override + public void setBackground(Drawable drawable) { + throw new UnsupportedOperationException( + "This method is not supported for ReactViewGroup instances"); + } + + public void setTranslucentBackgroundDrawable(@Nullable Drawable background) { + // it's required to call setBackground to null, as in some of the cases we may set new + // background to be a layer drawable that contains a drawable that has been previously setup + // as a background previously. This will not work correctly as the drawable callback logic is + // messed up in AOSP + super.setBackground(null); + if (mReactBackgroundDrawable != null && background != null) { + LayerDrawable layerDrawable = + new LayerDrawable(new Drawable[] {mReactBackgroundDrawable, background}); + super.setBackground(layerDrawable); + } else if (background != null) { + super.setBackground(background); + } + } + + @Override + public void setOnInterceptTouchEventListener(OnInterceptTouchEventListener listener) { + mOnInterceptTouchEventListener = listener; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mOnInterceptTouchEventListener != null && + mOnInterceptTouchEventListener.onInterceptTouchEvent(this, ev)) { + return true; + } + // We intercept the touch event if the children are not supposed to receive it. + if (mPointerEvents == PointerEvents.NONE || mPointerEvents == PointerEvents.BOX_ONLY) { + return true; + } + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + // We do not accept the touch event if this view is not supposed to receive it. + if (mPointerEvents == PointerEvents.NONE || mPointerEvents == PointerEvents.BOX_NONE) { + return false; + } + // The root view always assumes any view that was tapped wants the touch + // and sends the event to JS as such. + // We don't need to do bubbling in native (it's already happening in JS). + // For an explanation of bubbling and capturing, see + // http://javascript.info/tutorial/bubbling-and-capturing#capturing + return true; + } + + /** + * We override this to allow developers to determine whether they need offscreen alpha compositing + * or not. See the documentation of needsOffscreenAlphaCompositing in View.js. + */ + @Override + public boolean hasOverlappingRendering() { + return mNeedsOffscreenAlphaCompositing; + } + + /** + * See the documentation of needsOffscreenAlphaCompositing in View.js. + */ + public void setNeedsOffscreenAlphaCompositing(boolean needsOffscreenAlphaCompositing) { + mNeedsOffscreenAlphaCompositing = needsOffscreenAlphaCompositing; + } + + public void setBorderWidth(int position, float width) { + getOrCreateReactViewBackground().setBorderWidth(position, width); + } + + public void setBorderColor(int position, float color) { + getOrCreateReactViewBackground().setBorderColor(position, color); + } + + public void setBorderRadius(float borderRadius) { + getOrCreateReactViewBackground().setRadius(borderRadius); + } + + public void setBorderStyle(@Nullable String style) { + getOrCreateReactViewBackground().setBorderStyle(style); + } + + @Override + public void setRemoveClippedSubviews(boolean removeClippedSubviews) { + if (removeClippedSubviews == mRemoveClippedSubviews) { + return; + } + mRemoveClippedSubviews = removeClippedSubviews; + if (removeClippedSubviews) { + mClippingRect = new Rect(); + ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); + mAllChildrenCount = getChildCount(); + int initialSize = Math.max(12, mAllChildrenCount); + mAllChildren = new View[initialSize]; + mChildrenLayoutChangeListener = new ChildrenLayoutChangeListener(this); + for (int i = 0; i < mAllChildrenCount; i++) { + View child = getChildAt(i); + mAllChildren[i] = child; + child.addOnLayoutChangeListener(mChildrenLayoutChangeListener); + } + updateClippingRect(); + } else { + // Add all clipped views back, deallocate additional arrays, remove layoutChangeListener + Assertions.assertNotNull(mClippingRect); + Assertions.assertNotNull(mAllChildren); + Assertions.assertNotNull(mChildrenLayoutChangeListener); + for (int i = 0; i < mAllChildrenCount; i++) { + mAllChildren[i].removeOnLayoutChangeListener(mChildrenLayoutChangeListener); + } + getDrawingRect(mClippingRect); + updateClippingToRect(mClippingRect); + mAllChildren = null; + mClippingRect = null; + mAllChildrenCount = 0; + mChildrenLayoutChangeListener = null; + } + } + + @Override + public boolean getRemoveClippedSubviews() { + return mRemoveClippedSubviews; + } + + @Override + public void getClippingRect(Rect outClippingRect) { + outClippingRect.set(mClippingRect); + } + + @Override + public void updateClippingRect() { + if (!mRemoveClippedSubviews) { + return; + } + + Assertions.assertNotNull(mClippingRect); + Assertions.assertNotNull(mAllChildren); + + ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); + updateClippingToRect(mClippingRect); + } + + private void updateClippingToRect(Rect clippingRect) { + Assertions.assertNotNull(mAllChildren); + int clippedSoFar = 0; + for (int i = 0; i < mAllChildrenCount; i++) { + updateSubviewClipStatus(clippingRect, i, clippedSoFar); + if (mAllChildren[i].getParent() == null) { + clippedSoFar++; + } + } + } + + private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFar) { + View child = Assertions.assertNotNull(mAllChildren)[idx]; + sHelperRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); + boolean intersects = clippingRect + .intersects(sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom); + boolean needUpdateClippingRecursive = false; + if (!intersects && child.getParent() != null) { + // We can try saving on invalidate call here as the view that we remove is out of visible area + // therefore invalidation is not necessary. + super.removeViewsInLayout(idx - clippedSoFar, 1); + needUpdateClippingRecursive = true; + } else if (intersects && child.getParent() == null) { + super.addViewInLayout(child, idx - clippedSoFar, sDefaultLayoutParam, true); + invalidate(); + needUpdateClippingRecursive = true; + } else if (intersects && !clippingRect.contains(sHelperRect)) { + // View is partially clipped. + needUpdateClippingRecursive = true; + } + if (needUpdateClippingRecursive) { + if (child instanceof ReactClippingViewGroup) { + // we don't use {@link sHelperRect} until the end of this loop, therefore it's safe + // to call this method that may write to the same {@link sHelperRect} object. + ReactClippingViewGroup clippingChild = (ReactClippingViewGroup) child; + if (clippingChild.getRemoveClippedSubviews()) { + clippingChild.updateClippingRect(); + } + } + } + } + + private void updateSubviewClipStatus(View subview) { + if (!mRemoveClippedSubviews || getParent() == null) { + return; + } + + Assertions.assertNotNull(mClippingRect); + Assertions.assertNotNull(mAllChildren); + + // do fast check whether intersect state changed + sHelperRect.set(subview.getLeft(), subview.getTop(), subview.getRight(), subview.getBottom()); + boolean intersects = mClippingRect + .intersects(sHelperRect.left, sHelperRect.top, sHelperRect.right, sHelperRect.bottom); + + // If it was intersecting before, should be attached to the parent + boolean oldIntersects = (subview.getParent() != null); + + if (intersects != oldIntersects) { + int clippedSoFar = 0; + for (int i = 0; i < mAllChildrenCount; i++) { + if (mAllChildren[i] == subview) { + updateSubviewClipStatus(mClippingRect, i, clippedSoFar); + break; + } + if (mAllChildren[i].getParent() == null) { + clippedSoFar++; + } + } + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + updateClippingRect(); + } + + @Override + public PointerEvents getPointerEvents() { + return mPointerEvents; + } + + /*package*/ void setPointerEvents(PointerEvents pointerEvents) { + mPointerEvents = pointerEvents; + } + + /*package*/ int getAllChildrenCount() { + return mAllChildrenCount; + } + + /*package*/ View getChildAtWithSubviewClippingEnabled(int index) { + return Assertions.assertNotNull(mAllChildren)[index]; + } + + /*package*/ void addViewWithSubviewClippingEnabled(View child, int index) { + addViewWithSubviewClippingEnabled(child, index, sDefaultLayoutParam); + } + + /*package*/ void addViewWithSubviewClippingEnabled(View child, int index, LayoutParams params) { + Assertions.assertCondition(mRemoveClippedSubviews); + Assertions.assertNotNull(mClippingRect); + Assertions.assertNotNull(mAllChildren); + addInArray(child, index); + // we add view as "clipped" and then run {@link #updateSubviewClipStatus} to conditionally + // attach it + int clippedSoFar = 0; + for (int i = 0; i < index; i++) { + if (mAllChildren[i].getParent() == null) { + clippedSoFar++; + } + } + updateSubviewClipStatus(mClippingRect, index, clippedSoFar); + child.addOnLayoutChangeListener(mChildrenLayoutChangeListener); + } + + /*package*/ void removeViewWithSubviewClippingEnabled(View view) { + Assertions.assertCondition(mRemoveClippedSubviews); + Assertions.assertNotNull(mClippingRect); + Assertions.assertNotNull(mAllChildren); + view.removeOnLayoutChangeListener(mChildrenLayoutChangeListener); + int index = indexOfChildInAllChildren(view); + if (mAllChildren[index].getParent() != null) { + int clippedSoFar = 0; + for (int i = 0; i < index; i++) { + if (mAllChildren[i].getParent() == null) { + clippedSoFar++; + } + } + super.removeViewsInLayout(index - clippedSoFar, 1); + } + removeFromArray(index); + } + + private int indexOfChildInAllChildren(View child) { + final int count = mAllChildrenCount; + final View[] children = Assertions.assertNotNull(mAllChildren); + for (int i = 0; i < count; i++) { + if (children[i] == child) { + return i; + } + } + return -1; + } + + private void addInArray(View child, int index) { + View[] children = Assertions.assertNotNull(mAllChildren); + final int count = mAllChildrenCount; + final int size = children.length; + if (index == count) { + if (size == count) { + mAllChildren = new View[size + ARRAY_CAPACITY_INCREMENT]; + System.arraycopy(children, 0, mAllChildren, 0, size); + children = mAllChildren; + } + children[mAllChildrenCount++] = child; + } else if (index < count) { + if (size == count) { + mAllChildren = new View[size + ARRAY_CAPACITY_INCREMENT]; + System.arraycopy(children, 0, mAllChildren, 0, index); + System.arraycopy(children, index, mAllChildren, index + 1, count - index); + children = mAllChildren; + } else { + System.arraycopy(children, index, children, index + 1, count - index); + } + children[index] = child; + mAllChildrenCount++; + } else { + throw new IndexOutOfBoundsException("index=" + index + " count=" + count); + } + } + + // This method also sets the child's mParent to null + private void removeFromArray(int index) { + final View[] children = Assertions.assertNotNull(mAllChildren); + final int count = mAllChildrenCount; + if (index == count - 1) { + children[--mAllChildrenCount] = null; + } else if (index >= 0 && index < count) { + System.arraycopy(children, index + 1, children, index, count - index - 1); + children[--mAllChildrenCount] = null; + } else { + throw new IndexOutOfBoundsException(); + } + } + + @VisibleForTesting + public int getBackgroundColor() { + if (getBackground() != null) { + return ((ReactViewBackgroundDrawable) getBackground()).getColor(); + } + return DEFAULT_BACKGROUND_COLOR; + } + + private ReactViewBackgroundDrawable getOrCreateReactViewBackground() { + if (mReactBackgroundDrawable == null) { + mReactBackgroundDrawable = new ReactViewBackgroundDrawable(); + Drawable backgroundDrawable = getBackground(); + super.setBackground(null); // required so that drawable callback is cleared before we add the + // drawable back as a part of LayerDrawable + if (backgroundDrawable == null) { + super.setBackground(mReactBackgroundDrawable); + } else { + LayerDrawable layerDrawable = + new LayerDrawable(new Drawable[] {mReactBackgroundDrawable, backgroundDrawable}); + super.setBackground(layerDrawable); + } + } + return mReactBackgroundDrawable; + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java new file mode 100644 index 000000000..a3decbb6e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.view; + +import javax.annotation.Nullable; + +import java.util.Locale; +import java.util.Map; + +import android.os.Build; +import android.view.View; + +import com.facebook.csslayout.CSSConstants; +import com.facebook.csslayout.Spacing; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.BaseViewPropertyApplicator; +import com.facebook.react.uimanager.CSSColorUtil; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.PointerEvents; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIProp; +import com.facebook.react.uimanager.ViewGroupManager; +import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.common.annotations.VisibleForTesting; + +/** + * View manager for AndroidViews (plain React Views). + */ +public class ReactViewManager extends ViewGroupManager { + + @VisibleForTesting + public static final String REACT_CLASS = ViewProps.VIEW_CLASS_NAME; + + private static final int[] SPACING_TYPES = { + Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM, + }; + private static final String[] PROPS_BORDER_COLOR = { + "borderColor", "borderLeftColor", "borderRightColor", "borderTopColor", "borderBottomColor" + }; + private static final int CMD_HOTSPOT_UPDATE = 1; + private static final int CMD_SET_PRESSED = 2; + private static final int[] sLocationBuf = new int[2]; + + @UIProp(UIProp.Type.STRING) public static final String PROP_ACCESSIBLE = "accessible"; + @UIProp(UIProp.Type.NUMBER) public static final String PROP_BORDER_RADIUS = "borderRadius"; + @UIProp(UIProp.Type.STRING) public static final String PROP_BORDER_STYLE = "borderStyle"; + @UIProp(UIProp.Type.STRING) public static final String PROP_POINTER_EVENTS = "pointerEvents"; + @UIProp(UIProp.Type.MAP) public static final String PROP_NATIVE_BG = "nativeBackgroundAndroid"; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + public ReactViewGroup createViewInstance(ThemedReactContext context) { + return new ReactViewGroup(context); + } + + @Override + public Map getNativeProps() { + Map nativeProps = super.getNativeProps(); + Map baseProps = BaseViewPropertyApplicator.getCommonProps(); + for (Map.Entry entry : baseProps.entrySet()) { + nativeProps.put(entry.getKey(), entry.getValue()); + } + for (int i = 0; i < SPACING_TYPES.length; i++) { + nativeProps.put(ViewProps.BORDER_WIDTHS[i], UIProp.Type.NUMBER); + nativeProps.put(PROPS_BORDER_COLOR[i], UIProp.Type.STRING); + } + return nativeProps; + } + + @Override + public void updateView(ReactViewGroup view, CatalystStylesDiffMap props) { + super.updateView(view, props); + ReactClippingViewGroupHelper.applyRemoveClippedSubviewsProperty(view, props); + + // Border widths + for (int i = 0; i < SPACING_TYPES.length; i++) { + String key = ViewProps.BORDER_WIDTHS[i]; + if (props.hasKey(key)) { + float width = props.getFloat(key, CSSConstants.UNDEFINED); + if (!CSSConstants.isUndefined(width)) { + width = PixelUtil.toPixelFromDIP(width); + } + view.setBorderWidth(SPACING_TYPES[i], width); + } + } + + // Border colors + for (int i = 0; i < SPACING_TYPES.length; i++) { + String key = PROPS_BORDER_COLOR[i]; + if (props.hasKey(key)) { + String color = props.getString(key); + float colorFloat = color == null ? CSSConstants.UNDEFINED : CSSColorUtil.getColor(color); + view.setBorderColor(SPACING_TYPES[i], colorFloat); + } + } + + // Border radius + if (props.hasKey(PROP_BORDER_RADIUS)) { + view.setBorderRadius(PixelUtil.toPixelFromDIP(props.getFloat(PROP_BORDER_RADIUS, 0.0f))); + } + + if (props.hasKey(PROP_BORDER_STYLE)) { + view.setBorderStyle(props.getString(PROP_BORDER_STYLE)); + } + + if (props.hasKey(PROP_POINTER_EVENTS)) { + String pointerEventsStr = props.getString(PROP_POINTER_EVENTS); + if (pointerEventsStr != null) { + PointerEvents pointerEvents = + PointerEvents.valueOf(pointerEventsStr.toUpperCase(Locale.US).replace("-", "_")); + view.setPointerEvents(pointerEvents); + } + } + + // Native background + if (props.hasKey(PROP_NATIVE_BG)) { + ReadableMap map = props.getMap(PROP_NATIVE_BG); + view.setTranslucentBackgroundDrawable(map == null ? + null : ReactDrawableHelper.createDrawableFromJSDescription(view.getContext(), map)); + } + + if (props.hasKey(PROP_ACCESSIBLE)) { + view.setFocusable(props.getBoolean(PROP_ACCESSIBLE, false)); + } + + if (props.hasKey(ViewProps.NEEDS_OFFSCREEN_ALPHA_COMPOSITING)) { + view.setNeedsOffscreenAlphaCompositing( + props.getBoolean(ViewProps.NEEDS_OFFSCREEN_ALPHA_COMPOSITING, false)); + } + } + + @Override + public Map getCommandsMap() { + return MapBuilder.of("hotspotUpdate", CMD_HOTSPOT_UPDATE, "setPressed", CMD_SET_PRESSED); + } + + @Override + public void receiveCommand(ReactViewGroup root, int commandId, @Nullable ReadableArray args) { + switch (commandId) { + case CMD_HOTSPOT_UPDATE: { + if (args == null || args.size() != 2) { + throw new JSApplicationIllegalArgumentException( + "Illegal number of arguments for 'updateHotspot' command"); + } + if (Build.VERSION.SDK_INT >= 21) { + root.getLocationOnScreen(sLocationBuf); + float x = PixelUtil.toPixelFromDIP(args.getDouble(0)) - sLocationBuf[0]; + float y = PixelUtil.toPixelFromDIP(args.getDouble(1)) - sLocationBuf[1]; + root.drawableHotspotChanged(x, y); + } + break; + } + case CMD_SET_PRESSED: { + if (args == null || args.size() != 1) { + throw new JSApplicationIllegalArgumentException( + "Illegal number of arguments for 'setPressed' command"); + } + root.setPressed(args.getBoolean(0)); + break; + } + } + } + + @Override + public void addView(ReactViewGroup parent, View child, int index) { + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + parent.addViewWithSubviewClippingEnabled(child, index); + } else { + parent.addView(child, index); + } + } + + @Override + public int getChildCount(ReactViewGroup parent) { + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + return parent.getAllChildrenCount(); + } else { + return parent.getChildCount(); + } + } + + @Override + public View getChildAt(ReactViewGroup parent, int index) { + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + return parent.getChildAtWithSubviewClippingEnabled(index); + } else { + return parent.getChildAt(index); + } + } + + @Override + public void removeView(ReactViewGroup parent, View child) { + boolean removeClippedSubviews = parent.getRemoveClippedSubviews(); + if (removeClippedSubviews) { + if (child.getParent() != null) { + parent.removeView(child); + } + parent.removeViewWithSubviewClippingEnabled(child); + } else { + parent.removeView(child); + } + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/ApkSoSource.java b/ReactAndroid/src/main/java/com/facebook/soloader/ApkSoSource.java new file mode 100644 index 000000000..eba1eeaf5 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/ApkSoSource.java @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.soloader; + +import java.io.File; +import java.io.IOException; +import android.content.Context; + +import java.util.jar.JarFile; +import java.util.jar.JarEntry; + +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +import android.os.Build; +import android.system.Os; +import android.system.ErrnoException; + +import java.util.HashMap; +import java.util.Map; +import java.util.Enumeration; + +import java.io.InputStream; +import java.io.FileOutputStream; + +import android.util.Log; + +/** + * {@link SoSource} that extracts libraries from an APK to the filesystem. + */ +public class ApkSoSource extends DirectorySoSource { + + private static final String TAG = SoLoader.TAG; + private static final boolean DEBUG = SoLoader.DEBUG; + + /** + * Make a new ApkSoSource that extracts DSOs from our APK instead of relying on the system to do + * the extraction for us. + * + * @param context Application context + */ + public ApkSoSource(Context context) throws IOException { + // + // Initialize a normal DirectorySoSource that will load from our extraction directory. At this + // point, the directory may be empty or contain obsolete libraries, but that's okay. + // + + super(SysUtil.createLibsDirectory(context), DirectorySoSource.RESOLVE_DEPENDENCIES); + + // + // Synchronize the contents of that directory with the library payload in our APK, deleting and + // extracting as needed. + // + + try (JarFile apk = new JarFile(context.getApplicationInfo().publicSourceDir)) { + File libsDir = super.soDirectory; + + if (DEBUG) { + Log.v(TAG, "synchronizing log directory: " + libsDir); + } + + Map providedLibraries = findProvidedLibraries(apk); + try (FileLocker lock = SysUtil.lockLibsDirectory(context)) { + // Delete files in libsDir that we don't provide or that are out of date. Forget about any + // libraries that are up-to-date already so we don't unpack them below. + File extantFiles[] = libsDir.listFiles(); + for (int i = 0; i < extantFiles.length; ++i) { + File extantFile = extantFiles[i]; + + if (DEBUG) { + Log.v(TAG, "considering libdir file: " + extantFile); + } + + String name = extantFile.getName(); + SoInfo so = providedLibraries.get(name); + boolean shouldDelete = + (so == null || + so.entry.getSize() != extantFile.length() || + so.entry.getTime() != extantFile.lastModified()); + boolean upToDate = (so != null && !shouldDelete); + + if (shouldDelete) { + if (DEBUG) { + Log.v(TAG, "deleting obsolete or unexpected file: " + extantFile); + } + SysUtil.deleteOrThrow(extantFile); + } + + if (upToDate) { + if (DEBUG) { + Log.v(TAG, "found up-to-date library: " + extantFile); + } + providedLibraries.remove(name); + } + } + + // Now extract any libraries left in providedLibraries; we removed all the up-to-date ones. + for (SoInfo so : providedLibraries.values()) { + JarEntry entry = so.entry; + try (InputStream is = apk.getInputStream(entry)) { + if (DEBUG) { + Log.v(TAG, "extracting library: " + so.soName); + } + SysUtil.reliablyCopyExecutable( + is, + new File(libsDir, so.soName), + entry.getSize(), + entry.getTime()); + } + + SysUtil.freeCopyBuffer(); + } + } + } + } + + /** + * Find the shared libraries provided in this APK and supported on this system. Each returend + * SoInfo points to the most preferred version of that library bundled with the given APK: for + * example, if we're on an armv7-a system and we have both arm and armv7-a versions of libfoo, the + * returned entry for libfoo points to the armv7-a version of libfoo. + * + * The caller owns the returned value and may mutate it. + * + * @param apk Opened application APK file + * @return Map of sonames to SoInfo instances + */ + private static Map findProvidedLibraries(JarFile apk) { + // Subgroup 1: ABI. Subgroup 2: soname. + Pattern libPattern = Pattern.compile("^lib/([^/]+)/([^/]+\\.so)$"); + HashMap providedLibraries = new HashMap<>(); + String[] supportedAbis = SysUtil.getSupportedAbis(); + Enumeration entries = apk.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + Matcher m = libPattern.matcher(entry.getName()); + if (m.matches()) { + String libraryAbi = m.group(1); + String soName = m.group(2); + int abiScore = SysUtil.findAbiScore(supportedAbis, libraryAbi); + if (abiScore >= 0) { + SoInfo so = providedLibraries.get(soName); + if (so == null || abiScore < so.abiScore) { + providedLibraries.put(soName, new SoInfo(soName, entry, abiScore)); + } + } + } + } + + return providedLibraries; + } + + private static final class SoInfo { + public final String soName; + public final JarEntry entry; + public final int abiScore; + + SoInfo(String soName, JarEntry entry, int abiScore) { + this.soName = soName; + this.entry = entry; + this.abiScore = abiScore; + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/DirectorySoSource.java b/ReactAndroid/src/main/java/com/facebook/soloader/DirectorySoSource.java new file mode 100644 index 000000000..47cdb0232 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/DirectorySoSource.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.soloader; + +import java.io.File; +import java.io.IOException; + +/** + * {@link SoSource} that finds shared libraries in a given directory. + */ +public class DirectorySoSource extends SoSource { + + public static final int RESOLVE_DEPENDENCIES = 1; + public static final int ON_LD_LIBRARY_PATH = 2; + + protected final File soDirectory; + private final int flags; + + /** + * Make a new DirectorySoSource. If {@code flags} contains {@code RESOLVE_DEPENDENCIES}, + * recursively load dependencies for shared objects loaded from this directory. (We shouldn't + * need to resolve dependencies for libraries loaded from system directories: the dynamic linker + * is smart enough to do it on its own there.) + */ + public DirectorySoSource(File soDirectory, int flags) { + this.soDirectory = soDirectory; + this.flags = flags; + } + + @Override + public int loadLibrary(String soName, int loadFlags) throws IOException { + File soFile = new File(soDirectory, soName); + if (!soFile.exists()) { + return LOAD_RESULT_NOT_FOUND; + } + + if ((loadFlags & LOAD_FLAG_ALLOW_IMPLICIT_PROVISION) != 0 && + (flags & ON_LD_LIBRARY_PATH) != 0) { + return LOAD_RESULT_IMPLICITLY_PROVIDED; + } + + if ((flags & RESOLVE_DEPENDENCIES) != 0) { + String dependencies[] = MinElf.extract_DT_NEEDED(soFile); + for (int i = 0; i < dependencies.length; ++i) { + String dependency = dependencies[i]; + if (dependency.startsWith("/")) { + continue; + } + + SoLoader.loadLibraryBySoName( + dependency, + (loadFlags | LOAD_FLAG_ALLOW_IMPLICIT_PROVISION)); + } + } + + System.load(soFile.getAbsolutePath()); + return LOAD_RESULT_LOADED; + } + + @Override + public File unpackLibrary(String soName) throws IOException { + File soFile = new File(soDirectory, soName); + if (soFile.exists()) { + return soFile; + } + + return null; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Dyn.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Dyn.java new file mode 100644 index 000000000..a9ec0713d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Dyn.java @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh. +package com.facebook.soloader; +public final class Elf32_Dyn { + public static final int d_tag = 0x0; + public static final int d_un = 0x4; +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Ehdr.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Ehdr.java new file mode 100644 index 000000000..a398ffe78 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Ehdr.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh. +package com.facebook.soloader; +public final class Elf32_Ehdr { + public static final int e_ident = 0x0; + public static final int e_type = 0x10; + public static final int e_machine = 0x12; + public static final int e_version = 0x14; + public static final int e_entry = 0x18; + public static final int e_phoff = 0x1c; + public static final int e_shoff = 0x20; + public static final int e_flags = 0x24; + public static final int e_ehsize = 0x28; + public static final int e_phentsize = 0x2a; + public static final int e_phnum = 0x2c; + public static final int e_shentsize = 0x2e; + public static final int e_shnum = 0x30; + public static final int e_shstrndx = 0x32; +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Phdr.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Phdr.java new file mode 100644 index 000000000..95e2c27b2 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Phdr.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh. +package com.facebook.soloader; +public final class Elf32_Phdr { + public static final int p_type = 0x0; + public static final int p_offset = 0x4; + public static final int p_vaddr = 0x8; + public static final int p_paddr = 0xc; + public static final int p_filesz = 0x10; + public static final int p_memsz = 0x14; + public static final int p_flags = 0x18; + public static final int p_align = 0x1c; +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Shdr.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Shdr.java new file mode 100644 index 000000000..35fc8599c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf32_Shdr.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh. +package com.facebook.soloader; +public final class Elf32_Shdr { + public static final int sh_name = 0x0; + public static final int sh_type = 0x4; + public static final int sh_flags = 0x8; + public static final int sh_addr = 0xc; + public static final int sh_offset = 0x10; + public static final int sh_size = 0x14; + public static final int sh_link = 0x18; + public static final int sh_info = 0x1c; + public static final int sh_addralign = 0x20; + public static final int sh_entsize = 0x24; +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Dyn.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Dyn.java new file mode 100644 index 000000000..89f2ddbdf --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Dyn.java @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh. +package com.facebook.soloader; +public final class Elf64_Dyn { + public static final int d_tag = 0x0; + public static final int d_un = 0x8; +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Ehdr.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Ehdr.java new file mode 100644 index 000000000..4f6fa44ce --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Ehdr.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh. +package com.facebook.soloader; +public final class Elf64_Ehdr { + public static final int e_ident = 0x0; + public static final int e_type = 0x10; + public static final int e_machine = 0x12; + public static final int e_version = 0x14; + public static final int e_entry = 0x18; + public static final int e_phoff = 0x20; + public static final int e_shoff = 0x28; + public static final int e_flags = 0x30; + public static final int e_ehsize = 0x34; + public static final int e_phentsize = 0x36; + public static final int e_phnum = 0x38; + public static final int e_shentsize = 0x3a; + public static final int e_shnum = 0x3c; + public static final int e_shstrndx = 0x3e; +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Phdr.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Phdr.java new file mode 100644 index 000000000..b6436cbcb --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Phdr.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh. +package com.facebook.soloader; +public final class Elf64_Phdr { + public static final int p_type = 0x0; + public static final int p_flags = 0x4; + public static final int p_offset = 0x8; + public static final int p_vaddr = 0x10; + public static final int p_paddr = 0x18; + public static final int p_filesz = 0x20; + public static final int p_memsz = 0x28; + public static final int p_align = 0x30; +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Shdr.java b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Shdr.java new file mode 100644 index 000000000..36e8693d4 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/Elf64_Shdr.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +// AUTOMATICALLY GENERATED CODE. Regenerate with genstructs.sh. +package com.facebook.soloader; +public final class Elf64_Shdr { + public static final int sh_name = 0x0; + public static final int sh_type = 0x4; + public static final int sh_flags = 0x8; + public static final int sh_addr = 0x10; + public static final int sh_offset = 0x18; + public static final int sh_size = 0x20; + public static final int sh_link = 0x28; + public static final int sh_info = 0x2c; + public static final int sh_addralign = 0x30; + public static final int sh_entsize = 0x38; +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/ExoSoSource.java b/ReactAndroid/src/main/java/com/facebook/soloader/ExoSoSource.java new file mode 100644 index 000000000..1520aa1c9 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/ExoSoSource.java @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.soloader; + +import java.io.File; +import java.io.IOException; +import android.content.Context; + +import java.util.jar.JarFile; +import java.util.jar.JarEntry; + +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +import android.os.Build; +import android.system.Os; +import android.system.ErrnoException; + +import java.util.HashMap; +import java.util.Map; +import java.util.Enumeration; + +import java.io.InputStream; +import java.io.FileOutputStream; +import java.io.FileInputStream; +import java.io.BufferedReader; +import java.io.FileReader; + +import android.util.Log; + +/** + * {@link SoSource} that retrieves libraries from an exopackage repository. + */ +public class ExoSoSource extends DirectorySoSource { + + private static final String TAG = SoLoader.TAG; + private static final boolean DEBUG = SoLoader.DEBUG; + + /** + * @param context Application context + */ + public ExoSoSource(Context context) throws IOException { + // + // Initialize a normal DirectorySoSource that will load from our extraction directory. At this + // point, the directory may be empty or contain obsolete libraries, but that's okay. + // + + super(SysUtil.createLibsDirectory(context), DirectorySoSource.RESOLVE_DEPENDENCIES); + + // + // Synchronize the contents of that directory with the library payload in our APK, deleting and + // extracting as needed. + // + + File libsDir = super.soDirectory; + + if (DEBUG) { + Log.v(TAG, "synchronizing log directory: " + libsDir); + } + + Map providedLibraries = findProvidedLibraries(context); + try (FileLocker lock = SysUtil.lockLibsDirectory(context)) { + // Delete files in libsDir that we don't provide or that are out of date. Forget about any + // libraries that are up-to-date already so we don't unpack them below. + File extantFiles[] = libsDir.listFiles(); + for (int i = 0; i < extantFiles.length; ++i) { + File extantFile = extantFiles[i]; + + if (DEBUG) { + Log.v(TAG, "considering libdir file: " + extantFile); + } + + String name = extantFile.getName(); + File sourceFile = providedLibraries.get(name); + boolean shouldDelete = + (sourceFile == null || + sourceFile.length() != extantFile.length() || + sourceFile.lastModified() != extantFile.lastModified()); + boolean upToDate = (sourceFile != null && !shouldDelete); + + if (shouldDelete) { + if (DEBUG) { + Log.v(TAG, "deleting obsolete or unexpected file: " + extantFile); + } + SysUtil.deleteOrThrow(extantFile); + } + + if (upToDate) { + if (DEBUG) { + Log.v(TAG, "found up-to-date library: " + extantFile); + } + providedLibraries.remove(name); + } + } + + // Now extract any libraries left in providedLibraries; we removed all the up-to-date ones. + for (String soName : providedLibraries.keySet()) { + File sourceFile = providedLibraries.get(soName); + try (InputStream is = new FileInputStream(sourceFile)) { + if (DEBUG) { + Log.v(TAG, "extracting library: " + soName); + } + SysUtil.reliablyCopyExecutable( + is, + new File(libsDir, soName), + sourceFile.length(), + sourceFile.lastModified()); + } + + SysUtil.freeCopyBuffer(); + } + } + } + + /** + * Find the shared libraries provided through the exopackage directory and supported on this + * system. Each returend SoInfo points to the most preferred version of that library included in + * our exopackage directory: for example, if we're on an armv7-a system and we have both arm and + * armv7-a versions of libfoo, the returned entry for libfoo points to the armv7-a version of + * libfoo. + * + * The caller owns the returned value and may mutate it. + * + * @param context Application context + * @return Map of sonames to providing files + */ + private static Map findProvidedLibraries(Context context) throws IOException { + File exoDir = new File( + "/data/local/tmp/exopackage/" + + context.getPackageName() + + "/native-libs/"); + + HashMap providedLibraries = new HashMap<>(); + for (String abi : SysUtil.getSupportedAbis()) { + File abiDir = new File(exoDir, abi); + if (!abiDir.isDirectory()) { + continue; + } + + File metadata = new File(abiDir, "metadata.txt"); + if (!metadata.isFile()) { + continue; + } + + try (FileReader fr = new FileReader(metadata); + BufferedReader br = new BufferedReader(fr)) { + String line; + while ((line = br.readLine()) != null) { + if (line.length() == 0) { + continue; + } + + int sep = line.indexOf(' '); + if (sep == -1) { + throw new RuntimeException("illegal line in exopackage metadata: [" + line + "]"); + } + + String soName = line.substring(0, sep) + ".so"; + String backingFile = line.substring(sep + 1); + + if (!providedLibraries.containsKey(soName)) { + providedLibraries.put(soName, new File(abiDir, backingFile)); + } + } + } + } + + return providedLibraries; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/FileLocker.java b/ReactAndroid/src/main/java/com/facebook/soloader/FileLocker.java new file mode 100644 index 000000000..96a11f994 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/FileLocker.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.soloader; +import java.io.FileOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileLock; +import java.io.Closeable; + +public final class FileLocker implements Closeable { + + private final FileOutputStream mLockFileOutputStream; + private final FileLock mLock; + + public static FileLocker lock(File lockFile) throws IOException { + return new FileLocker(lockFile); + } + + private FileLocker(File lockFile) throws IOException { + mLockFileOutputStream = new FileOutputStream(lockFile); + FileLock lock = null; + try { + lock = mLockFileOutputStream.getChannel().lock(); + } finally { + if (lock == null) { + mLockFileOutputStream.close(); + } + } + + mLock = lock; + } + + @Override + public void close() throws IOException { + try { + mLock.release(); + } finally { + mLockFileOutputStream.close(); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/MinElf.java b/ReactAndroid/src/main/java/com/facebook/soloader/MinElf.java new file mode 100644 index 000000000..0477ad71d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/MinElf.java @@ -0,0 +1,282 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.soloader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.File; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; + +/** + * Extract SoLoader boottsrap information from an ELF file. This is not a general purpose ELF + * library. + * + * See specification at http://www.sco.com/developers/gabi/latest/contents.html. You will not be + * able to verify the operation of the functions below without having read the ELF specification. + */ +public final class MinElf { + + public static final int ELF_MAGIC = 0x464c457f; + + public static final int DT_NULL = 0; + public static final int DT_NEEDED = 1; + public static final int DT_STRTAB = 5; + + public static final int PT_LOAD = 1; + public static final int PT_DYNAMIC = 2; + + public static final int PN_XNUM = 0xFFFF; + + public static String[] extract_DT_NEEDED(File elfFile) throws IOException { + FileInputStream is = new FileInputStream(elfFile); + try { + return extract_DT_NEEDED(is.getChannel()); + } finally { + is.close(); // Won't throw + } + } + + /** + * Treating {@code bb} as an ELF file, extract all the DT_NEEDED entries from its dynamic section. + * + * @param fc FileChannel referring to ELF file + * @return Array of strings, one for each DT_NEEDED entry, in file order + */ + public static String[] extract_DT_NEEDED(FileChannel fc) + throws IOException { + + // + // All constants below are fixed by the ELF specification and are the offsets of fields within + // the elf.h data structures. + // + + ByteBuffer bb = ByteBuffer.allocate(8 /* largest read unit */); + + // Read ELF header. + + bb.order(ByteOrder.LITTLE_ENDIAN); + if (getu32(fc, bb, Elf32_Ehdr.e_ident) != ELF_MAGIC) { + throw new ElfError("file is not ELF"); + } + + boolean is32 = (getu8(fc, bb, Elf32_Ehdr.e_ident + 0x4) == 1); + if (getu8(fc, bb, Elf32_Ehdr.e_ident + 0x5) == 2) { + bb.order(ByteOrder.BIG_ENDIAN); + } + + // Offsets above are identical in 32- and 64-bit cases. + + // Find the offset of the dynamic linking information. + + long e_phoff = is32 + ? getu32(fc, bb, Elf32_Ehdr.e_phoff) + : get64(fc, bb, Elf64_Ehdr.e_phoff); + + long e_phnum = is32 + ? getu16(fc, bb, Elf32_Ehdr.e_phnum) + : getu16(fc, bb, Elf64_Ehdr.e_phnum); + + int e_phentsize = is32 + ? getu16(fc, bb, Elf32_Ehdr.e_phentsize) + : getu16(fc, bb, Elf64_Ehdr.e_phentsize); + + if (e_phnum == PN_XNUM) { // Overflowed into section[0].sh_info + + long e_shoff = is32 + ? getu32(fc, bb, Elf32_Ehdr.e_shoff) + : get64(fc, bb, Elf64_Ehdr.e_shoff); + + long sh_info = is32 + ? getu32(fc, bb, e_shoff + Elf32_Shdr.sh_info) + : getu32(fc, bb, e_shoff + Elf64_Shdr.sh_info); + + e_phnum = sh_info; + } + + long dynStart = 0; + long phdr = e_phoff; + + for (long i = 0; i < e_phnum; ++i) { + long p_type = is32 + ? getu32(fc, bb, phdr + Elf32_Phdr.p_type) + : getu32(fc, bb, phdr + Elf64_Phdr.p_type); + + if (p_type == PT_DYNAMIC) { + long p_offset = is32 + ? getu32(fc, bb, phdr + Elf32_Phdr.p_offset) + : get64(fc, bb, phdr + Elf64_Phdr.p_offset); + + dynStart = p_offset; + break; + } + + phdr += e_phentsize; + } + + if (dynStart == 0) { + throw new ElfError("ELF file does not contain dynamic linking information"); + } + + // Walk the items in the dynamic section, counting the DT_NEEDED entries. Also remember where + // the string table for those entries lives. That table is a pointer, which we translate to an + // offset below. + + long d_tag; + int nr_DT_NEEDED = 0; + long dyn = dynStart; + long ptr_DT_STRTAB = 0; + + do { + d_tag = is32 + ? getu32(fc, bb, dyn + Elf32_Dyn.d_tag) + : get64(fc, bb, dyn + Elf64_Dyn.d_tag); + + if (d_tag == DT_NEEDED) { + if (nr_DT_NEEDED == Integer.MAX_VALUE) { + throw new ElfError("malformed DT_NEEDED section"); + } + + nr_DT_NEEDED += 1; + } else if (d_tag == DT_STRTAB) { + ptr_DT_STRTAB = is32 + ? getu32(fc, bb, dyn + Elf32_Dyn.d_un) + : get64(fc, bb, dyn + Elf64_Dyn.d_un); + } + + dyn += is32 ? 8 : 16; + } while (d_tag != DT_NULL); + + if (ptr_DT_STRTAB == 0) { + throw new ElfError("Dynamic section string-table not found"); + } + + // Translate the runtime string table pointer we found above to a file offset. + + long off_DT_STRTAB = 0; + phdr = e_phoff; + + for (int i = 0; i < e_phnum; ++i) { + long p_type = is32 + ? getu32(fc, bb, phdr + Elf32_Phdr.p_type) + : getu32(fc, bb, phdr + Elf64_Phdr.p_type); + + if (p_type == PT_LOAD) { + long p_vaddr = is32 + ? getu32(fc, bb, phdr + Elf32_Phdr.p_vaddr) + : get64(fc, bb, phdr + Elf64_Phdr.p_vaddr); + + long p_memsz = is32 + ? getu32(fc, bb, phdr + Elf32_Phdr.p_memsz) + : get64(fc, bb, phdr + Elf64_Phdr.p_memsz); + + if (p_vaddr <= ptr_DT_STRTAB && ptr_DT_STRTAB < p_vaddr + p_memsz) { + long p_offset = is32 + ? getu32(fc, bb, phdr + Elf32_Phdr.p_offset) + : get64(fc, bb, phdr + Elf64_Phdr.p_offset); + + off_DT_STRTAB = p_offset + (ptr_DT_STRTAB - p_vaddr); + break; + } + } + + phdr += e_phentsize; + } + + if (off_DT_STRTAB == 0) { + throw new ElfError("did not find file offset of DT_STRTAB table"); + } + + String[] needed = new String[nr_DT_NEEDED]; + + nr_DT_NEEDED = 0; + dyn = dynStart; + + do { + d_tag = is32 + ? getu32(fc, bb, dyn + Elf32_Dyn.d_tag) + : get64(fc, bb, dyn + Elf64_Dyn.d_tag); + + if (d_tag == DT_NEEDED) { + long d_val = is32 + ? getu32(fc, bb, dyn + Elf32_Dyn.d_un) + : get64(fc, bb, dyn + Elf64_Dyn.d_un); + + needed[nr_DT_NEEDED] = getSz(fc, bb, off_DT_STRTAB + d_val); + if (nr_DT_NEEDED == Integer.MAX_VALUE) { + throw new ElfError("malformed DT_NEEDED section"); + } + + nr_DT_NEEDED += 1; + } + + dyn += is32 ? 8 : 16; + } while (d_tag != DT_NULL); + + if (nr_DT_NEEDED != needed.length) { + throw new ElfError("malformed DT_NEEDED section"); + } + + return needed; + } + + private static String getSz(FileChannel fc, ByteBuffer bb, long offset) + throws IOException { + StringBuilder sb = new StringBuilder(); + short b; + while ((b = getu8(fc, bb, offset++)) != 0) { + sb.append((char) b); + } + + return sb.toString(); + } + + private static void read(FileChannel fc, ByteBuffer bb, int sz, long offset) + throws IOException { + bb.position(0); + bb.limit(sz); + if (fc.read(bb, offset) != sz) { + throw new ElfError("ELF file truncated"); + } + + bb.position(0); + } + + private static long get64(FileChannel fc, ByteBuffer bb, long offset) + throws IOException { + read(fc, bb, 8, offset); + return bb.getLong(); + } + + private static long getu32(FileChannel fc, ByteBuffer bb, long offset) + throws IOException { + read(fc, bb, 4, offset); + return bb.getInt() & 0xFFFFFFFFL; // signed -> unsigned + } + + private static int getu16(FileChannel fc, ByteBuffer bb, long offset) + throws IOException { + read(fc, bb, 2, offset); + return bb.getShort() & (int) 0xFFFF; // signed -> unsigned + } + + private static short getu8(FileChannel fc, ByteBuffer bb, long offset) + throws IOException { + read(fc, bb, 1, offset); + return (short) (bb.get() & 0xFF); // signed -> unsigned + } + + private static class ElfError extends RuntimeException { + ElfError(String why) { + super(why); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/NativeLibrary.java b/ReactAndroid/src/main/java/com/facebook/soloader/NativeLibrary.java new file mode 100644 index 000000000..7277474d6 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/NativeLibrary.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.soloader; + +import java.util.List; + +import android.util.Log; + +/** + * This is the base class for all the classes representing certain native library. + * For loading native libraries we should always inherit from this class and provide relevant + * information (libraries to load, code to test native call, dependencies?). + *

    + * This instances should be singletons provided by DI. + *

    + * This is a basic template but could be improved if we find the need. + */ +public abstract class NativeLibrary { + private static final String TAG = NativeLibrary.class.getName(); + + private final Object mLock; + private List mLibraryNames; + private Boolean mLoadLibraries; + private boolean mLibrariesLoaded; + private volatile UnsatisfiedLinkError mLinkError; + + protected NativeLibrary(List libraryNames) { + mLock = new Object(); + mLoadLibraries = true; + mLibrariesLoaded = false; + mLinkError = null; + mLibraryNames = libraryNames; + } + + /** + * safe loading of native libs + * @return true if native libs loaded properly, false otherwise + */ + public boolean loadLibraries() { + synchronized (mLock) { + if (mLoadLibraries == false) { + return mLibrariesLoaded; + } + try { + for (String name: mLibraryNames) { + SoLoader.loadLibrary(name); + } + initialNativeCheck(); + mLibrariesLoaded = true; + mLibraryNames = null; + } catch (UnsatisfiedLinkError error) { + Log.e(TAG, "Failed to load native lib: ", error); + mLinkError = error; + mLibrariesLoaded = false; + } + mLoadLibraries = false; + return mLibrariesLoaded; + } + } + + /** + * loads libraries (if not loaded yet), throws on failure + * @throws UnsatisfiedLinkError + */ + + public void ensureLoaded() throws UnsatisfiedLinkError { + if (!loadLibraries()) { + throw mLinkError; + } + } + + /** + * Override this method to make some concrete (quick and harmless) native call. + * This avoids lazy-loading some phones (LG) use when we call loadLibrary. If there's a problem + * we'll face an UnsupportedLinkError when first using the feature instead of here. + * This check force a check right when intended. + * This way clients of this library can know if it's loaded for sure or not. + * @throws UnsatisfiedLinkError if there was an error loading native library + */ + protected void initialNativeCheck() throws UnsatisfiedLinkError { + } + + public UnsatisfiedLinkError getError() { + return mLinkError; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/NoopSoSource.java b/ReactAndroid/src/main/java/com/facebook/soloader/NoopSoSource.java new file mode 100644 index 000000000..cd5d15e48 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/NoopSoSource.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.soloader; + +import java.io.File; + +/** + * {@link SoSource} that does nothing and pretends to successfully load all libraries. + */ +public class NoopSoSource extends SoSource { + @Override + public int loadLibrary(String soName, int loadFlags) { + return LOAD_RESULT_LOADED; + } + + @Override + public File unpackLibrary(String soName) { + throw new UnsupportedOperationException( + "unpacking not supported in test mode"); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/SoLoader.java b/ReactAndroid/src/main/java/com/facebook/soloader/SoLoader.java new file mode 100644 index 000000000..a070ed9a9 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/SoLoader.java @@ -0,0 +1,237 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.soloader; + +import java.io.BufferedOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.HashSet; +import java.util.ArrayList; +import java.io.FileNotFoundException; + +import java.util.Set; + +import javax.annotation.Nullable; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.os.StatFs; +import android.util.Log; + +import android.content.pm.ApplicationInfo; + +/** + * Note that {@link com.facebook.base.app.DelegatingApplication} will automatically register itself + * with SoLoader before running application-specific code; most applications do not need to call + * {@link #init} explicitly. + */ +@SuppressLint({ + "BadMethodUse-android.util.Log.v", + "BadMethodUse-android.util.Log.d", + "BadMethodUse-android.util.Log.i", + "BadMethodUse-android.util.Log.w", + "BadMethodUse-android.util.Log.e", +}) +public class SoLoader { + + /* package */ static final String TAG = "SoLoader"; + /* package */ static final boolean DEBUG = false; + + /** + * Ordered list of sources to consult when trying to load a shared library or one of its + * dependencies. {@code null} indicates that SoLoader is uninitialized. + */ + @Nullable private static SoSource[] sSoSources = null; + + /** + * Records the sonames (e.g., "libdistract.so") of shared libraries we've loaded. + */ + private static final Set sLoadedLibraries = new HashSet<>(); + + /** + * Initializes native code loading for this app; this class's other static facilities cannot be + * used until this {@link #init} is called. This method is idempotent: calls after the first are + * ignored. + * + * @param context - application context. + * @param isNativeExopackageEnabled - whether native exopackage feature is enabled in the build. + */ + public static synchronized void init(@Nullable Context context, boolean isNativeExopackageEnabled) { + if (sSoSources == null) { + ArrayList soSources = new ArrayList<>(); + + // + // Add SoSource objects for each of the system library directories. + // + + String LD_LIBRARY_PATH = System.getenv("LD_LIBRARY_PATH"); + if (LD_LIBRARY_PATH == null) { + LD_LIBRARY_PATH = "/vendor/lib:/system/lib"; + } + + String[] systemLibraryDirectories = LD_LIBRARY_PATH.split(":"); + for (int i = 0; i < systemLibraryDirectories.length; ++i) { + // Don't pass DirectorySoSource.RESOLVE_DEPENDENCIES for directories we find on + // LD_LIBRARY_PATH: Bionic's dynamic linker is capable of correctly resolving dependencies + // these libraries have on each other, so doing that ourselves would be a waste. + File systemSoDirectory = new File(systemLibraryDirectories[i]); + soSources.add( + new DirectorySoSource( + systemSoDirectory, + DirectorySoSource.ON_LD_LIBRARY_PATH)); + } + + // + // We can only proceed forward if we have a Context. The prominent case + // where we don't have a Context is barebones dalvikvm instantiations. In + // that case, the caller is responsible for providing a correct LD_LIBRARY_PATH. + // + + if (context != null) { + // + // Prepend our own SoSource for our own DSOs. + // + + ApplicationInfo applicationInfo = context.getApplicationInfo(); + boolean isSystemApplication = + (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 && + (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0; + + try { + if (isNativeExopackageEnabled) { + soSources.add(0, new ExoSoSource(context)); + } else if (isSystemApplication) { + soSources.add(0, new ApkSoSource(context)); + } else { + // Delete the old libs directory if we don't need it. + SysUtil.dumbDeleteRecrusive(SysUtil.getLibsDirectory(context)); + + int ourSoSourceFlags = 0; + + // On old versions of Android, Bionic doesn't add our library directory to its internal + // search path, and the system doesn't resolve dependencies between modules we ship. On + // these systems, we resolve dependencies ourselves. On other systems, Bionic's built-in + // resolver suffices. + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) { + ourSoSourceFlags |= DirectorySoSource.RESOLVE_DEPENDENCIES; + } + + SoSource ourSoSource = new DirectorySoSource( + new File(applicationInfo.nativeLibraryDir), + ourSoSourceFlags); + + soSources.add(0, ourSoSource); + } + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + sSoSources = soSources.toArray(new SoSource[soSources.size()]); + } + } + + /** + * Turn shared-library loading into a no-op. Useful in special circumstances. + */ + public static void setInTestMode() { + sSoSources = new SoSource[]{new NoopSoSource()}; + } + + /** + * Load a shared library, initializing any JNI binding it contains. + * + * @param shortName Name of library to find, without "lib" prefix or ".so" suffix + */ + public static synchronized void loadLibrary(String shortName) + throws UnsatisfiedLinkError + { + if (sSoSources == null) { + // This should never happen during normal operation, + // but if we're running in a non-Android environment, + // fall back to System.loadLibrary. + if ("http://www.android.com/".equals(System.getProperty("java.vendor.url"))) { + // This will throw. + assertInitialized(); + } else { + // Not on an Android system. Ask the JVM to load for us. + System.loadLibrary(shortName); + return; + } + } + + try { + loadLibraryBySoName(System.mapLibraryName(shortName), 0); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Unpack library and its dependencies, returning the location of the unpacked library file. All + * non-system dependencies of the given library will either be on LD_LIBRARY_PATH or will be in + * the same directory as the returned File. + * + * @param shortName Name of library to find, without "lib" prefix or ".so" suffix + * @return Unpacked DSO location + */ + public static File unpackLibraryAndDependencies(String shortName) + throws UnsatisfiedLinkError + { + assertInitialized(); + try { + return unpackLibraryBySoName(System.mapLibraryName(shortName)); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + /* package */ static void loadLibraryBySoName(String soName, int loadFlags) throws IOException { + int result = sLoadedLibraries.contains(soName) + ? SoSource.LOAD_RESULT_LOADED + : SoSource.LOAD_RESULT_NOT_FOUND; + + for (int i = 0; result == SoSource.LOAD_RESULT_NOT_FOUND && i < sSoSources.length; ++i) { + result = sSoSources[i].loadLibrary(soName, loadFlags); + } + + if (result == SoSource.LOAD_RESULT_NOT_FOUND) { + throw new UnsatisfiedLinkError("could find DSO to load: " + soName); + } + + if (result == SoSource.LOAD_RESULT_LOADED) { + sLoadedLibraries.add(soName); + } + } + + /* package */ static File unpackLibraryBySoName(String soName) throws IOException { + for (int i = 0; i < sSoSources.length; ++i) { + File unpacked = sSoSources[i].unpackLibrary(soName); + if (unpacked != null) { + return unpacked; + } + } + + throw new FileNotFoundException(soName); + } + + private static void assertInitialized() { + if (sSoSources == null) { + throw new RuntimeException("SoLoader.init() not yet called"); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/SoSource.java b/ReactAndroid/src/main/java/com/facebook/soloader/SoSource.java new file mode 100644 index 000000000..016013e15 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/SoSource.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.soloader; + +import java.io.File; +import java.io.IOException; + +abstract public class SoSource { + + /** + * This SoSource doesn't know how to provide the given library. + */ + public static final int LOAD_RESULT_NOT_FOUND = 0; + + /** + * This SoSource loaded the given library. + */ + public static final int LOAD_RESULT_LOADED = 1; + + /** + * This SoSource did not load the library, but verified that the system loader will load it if + * some other library depends on it. Returned only if LOAD_FLAG_ALLOW_IMPLICIT_PROVISION is + * provided to loadLibrary. + */ + public static final int LOAD_RESULT_IMPLICITLY_PROVIDED = 2; + + /** + * Allow loadLibrary to implicitly provide the library instead of actually loading it. + */ + public static final int LOAD_FLAG_ALLOW_IMPLICIT_PROVISION = 1; + + /** + * Load a shared library library into this process. This routine is independent of + * {@link #loadLibrary}. + * + * @param soName Name of library to load + * @param loadFlags Zero or more of the LOAD_FLAG_XXX constants. + * @return One of the LOAD_RESULT_XXX constants. + */ + abstract public int loadLibrary(String soName, int LoadFlags) throws IOException; + + /** + * Ensure that a shared library exists on disk somewhere. This routine is independent of + * {@link #loadLibrary}. + * + * @param soName Name of library to load + * @return File if library found; {@code null} if not. + */ + abstract public File unpackLibrary(String soName) throws IOException; +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/SysUtil.java b/ReactAndroid/src/main/java/com/facebook/soloader/SysUtil.java new file mode 100644 index 000000000..91f28583e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/SysUtil.java @@ -0,0 +1,205 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.soloader; + +import java.io.File; +import java.io.IOException; +import android.content.Context; + +import java.util.jar.JarFile; +import java.util.jar.JarEntry; + +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +import android.os.Build; +import android.system.Os; +import android.system.ErrnoException; + +import java.util.HashMap; +import java.util.Map; +import java.util.Enumeration; + +import java.io.InputStream; +import java.io.FileOutputStream; +import java.io.FileDescriptor; + +/*package*/ final class SysUtil { + + private static byte[] cachedBuffer = null; + + /** + * Copy from an inputstream to a named filesystem file. Take care to ensure that we can detect + * incomplete copies and that the copied bytes make it to stable storage before returning. + * The destination file will be marked executable. + * + * This routine caches an internal buffer between invocations; after making a sequence of calls + * {@link #reliablyCopyExecutable} calls, call {@link #freeCopyBuffer} to release this buffer. + * + * @param is Stream from which to copy + * @param destination File to which to write + * @param expectedSize Number of bytes we expect to write; -1 if unknown + * @param time Modification time to which to set file on success; must be in the past + */ + public static void reliablyCopyExecutable( + InputStream is, + File destination, + long expectedSize, + long time) throws IOException { + destination.delete(); + try (FileOutputStream os = new FileOutputStream(destination)) { + byte buffer[]; + if (cachedBuffer == null) { + cachedBuffer = buffer = new byte[16384]; + } else { + buffer = cachedBuffer; + } + + int nrBytes; + if (expectedSize > 0) { + fallocateIfSupported(os.getFD(), expectedSize); + } + + while ((nrBytes = is.read(buffer, 0, buffer.length)) >= 0) { + os.write(buffer, 0, nrBytes); + } + + os.getFD().sync(); + destination.setExecutable(true); + destination.setLastModified(time); + os.getFD().sync(); + } + } + + /** + * Free the internal buffer cache for {@link #reliablyCopyExecutable}. + */ + public static void freeCopyBuffer() { + cachedBuffer = null; + } + + /** + * Determine how preferred a given ABI is on this system. + * + * @param supportedAbis ABIs on this system + * @param abi ABI of a shared library we might want to unpack + * @return -1 if not supported or an integer, smaller being more preferred + */ + public static int findAbiScore(String[] supportedAbis, String abi) { + for (int i = 0; i < supportedAbis.length; ++i) { + if (supportedAbis[i] != null && abi.equals(supportedAbis[i])) { + return i; + } + } + + return -1; + } + + public static void deleteOrThrow(File file) throws IOException { + if (!file.delete()) { + throw new IOException("could not delete file " + file); + } + } + + /** + * Return an list of ABIs we supported on this device ordered according to preference. Use a + * separate inner class to isolate the version-dependent call where it won't cause the whole + * class to fail preverification. + * + * @return Ordered array of supported ABIs + */ + public static String[] getSupportedAbis() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return new String[]{Build.CPU_ABI, Build.CPU_ABI2}; + } else { + return LollipopSysdeps.getSupportedAbis(); + } + } + + /** + * Pre-allocate disk space for a file if we can do that + * on this version of the OS. + * + * @param fd File descriptor for file + * @param length Number of bytes to allocate. + */ + public static void fallocateIfSupported(FileDescriptor fd, long length) throws IOException { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + LollipopSysdeps.fallocate(fd, length); + } + } + + public static FileLocker lockLibsDirectory(Context context) throws IOException { + File lockFile = new File(context.getApplicationInfo().dataDir, "libs-dir-lock"); + return FileLocker.lock(lockFile); + } + + /** + * Return the directory into which we put our self-extracted native libraries. + * + * @param context Application context + * @return File pointing to an existing directory + */ + /* package */ static File getLibsDirectory(Context context) { + return new File(context.getApplicationInfo().dataDir, "app_libs"); + } + + /** + * Return the directory into which we put our self-extracted native libraries and make sure it + * exists. + */ + /* package */ static File createLibsDirectory(Context context) { + File libsDirectory = getLibsDirectory(context); + if (!libsDirectory.isDirectory() && !libsDirectory.mkdirs()) { + throw new RuntimeException("could not create libs directory"); + } + + return libsDirectory; + } + + /** + * Delete a directory and its contents. + * + * WARNING: Java APIs do not let us distinguish directories from symbolic links to directories. + * Consequently, if the directory contains symbolic links to directories, we will attempt to + * delete the contents of pointed-to directories. + * + * @param file File or directory to delete + */ + /* package */ static void dumbDeleteRecrusive(File file) throws IOException { + if (file.isDirectory()) { + for (File entry : file.listFiles()) { + dumbDeleteRecrusive(entry); + } + } + + if (!file.delete() && file.exists()) { + throw new IOException("could not delete: " + file); + } + } + + /** + * Encapsulate Lollipop-specific calls into an independent class so we don't fail preverification + * downlevel. + */ + private static final class LollipopSysdeps { + public static String[] getSupportedAbis() { + return Build.SUPPORTED_32_BIT_ABIS; // We ain't doing no newfangled 64-bit + } + + public static void fallocate(FileDescriptor fd, long length) throws IOException { + try { + Os.posix_fallocate(fd, 0, length); + } catch (ErrnoException ex) { + throw new IOException(ex.toString(), ex); + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/genstructs.sh b/ReactAndroid/src/main/java/com/facebook/soloader/genstructs.sh new file mode 100644 index 000000000..a7bcd49a5 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/genstructs.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# +# This script generates Java structures that contain the offsets of +# fields in various ELF ABI structures. com.facebook.soloader.MinElf +# uses these structures while parsing ELF files. +# + +set -euo pipefail + +struct2java() { + ../../../../scripts/struct2java.py "$@" +} + +declare -a structs=(Elf32_Ehdr Elf64_Ehdr) +structs+=(Elf32_Ehdr Elf64_Ehdr) +structs+=(Elf32_Phdr Elf64_Phdr) +structs+=(Elf32_Shdr Elf64_Shdr) +structs+=(Elf32_Dyn Elf64_Dyn) + +for struct in "${structs[@]}"; do + cat > elfhdr.c < +static const $struct a; +EOF + gcc -g -c -o elfhdr.o elfhdr.c + cat > $struct.java <> $struct.java +done + +rm -f elfhdr.o elfhdr.c diff --git a/ReactAndroid/src/main/java/com/facebook/soloader/soloader.pro b/ReactAndroid/src/main/java/com/facebook/soloader/soloader.pro new file mode 100644 index 000000000..4a832314c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/soloader/soloader.pro @@ -0,0 +1,6 @@ +# Ensure that methods from LollipopSysdeps don't get inlined. LollipopSysdeps.fallocate references +# an exception that isn't present prior to Lollipop, which trips up the verifier if the class is +# loaded on a pre-Lollipop OS. +-keep class com.facebook.soloader.SysUtil$LollipopSysdeps { + public ; +} diff --git a/ReactAndroid/src/main/java/com/facebook/systrace/Systrace.java b/ReactAndroid/src/main/java/com/facebook/systrace/Systrace.java new file mode 100644 index 000000000..dd523375b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/systrace/Systrace.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.systrace; + + +/** + * Systrace stub. + */ +public class Systrace { + + public static final long TRACE_TAG_REACT_JAVA_BRIDGE = 0L; + + public static void beginSection(long tag, final String sectionName) { + } + + public static void endSection(long tag) { + } + + public static void traceCounter( + long tag, + final String counterName, + final int counterValue) { + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/systrace/SystraceMessage.java b/ReactAndroid/src/main/java/com/facebook/systrace/SystraceMessage.java new file mode 100644 index 000000000..3a255eb94 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/systrace/SystraceMessage.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.systrace; + +/** + * Systrace stub. + */ +public final class SystraceMessage { + + private static final Builder NOOP_BUILDER = new NoopBuilder(); + + public static Builder beginSection(long tag, String sectionName) { + return NOOP_BUILDER; + } + + public static Builder endSection(long tag) { + return NOOP_BUILDER; + } + + public static abstract class Builder { + + public abstract void flush(); + + public abstract Builder arg(String key, Object value); + + public abstract Builder arg(String key, int value); + + public abstract Builder arg(String key, long value); + + public abstract Builder arg(String key, double value); + } + + private interface Flusher { + void flush(StringBuilder builder); + } + + private static class NoopBuilder extends Builder { + @Override + public void flush() { + } + + @Override + public Builder arg(String key, Object value) { + return this; + } + + @Override + public Builder arg(String key, int value) { + return this; + } + + @Override + public Builder arg(String key, long value) { + return this; + } + + @Override + public Builder arg(String key, double value) { + return this; + } + } +} diff --git a/ReactAndroid/src/main/jni/Application.mk b/ReactAndroid/src/main/jni/Application.mk new file mode 100644 index 000000000..d8f9dda84 --- /dev/null +++ b/ReactAndroid/src/main/jni/Application.mk @@ -0,0 +1,14 @@ +APP_BUILD_SCRIPT := Android.mk + +APP_ABI := armeabi-v7a x86 +APP_PLATFORM := android-9 + +APP_MK_DIR := $(dir $(lastword $(MAKEFILE_LIST))) +NDK_MODULE_PATH := $(APP_MK_DIR):$(THIRD_PARTY_NDK_DIR):$(APP_MK_DIR)/first-party + +APP_STL := gnustl_shared + +# Make sure every shared lib includes a .note.gnu.build-id header +APP_LDFLAGS := -Wl,--build-id + +NDK_TOOLCHAIN_VERSION := 4.8 diff --git a/ReactAndroid/src/main/jni/first-party/fb/Android.mk b/ReactAndroid/src/main/jni/first-party/fb/Android.mk new file mode 100644 index 000000000..3361c433d --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/Android.mk @@ -0,0 +1,30 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + assert.cpp \ + log.cpp \ + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/.. $(LOCAL_PATH)/include +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/.. $(LOCAL_PATH)/include + +LOCAL_CFLAGS := -DLOG_TAG=\"libfb\" +LOCAL_CFLAGS += -Wall -Werror +# include/utils/threads.h has unused parameters +LOCAL_CFLAGS += -Wno-unused-parameter +ifeq ($(TOOLCHAIN_PERMISSIVE),true) + LOCAL_CFLAGS += -Wno-error=unused-but-set-variable +endif +LOCAL_CFLAGS += -DHAVE_POSIX_CLOCKS + +CXX11_FLAGS := -std=c++11 +LOCAL_CFLAGS += $(CXX11_FLAGS) + +LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) + +LOCAL_LDLIBS := -llog -ldl -landroid +LOCAL_EXPORT_LDLIBS := -llog + +LOCAL_MODULE := libfb + +include $(BUILD_SHARED_LIBRARY) \ No newline at end of file diff --git a/ReactAndroid/src/main/jni/first-party/fb/Countable.h b/ReactAndroid/src/main/jni/first-party/fb/Countable.h new file mode 100644 index 000000000..1e402a3fc --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/Countable.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once +#include +#include +#include +#include +#include + +namespace facebook { + +class Countable : public noncopyable, public nonmovable { +public: + // RefPtr expects refcount to start at 0 + Countable() : m_refcount(0) {} + virtual ~Countable() + { + FBASSERT(m_refcount == 0); + } + +private: + void ref() { + ++m_refcount; + } + + void unref() { + if (0 == --m_refcount) { + delete this; + } + } + + bool hasOnlyOneRef() const { + return m_refcount == 1; + } + + template friend class RefPtr; + std::atomic m_refcount; +}; + +} diff --git a/ReactAndroid/src/main/jni/first-party/fb/ProgramLocation.h b/ReactAndroid/src/main/jni/first-party/fb/ProgramLocation.h new file mode 100644 index 000000000..36f7737f6 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/ProgramLocation.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once +#include +#include +#include + +namespace facebook { + +#define FROM_HERE facebook::ProgramLocation(__FUNCTION__, __FILE__, __LINE__) + +class ProgramLocation { +public: + ProgramLocation() : m_functionName("Unspecified"), m_fileName("Unspecified"), m_lineNumber(0) {} + + ProgramLocation(const char* functionName, const char* fileName, int line) : + m_functionName(functionName), + m_fileName(fileName), + m_lineNumber(line) + {} + + const char* functionName() const { return m_functionName; } + const char* fileName() const { return m_fileName; } + int lineNumber() const { return m_lineNumber; } + + std::string asFormattedString() const { + std::stringstream str; + str << "Function " << m_functionName << " in file " << m_fileName << ":" << m_lineNumber; + return str.str(); + } + + bool operator==(const ProgramLocation& other) const { + // Assumes that the strings are static + return (m_functionName == other.m_functionName) && (m_fileName == other.m_fileName) && m_lineNumber == other.m_lineNumber; + } + +private: + const char* m_functionName; + const char* m_fileName; + int m_lineNumber; +}; + +} diff --git a/ReactAndroid/src/main/jni/first-party/fb/RefPtr.h b/ReactAndroid/src/main/jni/first-party/fb/RefPtr.h new file mode 100644 index 000000000..d21fe697e --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/RefPtr.h @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once +#include +#include + +namespace facebook { + +// Reference counting smart pointer. This is designed to work with the +// Countable class or other implementations in the future. It is designed in a +// way to be both efficient and difficult to misuse. Typical usage is very +// simple once you learn the patterns (and the compiler will help!): +// +// By default, the internal pointer is null. +// RefPtr ref; +// +// Object creation requires explicit construction: +// RefPtr ref = createNew(...); +// +// Or if the constructor is not public: +// RefPtr ref = adoptRef(new Foo(...)); +// +// But you can implicitly create from nullptr: +// RefPtr maybeRef = cond ? ref : nullptr; +// +// Move/Copy Construction/Assignment are straightforward: +// RefPtr ref2 = ref; +// ref = std::move(ref2); +// +// Destruction automatically drops the RefPtr's reference as expected. +// +// Upcasting is implicit but downcasting requires an explicit cast: +// struct Bar : public Foo {}; +// RefPtr barRef = static_cast>(ref); +// ref = barRef; +// +template +class RefPtr { +public: + constexpr RefPtr() : + m_ptr(nullptr) + {} + + // Allow implicit construction from a pointer only from nullptr + constexpr RefPtr(std::nullptr_t ptr) : + m_ptr(nullptr) + {} + + RefPtr(const RefPtr& ref) : + m_ptr(ref.m_ptr) + { + refIfNecessary(m_ptr); + } + + // Only allow implicit upcasts. A downcast will result in a compile error + // unless you use static_cast (which will end up invoking the explicit + // operator below). + template + RefPtr(const RefPtr& ref, typename std::enable_if::value, U>::type* = nullptr) : + m_ptr(ref.get()) + { + refIfNecessary(m_ptr); + } + + RefPtr(RefPtr&& ref) : + m_ptr(nullptr) + { + *this = std::move(ref); + } + + // Only allow implicit upcasts. A downcast will result in a compile error + // unless you use static_cast (which will end up invoking the explicit + // operator below). + template + RefPtr(RefPtr&& ref, typename std::enable_if::value, U>::type* = nullptr) : + m_ptr(nullptr) + { + *this = std::move(ref); + } + + ~RefPtr() { + unrefIfNecessary(m_ptr); + m_ptr = nullptr; + } + + RefPtr& operator=(const RefPtr& ref) { + if (m_ptr != ref.m_ptr) { + unrefIfNecessary(m_ptr); + m_ptr = ref.m_ptr; + refIfNecessary(m_ptr); + } + return *this; + } + + // The STL assumes rvalue references are unique and for simplicity's sake, we + // make the same assumption here, that &ref != this. + RefPtr& operator=(RefPtr&& ref) { + unrefIfNecessary(m_ptr); + m_ptr = ref.m_ptr; + ref.m_ptr = nullptr; + return *this; + } + + template + RefPtr& operator=(RefPtr&& ref) { + unrefIfNecessary(m_ptr); + m_ptr = ref.m_ptr; + ref.m_ptr = nullptr; + return *this; + } + + void reset() { + unrefIfNecessary(m_ptr); + m_ptr = nullptr; + } + + T* get() const { + return m_ptr; + } + + T* operator->() const { + return m_ptr; + } + + T& operator*() const { + return *m_ptr; + } + + template + explicit operator RefPtr () const; + + explicit operator bool() const { + return m_ptr ? true : false; + } + + bool isTheLastRef() const { + FBASSERT(m_ptr); + return m_ptr->hasOnlyOneRef(); + } + + // Creates a strong reference from a raw pointer, assuming that is already + // referenced from some other RefPtr. This should be used sparingly. + static inline RefPtr assumeAlreadyReffed(T* ptr) { + return RefPtr(ptr, ConstructionMode::External); + } + + // Creates a strong reference from a raw pointer, assuming that it points to a + // freshly-created object. See the documentation for RefPtr for usage. + static inline RefPtr adoptRef(T* ptr) { + return RefPtr(ptr, ConstructionMode::Adopted); + } + +private: + enum class ConstructionMode { + Adopted, + External + }; + + RefPtr(T* ptr, ConstructionMode mode) : + m_ptr(ptr) + { + FBASSERTMSGF(ptr, "Got null pointer in %s construction mode", mode == ConstructionMode::Adopted ? "adopted" : "external"); + ptr->ref(); + if (mode == ConstructionMode::Adopted) { + FBASSERT(ptr->hasOnlyOneRef()); + } + } + + static inline void refIfNecessary(T* ptr) { + if (ptr) { + ptr->ref(); + } + } + static inline void unrefIfNecessary(T* ptr) { + if (ptr) { + ptr->unref(); + } + } + + template friend class RefPtr; + + T* m_ptr; +}; + +// Creates a strong reference from a raw pointer, assuming that is already +// referenced from some other RefPtr and that it is non-null. This should be +// used sparingly. +template +static inline RefPtr assumeAlreadyReffed(T* ptr) { + return RefPtr::assumeAlreadyReffed(ptr); +} + +// As above, but tolerant of nullptr. +template +static inline RefPtr assumeAlreadyReffedOrNull(T* ptr) { + return ptr ? RefPtr::assumeAlreadyReffed(ptr) : nullptr; +} + +// Creates a strong reference from a raw pointer, assuming that it points to a +// freshly-created object. See the documentation for RefPtr for usage. +template +static inline RefPtr adoptRef(T* ptr) { + return RefPtr::adoptRef(ptr); +} + +template +static inline RefPtr createNew(Args&&... arguments) { + return RefPtr::adoptRef(new T(std::forward(arguments)...)); +} + +template template +RefPtr::operator RefPtr() const { + static_assert(std::is_base_of::value, "Invalid static cast"); + return assumeAlreadyReffedOrNull(static_cast(m_ptr)); +} + +template +inline bool operator==(const RefPtr& a, const RefPtr& b) { + return a.get() == b.get(); +} + +template +inline bool operator!=(const RefPtr& a, const RefPtr& b) { + return a.get() != b.get(); +} + +template +inline bool operator==(const RefPtr& ref, U* ptr) { + return ref.get() == ptr; +} + +template +inline bool operator!=(const RefPtr& ref, U* ptr) { + return ref.get() != ptr; +} + +template +inline bool operator==(U* ptr, const RefPtr& ref) { + return ref.get() == ptr; +} + +template +inline bool operator!=(U* ptr, const RefPtr& ref) { + return ref.get() != ptr; +} + +template +inline bool operator==(const RefPtr& ref, std::nullptr_t ptr) { + return ref.get() == ptr; +} + +template +inline bool operator!=(const RefPtr& ref, std::nullptr_t ptr) { + return ref.get() != ptr; +} + +template +inline bool operator==(std::nullptr_t ptr, const RefPtr& ref) { + return ref.get() == ptr; +} + +template +inline bool operator!=(std::nullptr_t ptr, const RefPtr& ref) { + return ref.get() != ptr; +} + +} diff --git a/ReactAndroid/src/main/jni/first-party/fb/StaticInitialized.h b/ReactAndroid/src/main/jni/first-party/fb/StaticInitialized.h new file mode 100644 index 000000000..6d943972a --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/StaticInitialized.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once +#include +#include + +namespace facebook { + +// Class that lets you declare a global but does not add a static constructor +// to the binary. Eventually I'd like to have this auto-initialize in a +// multithreaded environment but for now it's easiest just to use manual +// initialization. +template +class StaticInitialized { +public: + constexpr StaticInitialized() : + m_instance(nullptr) + {} + + template + void initialize(Args&&... arguments) { + FBASSERT(!m_instance); + m_instance = new T(std::forward(arguments)...); + } + + T* operator->() const { + return m_instance; + } +private: + T* m_instance; +}; + +} diff --git a/ReactAndroid/src/main/jni/first-party/fb/ThreadLocal.h b/ReactAndroid/src/main/jni/first-party/fb/ThreadLocal.h new file mode 100644 index 000000000..d86a2f0de --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/ThreadLocal.h @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#include +#include + +#include + +namespace facebook { + +/////////////////////////////////////////////////////////////////////////////// + +/** + * A thread-local object is a "global" object within a thread. This is useful + * for writing apartment-threaded code, where nothing is actullay shared + * between different threads (hence no locking) but those variables are not + * on stack in local scope. To use it, just do something like this, + * + * ThreadLocal static_object; + * static_object->data_ = ...; + * static_object->doSomething(); + * + * ThreadLocal static_number; + * int value = *static_number; + * + * So, syntax-wise it's similar to pointers. T can be primitive types, and if + * it's a class, there has to be a default constructor. + */ +template +class ThreadLocal { +public: + /** + * Constructor that has to be called from a thread-neutral place. + */ + ThreadLocal() : + m_key(0), + m_cleanup(OnThreadExit) { + initialize(); + } + + /** + * As above but with a custom cleanup function + */ + typedef void (*CleanupFunction)(void* obj); + explicit ThreadLocal(CleanupFunction cleanup) : + m_key(0), + m_cleanup(cleanup) { + FBASSERT(cleanup); + initialize(); + } + + /** + * Access object's member or method through this operator overload. + */ + T *operator->() const { + return get(); + } + + T &operator*() const { + return *get(); + } + + T *get() const { + return (T*)pthread_getspecific(m_key); + } + + T* release() { + T* obj = get(); + pthread_setspecific(m_key, NULL); + return obj; + } + + void reset(T* other = NULL) { + T* old = (T*)pthread_getspecific(m_key); + if (old != other) { + FBASSERT(m_cleanup); + m_cleanup(old); + pthread_setspecific(m_key, other); + } + } + +private: + void initialize() { + int ret = pthread_key_create(&m_key, m_cleanup); + if (ret != 0) { + const char *msg = "(unknown error)"; + switch (ret) { + case EAGAIN: + msg = "PTHREAD_KEYS_MAX (1024) is exceeded"; + break; + case ENOMEM: + msg = "Out-of-memory"; + break; + } + (void) msg; + FBASSERTMSGF(0, "pthread_key_create failed: %d %s", ret, msg); + } + } + + static void OnThreadExit(void *obj) { + if (NULL != obj) { + delete (T*)obj; + } + } + + pthread_key_t m_key; + CleanupFunction m_cleanup; +}; + +} diff --git a/ReactAndroid/src/main/jni/first-party/fb/assert.cpp b/ReactAndroid/src/main/jni/first-party/fb/assert.cpp new file mode 100644 index 000000000..db9a43159 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/assert.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include + +#include +#include + +namespace facebook { + +#define ASSERT_BUF_SIZE 4096 +static char sAssertBuf[ASSERT_BUF_SIZE]; +static AssertHandler gAssertHandler; + +void assertInternal(const char* formatstr ...) { + va_list va_args; + va_start(va_args, formatstr); + vsnprintf(sAssertBuf, sizeof(sAssertBuf), formatstr, va_args); + va_end(va_args); + if (gAssertHandler != NULL) { + gAssertHandler(sAssertBuf); + } + FBLOG(LOG_FATAL, "fbassert", "%s", sAssertBuf); + // crash at this specific address so that we can find our crashes easier + *(int*)0xdeadb00c = 0; + // let the compiler know we won't reach the end of the function + __builtin_unreachable(); +} + +void setAssertHandler(AssertHandler assertHandler) { + gAssertHandler = assertHandler; +} + +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/first-party/fb/include/fb/assert.h b/ReactAndroid/src/main/jni/first-party/fb/include/fb/assert.h new file mode 100644 index 000000000..648ab2cdc --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/include/fb/assert.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef FBASSERT_H +#define FBASSERT_H + +namespace facebook { +#define ENABLE_FBASSERT 1 + +#if ENABLE_FBASSERT +#define FBASSERTMSGF(expr, msg, ...) !(expr) ? facebook::assertInternal("Assert (%s:%d): " msg, __FILE__, __LINE__, ##__VA_ARGS__) : (void) 0 +#else +#define FBASSERTMSGF(expr, msg, ...) +#endif // ENABLE_FBASSERT + +#define FBASSERT(expr) FBASSERTMSGF(expr, "%s", #expr) + +#define FBCRASH(msg, ...) facebook::assertInternal("Fatal error (%s:%d): " msg, __FILE__, __LINE__, ##__VA_ARGS__) +#define FBUNREACHABLE() facebook::assertInternal("This code should be unreachable (%s:%d)", __FILE__, __LINE__) + +void assertInternal(const char* formatstr, ...) __attribute__((noreturn)); + +// This allows storing the assert message before the current process terminates due to a crash +typedef void (*AssertHandler)(const char* message); +void setAssertHandler(AssertHandler assertHandler); + +} // namespace facebook +#endif // FBASSERT_H diff --git a/ReactAndroid/src/main/jni/first-party/fb/include/fb/log.h b/ReactAndroid/src/main/jni/first-party/fb/include/fb/log.h new file mode 100644 index 000000000..4e08b558d --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/include/fb/log.h @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * FB Wrapper for logging functions. + * + * The android logging API uses the macro "LOG()" for its logic, which means + * that it conflicts with random other places that use LOG for their own + * purposes and doesn't work right half the places you include it + * + * FBLOG uses exactly the same semantics (FBLOGD for debug etc) but because of + * the FB prefix it's strictly better. FBLOGV also gets stripped out based on + * whether NDEBUG is set, but can be overridden by FBLOG_NDEBUG + * + * Most of the rest is a copy of with minor changes. + */ + +// +// C/C++ logging functions. See the logging documentation for API details. +// +// We'd like these to be available from C code (in case we import some from +// somewhere), so this has a C interface. +// +// The output will be correct when the log file is shared between multiple +// threads and/or multiple processes so long as the operating system +// supports O_APPEND. These calls have mutex-protected data structures +// and so are NOT reentrant. Do not use LOG in a signal handler. +// +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef ANDROID +#include +#else + // These declarations are needed for our internal use even on non-Android builds. + // (they are borrowed from ) + + /* + * Android log priority values, in ascending priority order. + */ + typedef enum android_LogPriority { + ANDROID_LOG_UNKNOWN = 0, + ANDROID_LOG_DEFAULT, /* only for SetMinPriority() */ + ANDROID_LOG_VERBOSE, + ANDROID_LOG_DEBUG, + ANDROID_LOG_INFO, + ANDROID_LOG_WARN, + ANDROID_LOG_ERROR, + ANDROID_LOG_FATAL, + ANDROID_LOG_SILENT, /* only for SetMinPriority(); must be last */ + } android_LogPriority; + + /* + * Send a simple string to the log. + */ + int __android_log_write(int prio, const char *tag, const char *text); + + /* + * Send a formatted string to the log, used like printf(fmt,...) + */ + int __android_log_print(int prio, const char *tag, const char *fmt, ...) +#if defined(__GNUC__) + __attribute__ ((format(printf, 3, 4))) +#endif + ; + +#endif + +// --------------------------------------------------------------------- + +/* + * Normally we strip FBLOGV (VERBOSE messages) from release builds. + * You can modify this (for example with "#define FBLOG_NDEBUG 0" + * at the top of your source file) to change that behavior. + */ +#ifndef FBLOG_NDEBUG +#ifdef NDEBUG +#define FBLOG_NDEBUG 1 +#else +#define FBLOG_NDEBUG 0 +#endif +#endif + +/* + * This is the local tag used for the following simplified + * logging macros. You can change this preprocessor definition + * before using the other macros to change the tag. + */ +#ifndef LOG_TAG +#define LOG_TAG NULL +#endif + +// --------------------------------------------------------------------- + +/* + * Simplified macro to send a verbose log message using the current LOG_TAG. + */ +#ifndef FBLOGV +#if FBLOG_NDEBUG +#define FBLOGV(...) ((void)0) +#else +#define FBLOGV(...) ((void)FBLOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) +#endif +#endif + +#define CONDITION(cond) (__builtin_expect((cond)!=0, 0)) + +#ifndef FBLOGV_IF +#if FBLOG_NDEBUG +#define FBLOGV_IF(cond, ...) ((void)0) +#else +#define FBLOGV_IF(cond, ...) \ + ( (CONDITION(cond)) \ + ? ((void)FBLOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) \ + : (void)0 ) +#endif +#endif + +/* + * Simplified macro to send a debug log message using the current LOG_TAG. + */ +#ifndef FBLOGD +#define FBLOGD(...) ((void)FBLOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef FBLOGD_IF +#define FBLOGD_IF(cond, ...) \ + ( (CONDITION(cond)) \ + ? ((void)FBLOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) \ + : (void)0 ) +#endif + +/* + * Simplified macro to send an info log message using the current LOG_TAG. + */ +#ifndef FBLOGI +#define FBLOGI(...) ((void)FBLOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef FBLOGI_IF +#define FBLOGI_IF(cond, ...) \ + ( (CONDITION(cond)) \ + ? ((void)FBLOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) \ + : (void)0 ) +#endif + +/* + * Simplified macro to send a warning log message using the current LOG_TAG. + */ +#ifndef FBLOGW +#define FBLOGW(...) ((void)FBLOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef FBLOGW_IF +#define FBLOGW_IF(cond, ...) \ + ( (CONDITION(cond)) \ + ? ((void)FBLOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) \ + : (void)0 ) +#endif + +/* + * Simplified macro to send an error log message using the current LOG_TAG. + */ +#ifndef FBLOGE +#define FBLOGE(...) ((void)FBLOG(LOG_ERROR, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef FBLOGE_IF +#define FBLOGE_IF(cond, ...) \ + ( (CONDITION(cond)) \ + ? ((void)FBLOG(LOG_ERROR, LOG_TAG, __VA_ARGS__)) \ + : (void)0 ) +#endif + +// --------------------------------------------------------------------- + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * verbose priority. + */ +#ifndef IF_FBLOGV +#if FBLOG_NDEBUG +#define IF_FBLOGV() if (false) +#else +#define IF_FBLOGV() IF_FBLOG(LOG_VERBOSE, LOG_TAG) +#endif +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * debug priority. + */ +#ifndef IF_FBLOGD +#define IF_FBLOGD() IF_FBLOG(LOG_DEBUG, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * info priority. + */ +#ifndef IF_FBLOGI +#define IF_FBLOGI() IF_FBLOG(LOG_INFO, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * warn priority. + */ +#ifndef IF_FBLOGW +#define IF_FBLOGW() IF_FBLOG(LOG_WARN, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * error priority. + */ +#ifndef IF_FBLOGE +#define IF_FBLOGE() IF_FBLOG(LOG_ERROR, LOG_TAG) +#endif + + +// --------------------------------------------------------------------- + +/* + * Log a fatal error. If the given condition fails, this stops program + * execution like a normal assertion, but also generating the given message. + * It is NOT stripped from release builds. Note that the condition test + * is -inverted- from the normal assert() semantics. + */ +#define FBLOG_ALWAYS_FATAL_IF(cond, ...) \ + ( (CONDITION(cond)) \ + ? ((void)fb_printAssert(#cond, LOG_TAG, __VA_ARGS__)) \ + : (void)0 ) + +#define FBLOG_ALWAYS_FATAL(...) \ + ( ((void)fb_printAssert(NULL, LOG_TAG, __VA_ARGS__)) ) + +/* + * Versions of LOG_ALWAYS_FATAL_IF and LOG_ALWAYS_FATAL that + * are stripped out of release builds. + */ +#if FBLOG_NDEBUG + +#define FBLOG_FATAL_IF(cond, ...) ((void)0) +#define FBLOG_FATAL(...) ((void)0) + +#else + +#define FBLOG_FATAL_IF(cond, ...) FBLOG_ALWAYS_FATAL_IF(cond, __VA_ARGS__) +#define FBLOG_FATAL(...) FBLOG_ALWAYS_FATAL(__VA_ARGS__) + +#endif + +/* + * Assertion that generates a log message when the assertion fails. + * Stripped out of release builds. Uses the current LOG_TAG. + */ +#define FBLOG_ASSERT(cond, ...) FBLOG_FATAL_IF(!(cond), __VA_ARGS__) +//#define LOG_ASSERT(cond) LOG_FATAL_IF(!(cond), "Assertion failed: " #cond) + +// --------------------------------------------------------------------- + +/* + * Basic log message macro. + * + * Example: + * FBLOG(LOG_WARN, NULL, "Failed with error %d", errno); + * + * The second argument may be NULL or "" to indicate the "global" tag. + */ +#ifndef FBLOG +#define FBLOG(priority, tag, ...) \ + FBLOG_PRI(ANDROID_##priority, tag, __VA_ARGS__) +#endif + +#ifndef FBLOG_BY_DELIMS +#define FBLOG_BY_DELIMS(priority, tag, delims, msg, ...) \ + logPrintByDelims(ANDROID_##priority, tag, delims, msg, ##__VA_ARGS__) +#endif + +/* + * Log macro that allows you to specify a number for the priority. + */ +#ifndef FBLOG_PRI +#define FBLOG_PRI(priority, tag, ...) \ + fb_printLog(priority, tag, __VA_ARGS__) +#endif + +/* + * Log macro that allows you to pass in a varargs ("args" is a va_list). + */ +#ifndef FBLOG_PRI_VA +#define FBLOG_PRI_VA(priority, tag, fmt, args) \ + fb_vprintLog(priority, NULL, tag, fmt, args) +#endif + +/* + * Conditional given a desired logging priority and tag. + */ +#ifndef IF_FBLOG +#define IF_FBLOG(priority, tag) \ + if (fb_testLog(ANDROID_##priority, tag)) +#endif + +typedef void (*LogHandler)(int priority, const char* tag, const char* message); +void setLogHandler(LogHandler logHandler); + +/* + * =========================================================================== + * + * The stuff in the rest of this file should not be used directly. + */ +int fb_printLog(int prio, const char *tag, const char *fmt, ...) +#if defined(__GNUC__) + __attribute__ ((format(printf, 3, 4))) +#endif +; + +#define fb_vprintLog(prio, cond, tag, fmt...) \ + __android_log_vprint(prio, tag, fmt) + +#define fb_printAssert(cond, tag, fmt...) \ + __android_log_assert(cond, tag, fmt) + +#define fb_writeLog(prio, tag, text) \ + __android_log_write(prio, tag, text) + +#define fb_bWriteLog(tag, payload, len) \ + __android_log_bwrite(tag, payload, len) +#define fb_btWriteLog(tag, type, payload, len) \ + __android_log_btwrite(tag, type, payload, len) + +#define fb_testLog(prio, tag) (1) + +/* + * FB extensions + */ +void logPrintByDelims(int priority, const char* tag, const char* delims, + const char* msg, ...); + + +#ifdef __cplusplus +} +#endif + diff --git a/ReactAndroid/src/main/jni/first-party/fb/log.cpp b/ReactAndroid/src/main/jni/first-party/fb/log.cpp new file mode 100644 index 000000000..b58b7ac94 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/log.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include +#include +#include + +#define LOG_BUFFER_SIZE 4096 +static LogHandler gLogHandler; + +void setLogHandler(LogHandler logHandler) { + gLogHandler = logHandler; +} + +int fb_printLog(int prio, const char *tag, const char *fmt, ...) { + char logBuffer[LOG_BUFFER_SIZE]; + + va_list va_args; + va_start(va_args, fmt); + int result = vsnprintf(logBuffer, sizeof(logBuffer), fmt, va_args); + va_end(va_args); + if (gLogHandler != NULL) { + gLogHandler(prio, tag, logBuffer); + } + __android_log_write(prio, tag, logBuffer); + return result; +} + +void logPrintByDelims(int priority, const char* tag, const char* delims, + const char* msg, ...) +{ + va_list ap; + char buf[32768]; + char* context; + char* tok; + + va_start(ap, msg); + vsnprintf(buf, sizeof(buf), msg, ap); + va_end(ap); + + tok = strtok_r(buf, delims, &context); + + if (!tok) { + return; + } + + do { + __android_log_write(priority, tag, tok); + } while ((tok = strtok_r(NULL, delims, &context))); +} + +#ifndef ANDROID + +// Implementations of the basic android logging functions for non-android platforms. + +static char logTagChar(int prio) { + switch (prio) { + default: + case ANDROID_LOG_UNKNOWN: + case ANDROID_LOG_DEFAULT: + case ANDROID_LOG_SILENT: + return ' '; + case ANDROID_LOG_VERBOSE: + return 'V'; + case ANDROID_LOG_DEBUG: + return 'D'; + case ANDROID_LOG_INFO: + return 'I'; + case ANDROID_LOG_WARN: + return 'W'; + case ANDROID_LOG_ERROR: + return 'E'; + case ANDROID_LOG_FATAL: + return 'F'; + } +} + +int __android_log_write(int prio, const char *tag, const char *text) { + return fprintf(stderr, "[%c/%.16s] %s\n", logTagChar(prio), tag, text); +} + +int __android_log_print(int prio, const char *tag, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + + int res = fprintf(stderr, "[%c/%.16s] ", logTagChar(prio), tag); + res += vfprintf(stderr, "%s\n", ap); + + va_end(ap); + return res; +} + +#endif diff --git a/ReactAndroid/src/main/jni/first-party/fb/noncopyable.h b/ReactAndroid/src/main/jni/first-party/fb/noncopyable.h new file mode 100644 index 000000000..7212cc4d0 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/noncopyable.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +namespace facebook { + +struct noncopyable { + noncopyable(const noncopyable&) = delete; + noncopyable& operator=(const noncopyable&) = delete; +protected: + noncopyable() = default; +}; + +} diff --git a/ReactAndroid/src/main/jni/first-party/fb/nonmovable.h b/ReactAndroid/src/main/jni/first-party/fb/nonmovable.h new file mode 100644 index 000000000..37f006a49 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/fb/nonmovable.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +namespace facebook { + +struct nonmovable { + nonmovable(nonmovable&&) = delete; + nonmovable& operator=(nonmovable&&) = delete; +protected: + nonmovable() = default; +}; + +} diff --git a/ReactAndroid/src/main/jni/first-party/jni/ALog.h b/ReactAndroid/src/main/jni/first-party/jni/ALog.h new file mode 100644 index 000000000..0ed1c5fd6 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/ALog.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** @file ALog.h + * + * Very simple android only logging. Define LOG_TAG to enable the macros. + */ + +#pragma once + +#ifdef __ANDROID__ + +#include + +namespace facebook { +namespace alog { + +template +inline void log(int level, const char* tag, const char* msg, ARGS... args) noexcept { + __android_log_print(level, tag, msg, args...); +} + +template +inline void log(int level, const char* tag, const char* msg) noexcept { + __android_log_write(level, tag, msg); +} + +template +inline void logv(const char* tag, const char* msg, ARGS... args) noexcept { + log(ANDROID_LOG_VERBOSE, tag, msg, args...); +} + +template +inline void logd(const char* tag, const char* msg, ARGS... args) noexcept { + log(ANDROID_LOG_DEBUG, tag, msg, args...); +} + +template +inline void logi(const char* tag, const char* msg, ARGS... args) noexcept { + log(ANDROID_LOG_INFO, tag, msg, args...); +} + +template +inline void logw(const char* tag, const char* msg, ARGS... args) noexcept { + log(ANDROID_LOG_WARN, tag, msg, args...); +} + +template +inline void loge(const char* tag, const char* msg, ARGS... args) noexcept { + log(ANDROID_LOG_ERROR, tag, msg, args...); +} + +template +inline void logf(const char* tag, const char* msg, ARGS... args) noexcept { + log(ANDROID_LOG_FATAL, tag, msg, args...); +} + + +#ifdef LOG_TAG +# define ALOGV(...) ::facebook::alog::logv(LOG_TAG, __VA_ARGS__) +# define ALOGD(...) ::facebook::alog::logd(LOG_TAG, __VA_ARGS__) +# define ALOGI(...) ::facebook::alog::logi(LOG_TAG, __VA_ARGS__) +# define ALOGW(...) ::facebook::alog::logw(LOG_TAG, __VA_ARGS__) +# define ALOGE(...) ::facebook::alog::loge(LOG_TAG, __VA_ARGS__) +# define ALOGF(...) ::facebook::alog::logf(LOG_TAG, __VA_ARGS__) +#endif + +}} + +#else +# define ALOGV(...) ((void)0) +# define ALOGD(...) ((void)0) +# define ALOGI(...) ((void)0) +# define ALOGW(...) ((void)0) +# define ALOGE(...) ((void)0) +# define ALOGF(...) ((void)0) +#endif diff --git a/ReactAndroid/src/main/jni/first-party/jni/Android.mk b/ReactAndroid/src/main/jni/first-party/jni/Android.mk new file mode 100644 index 000000000..e77eaf1a0 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Android.mk @@ -0,0 +1,35 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + Countable.cpp \ + Environment.cpp \ + fbjni.cpp \ + jni_helpers.cpp \ + LocalString.cpp \ + OnLoad.cpp \ + WeakReference.cpp \ + fbjni/Exceptions.cpp \ + fbjni/Hybrid.cpp \ + fbjni/References.cpp + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/.. +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/.. + +LOCAL_CFLAGS := -DLOG_TAG=\"fbjni\" -fexceptions -frtti +LOCAL_CFLAGS += -Wall -Werror + +CXX11_FLAGS := -std=gnu++11 +LOCAL_CFLAGS += $(CXX11_FLAGS) + +LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) + +LOCAL_LDLIBS := -landroid + +LOCAL_SHARED_LIBRARIES := libfb + +LOCAL_MODULE := libfbjni + +include $(BUILD_SHARED_LIBRARY) + +$(call import-module,fb) diff --git a/ReactAndroid/src/main/jni/first-party/jni/Countable.cpp b/ReactAndroid/src/main/jni/first-party/jni/Countable.cpp new file mode 100644 index 000000000..6ff7efe90 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Countable.cpp @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include +#include +#include + +namespace facebook { +namespace jni { + +static jfieldID gCountableNativePtr; + +static RefPtr* rawCountableFromJava(JNIEnv* env, jobject obj) { + FBASSERT(obj); + return reinterpret_cast*>(env->GetLongField(obj, gCountableNativePtr)); +} + +const RefPtr& countableFromJava(JNIEnv* env, jobject obj) { + FBASSERT(obj); + return *rawCountableFromJava(env, obj); +} + +void setCountableForJava(JNIEnv* env, jobject obj, RefPtr&& countable) { + int oldValue = env->GetLongField(obj, gCountableNativePtr); + FBASSERTMSGF(oldValue == 0, "Cannot reinitialize object; expected nullptr, got %x", oldValue); + + FBASSERT(countable); + uintptr_t fieldValue = (uintptr_t) new RefPtr(std::move(countable)); + env->SetLongField(obj, gCountableNativePtr, fieldValue); +} + +/** + * NB: THREAD SAFETY (this comment also exists at Countable.java) + * + * This method deletes the corresponding native object on whatever thread the method is called + * on. In the common case when this is called by Countable#finalize(), this will be called on the + * system finalizer thread. If you manually call dispose on the Java object, the native object + * will be deleted synchronously on that thread. + */ +void dispose(JNIEnv* env, jobject obj) { + // Grab the pointer + RefPtr* countable = rawCountableFromJava(env, obj); + if (!countable) { + // That was easy. + return; + } + + // Clear out the old value to avoid double-frees + env->SetLongField(obj, gCountableNativePtr, 0); + + delete countable; +} + +void CountableOnLoad(JNIEnv* env) { + jclass countable = env->FindClass("com/facebook/jni/Countable"); + gCountableNativePtr = env->GetFieldID(countable, "mInstance", "J"); + registerNatives(env, countable, { + { "dispose", "()V", (void*) dispose }, + }); +} + +} } diff --git a/ReactAndroid/src/main/jni/first-party/jni/Countable.h b/ReactAndroid/src/main/jni/first-party/jni/Countable.h new file mode 100644 index 000000000..69460d8a7 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Countable.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once +#include +#include +#include + +namespace facebook { +namespace jni { + +const RefPtr& countableFromJava(JNIEnv* env, jobject obj); + +template RefPtr extractRefPtr(JNIEnv* env, jobject obj) { + return static_cast>(countableFromJava(env, obj)); +} + +template RefPtr extractPossiblyNullRefPtr(JNIEnv* env, jobject obj) { + return obj ? extractRefPtr(env, obj) : nullptr; +} + +void setCountableForJava(JNIEnv* env, jobject obj, RefPtr&& countable); + +void CountableOnLoad(JNIEnv* env); + +} } + diff --git a/ReactAndroid/src/main/jni/first-party/jni/Doxyfile b/ReactAndroid/src/main/jni/first-party/jni/Doxyfile new file mode 100644 index 000000000..8b4df6a7c --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Doxyfile @@ -0,0 +1,18 @@ +PROJECT_NAME = "Facebook JNI" +PROJECT_BRIEF = "Helper library to provide safe and convenient access to JNI with very low overhead" +JAVADOC_AUTOBRIEF = YES +EXTRACT_ALL = YES +RECURSIVE = YES +EXCLUDE = tests Asserts.h Countable.h GlobalReference.h LocalReference.h LocalString.h Registration.h WeakReference.h jni_helpers.h Environment.h +EXCLUDE_PATTERNS = *-inl.h *.cpp +GENERATE_HTML = YES +GENERATE_LATEX = NO +ENABLE_PREPROCESSING = YES +HIDE_UNDOC_MEMBERS = YES +HIDE_SCOPE_NAMES = YES +HIDE_FRIEND_COMPOUNDS = YES +HIDE_UNDOC_CLASSES = YES +SHOW_INCLUDE_FILES = NO +PREDEFINED = LOG_TAG=fbjni +EXAMPLE_PATH = samples +#ENABLED_SECTIONS = INTERNAL diff --git a/ReactAndroid/src/main/jni/first-party/jni/Environment.cpp b/ReactAndroid/src/main/jni/first-party/jni/Environment.cpp new file mode 100644 index 000000000..02b88ce73 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Environment.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include +#include +#include +#include + +namespace facebook { +namespace jni { + +static StaticInitialized> g_env; +static JavaVM* g_vm = nullptr; + +/* static */ +JNIEnv* Environment::current() { + JNIEnv* env = g_env->get(); + if ((env == nullptr) && (g_vm != nullptr)) { + if (g_vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) { + FBLOGE("Error retrieving JNI Environment, thread is probably not attached to JVM"); + env = nullptr; + } else { + g_env->reset(env); + } + } + return env; +} + +/* static */ +void Environment::detachCurrentThread() { + auto env = g_env->get(); + if (env) { + FBASSERT(g_vm); + g_vm->DetachCurrentThread(); + g_env->reset(); + } +} + +struct EnvironmentInitializer { + EnvironmentInitializer(JavaVM* vm) { + FBASSERT(!g_vm); + FBASSERT(vm); + g_vm = vm; + g_env.initialize([] (void*) {}); + } +}; + +/* static */ +void Environment::initialize(JavaVM* vm) { + static EnvironmentInitializer init(vm); +} + +/* static */ +JNIEnv* Environment::ensureCurrentThreadIsAttached() { + auto env = g_env->get(); + if (!env) { + FBASSERT(g_vm); + g_vm->AttachCurrentThread(&env, nullptr); + g_env->reset(env); + } + return env; +} + +ThreadScope::ThreadScope() + : attachedWithThisScope_(false) { + JNIEnv* env = nullptr; + if (g_vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_EDETACHED) { + return; + } + env = facebook::jni::Environment::ensureCurrentThreadIsAttached(); + FBASSERT(env); + attachedWithThisScope_ = true; +} + +ThreadScope::~ThreadScope() { + if (attachedWithThisScope_) { + Environment::detachCurrentThread(); + } +} + +} } + diff --git a/ReactAndroid/src/main/jni/first-party/jni/Environment.h b/ReactAndroid/src/main/jni/first-party/jni/Environment.h new file mode 100644 index 000000000..4dc6966a2 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Environment.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once +#include +#include + +namespace facebook { +namespace jni { + +// Keeps a thread-local reference to the current thread's JNIEnv. +struct Environment { + // May be null if this thread isn't attached to the JVM + static JNIEnv* current(); + static void initialize(JavaVM* vm); + static JNIEnv* ensureCurrentThreadIsAttached(); + static void detachCurrentThread(); +}; + +/** + * RAII Object that attaches a thread to the JVM. Failing to detach from a thread before it + * exits will cause a crash, as will calling Detach an extra time, and this guard class helps + * keep that straight. In addition, it remembers whether it performed the attach or not, so it + * is safe to nest it with itself or with non-fbjni code that manages the attachment correctly. + * + * Potential concerns: + * - Attaching to the JVM is fast (~100us on MotoG), but ideally you would attach while the + * app is not busy. + * - Having a thread detach at arbitrary points is not safe in Dalvik; you need to be sure that + * there is no Java code on the current stack or you run the risk of a crash like: + * ERROR: detaching thread with interp frames (count=18) + * (More detail at https://groups.google.com/forum/#!topic/android-ndk/2H8z5grNqjo) + * ThreadScope won't do a detach if the thread was already attached before the guard is + * instantiated, but there's probably some usage that could trip this up. + * - Newly attached C++ threads only get the bootstrap class loader -- i.e. java language + * classes, not any of our application's classes. This will be different behavior than threads + * that were initiated on the Java side. A workaround is to pass a global reference for a + * class or instance to the new thread; this bypasses the need for the class loader. + * (See http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/invocation.html#attach_current_thread) + */ +class ThreadScope { + public: + ThreadScope(); + ThreadScope(ThreadScope&) = delete; + ThreadScope(ThreadScope&&) = default; + ThreadScope& operator=(ThreadScope&) = delete; + ThreadScope& operator=(ThreadScope&&) = delete; + ~ThreadScope(); + + private: + bool attachedWithThisScope_; +}; + +} } + diff --git a/ReactAndroid/src/main/jni/first-party/jni/GlobalReference.h b/ReactAndroid/src/main/jni/first-party/jni/GlobalReference.h new file mode 100644 index 000000000..55f537ab5 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/GlobalReference.h @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#include +#include + +#include + +#include + +namespace facebook { namespace jni { + +template +class GlobalReference { + static_assert(std::is_convertible::value, + "GlobalReference instantiated with type that is not " + "convertible to jobject"); + + public: + explicit GlobalReference(T globalReference) : + reference_(globalReference? Environment::current()->NewGlobalRef(globalReference) : nullptr) { + } + + ~GlobalReference() { + reset(); + } + + GlobalReference() : + reference_(nullptr) { + } + + // enable move constructor and assignment + GlobalReference(GlobalReference&& rhs) : + reference_(std::move(rhs.reference_)) { + rhs.reference_ = nullptr; + } + + GlobalReference& operator=(GlobalReference&& rhs) { + if (this != &rhs) { + reset(); + reference_ = std::move(rhs.reference_); + rhs.reference_ = nullptr; + } + return *this; + } + + GlobalReference(const GlobalReference& rhs) : + reference_{} { + reset(rhs.get()); + } + + GlobalReference& operator=(const GlobalReference& rhs) { + if (this == &rhs) { + return *this; + } + reset(rhs.get()); + return *this; + } + + explicit operator bool() const { + return (reference_ != nullptr); + } + + T get() const { + return reinterpret_cast(reference_); + } + + void reset(T globalReference = nullptr) { + if (reference_) { + Environment::current()->DeleteGlobalRef(reference_); + } + if (globalReference) { + reference_ = Environment::current()->NewGlobalRef(globalReference); + } else { + reference_ = nullptr; + } + } + + private: + jobject reference_; +}; + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/LocalReference.h b/ReactAndroid/src/main/jni/first-party/jni/LocalReference.h new file mode 100644 index 000000000..b5d9c54a1 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/LocalReference.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#include +#include + +#include + +#include + +namespace facebook { +namespace jni { + +template +struct LocalReferenceDeleter { + static_assert(std::is_convertible::value, + "LocalReferenceDeleter instantiated with type that is not convertible to jobject"); + void operator()(T localReference) { + if (localReference != nullptr) { + Environment::current()->DeleteLocalRef(localReference); + } + } + }; + +template +using LocalReference = + std::unique_ptr::type, LocalReferenceDeleter>; + +} } diff --git a/ReactAndroid/src/main/jni/first-party/jni/LocalString.cpp b/ReactAndroid/src/main/jni/first-party/jni/LocalString.cpp new file mode 100644 index 000000000..5fc8d0c84 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/LocalString.cpp @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include +#include + +#include + +namespace facebook { +namespace jni { + +namespace { + +inline void encode3ByteUTF8(char32_t code, uint8_t* out) { + FBASSERTMSGF((code & 0xffff0000) == 0, "3 byte utf-8 encodings only valid for up to 16 bits"); + + out[0] = 0xE0 | (code >> 12); + out[1] = 0x80 | ((code >> 6) & 0x3F); + out[2] = 0x80 | (code & 0x3F); +} + +inline char32_t decode3ByteUTF8(const uint8_t* in) { + return (((in[0] & 0x0f) << 12) | + ((in[1] & 0x3f) << 6) | + ( in[2] & 0x3f)); +} + +inline void encode4ByteUTF8(char32_t code, std::string& out, size_t offset) { + FBASSERTMSGF((code & 0xfff80000) == 0, "4 byte utf-8 encodings only valid for up to 21 bits"); + + out[offset] = (char) (0xF0 | (code >> 18)); + out[offset + 1] = (char) (0x80 | ((code >> 12) & 0x3F)); + out[offset + 2] = (char) (0x80 | ((code >> 6) & 0x3F)); + out[offset + 3] = (char) (0x80 | (code & 0x3F)); +} + +template +inline bool isFourByteUTF8Encoding(const T* utf8) { + return ((*utf8 & 0xF8) == 0xF0); +} + +} + +namespace detail { + +size_t modifiedLength(const std::string& str) { + // Scan for supplementary characters + size_t j = 0; + for (size_t i = 0; i < str.size(); ) { + if (str[i] == 0) { + i += 1; + j += 2; + } else if (i + 4 > str.size() || + !isFourByteUTF8Encoding(&(str[i]))) { + // See the code in utf8ToModifiedUTF8 for what's happening here. + i += 1; + j += 1; + } else { + i += 4; + j += 6; + } + } + + return j; +} + +// returns modified utf8 length; *length is set to strlen(str) +size_t modifiedLength(const uint8_t* str, size_t* length) { + // NUL-terminated: Scan for length and supplementary characters + size_t i = 0; + size_t j = 0; + while (str[i] != 0) { + if (str[i + 1] == 0 || + str[i + 2] == 0 || + str[i + 3] == 0 || + !isFourByteUTF8Encoding(&(str[i]))) { + i += 1; + j += 1; + } else { + i += 4; + j += 6; + } + } + + *length = i; + return j; +} + +void utf8ToModifiedUTF8(const uint8_t* utf8, size_t len, uint8_t* modified, size_t modifiedBufLen) +{ + size_t j = 0; + for (size_t i = 0; i < len; ) { + FBASSERTMSGF(j < modifiedBufLen, "output buffer is too short"); + if (utf8[i] == 0) { + FBASSERTMSGF(j + 1 < modifiedBufLen, "output buffer is too short"); + modified[j] = 0xc0; + modified[j + 1] = 0x80; + i += 1; + j += 2; + continue; + } + + if (i + 4 > len || + !isFourByteUTF8Encoding(utf8 + i)) { + // If the input is too short for this to be a four-byte + // encoding, or it isn't one for real, just copy it on through. + modified[j] = utf8[i]; + i++; + j++; + continue; + } + + // Convert 4 bytes of input to 2 * 3 bytes of output + char32_t code = (((utf8[i] & 0x07) << 18) | + ((utf8[i + 1] & 0x3f) << 12) | + ((utf8[i + 2] & 0x3f) << 6) | + ( utf8[i + 3] & 0x3f)); + char32_t first; + char32_t second; + + if (code > 0x10ffff) { + // These could be valid utf-8, but cannot be represented as modified UTF-8, due to the 20-bit + // limit on that representation. Encode two replacement characters, so the expected output + // length lines up. + const char32_t kUnicodeReplacementChar = 0xfffd; + first = kUnicodeReplacementChar; + second = kUnicodeReplacementChar; + } else { + // split into surrogate pair + first = ((code - 0x010000) >> 10) | 0xd800; + second = ((code - 0x010000) & 0x3ff) | 0xdc00; + } + + // encode each as a 3 byte surrogate value + FBASSERTMSGF(j + 5 < modifiedBufLen, "output buffer is too short"); + encode3ByteUTF8(first, modified + j); + encode3ByteUTF8(second, modified + j + 3); + i += 4; + j += 6; + } + + FBASSERTMSGF(j < modifiedBufLen, "output buffer is too short"); + modified[j++] = '\0'; +} + +std::string modifiedUTF8ToUTF8(const uint8_t* modified, size_t len) { + // Converting from modified utf8 to utf8 will always shrink, so this will always be sufficient + std::string utf8(len, 0); + size_t j = 0; + for (size_t i = 0; i < len; ) { + // surrogate pair: 1101 10xx xxxx xxxx 1101 11xx xxxx xxxx + // encoded pair: 1110 1101 1010 xxxx 10xx xxxx 1110 1101 1011 xxxx 10xx xxxx + + if (len >= i + 6 && + modified[i] == 0xed && + (modified[i + 1] & 0xf0) == 0xa0 && + modified[i + 3] == 0xed && + (modified[i + 4] & 0xf0) == 0xb0) { + // Valid surrogate pair + char32_t pair1 = decode3ByteUTF8(modified + i); + char32_t pair2 = decode3ByteUTF8(modified + i + 3); + char32_t ch = 0x10000 + (((pair1 & 0x3ff) << 10) | + ( pair2 & 0x3ff)); + encode4ByteUTF8(ch, utf8, j); + i += 6; + j += 4; + continue; + } else if (len >= i + 2 && + modified[i] == 0xc0 && + modified[i + 1] == 0x80) { + utf8[j] = 0; + i += 2; + j += 1; + continue; + } + + // copy one byte. This might be a one, two, or three-byte encoding. It might be an invalid + // encoding of some sort, but garbage in garbage out is ok. + + utf8[j] = (char) modified[i]; + i++; + j++; + } + + utf8.resize(j); + + return utf8; +} + +} + +LocalString::LocalString(const std::string& str) +{ + size_t modlen = detail::modifiedLength(str); + if (modlen == str.size()) { + // no supplementary characters, build jstring from input buffer + m_string = Environment::current()->NewStringUTF(str.data()); + return; + } + auto modified = std::vector(modlen + 1); // allocate extra byte for \0 + detail::utf8ToModifiedUTF8( + reinterpret_cast(str.data()), str.size(), + reinterpret_cast(modified.data()), modified.size()); + m_string = Environment::current()->NewStringUTF(modified.data()); +} + +LocalString::LocalString(const char* str) +{ + size_t len; + size_t modlen = detail::modifiedLength(reinterpret_cast(str), &len); + if (modlen == len) { + // no supplementary characters, build jstring from input buffer + m_string = Environment::current()->NewStringUTF(str); + return; + } + auto modified = std::vector(modlen + 1); // allocate extra byte for \0 + detail::utf8ToModifiedUTF8( + reinterpret_cast(str), len, + reinterpret_cast(modified.data()), modified.size()); + m_string = Environment::current()->NewStringUTF(modified.data()); +} + +LocalString::~LocalString() { + Environment::current()->DeleteLocalRef(m_string); +} + +std::string fromJString(JNIEnv* env, jstring str) { + const char* modified = env->GetStringUTFChars(str, NULL); + jsize length = env->GetStringUTFLength(str); + std::string s = detail::modifiedUTF8ToUTF8(reinterpret_cast(modified), length); + env->ReleaseStringUTFChars(str, modified); + return s; +} + +} } diff --git a/ReactAndroid/src/main/jni/first-party/jni/LocalString.h b/ReactAndroid/src/main/jni/first-party/jni/LocalString.h new file mode 100644 index 000000000..a85efa48a --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/LocalString.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once +#include +#include + +namespace facebook { +namespace jni { + +namespace detail { + +void utf8ToModifiedUTF8(const uint8_t* bytes, size_t len, uint8_t* modified, size_t modifiedLength); +size_t modifiedLength(const std::string& str); +size_t modifiedLength(const uint8_t* str, size_t* length); +std::string modifiedUTF8ToUTF8(const uint8_t* modified, size_t len); + +} + +// JNI represents strings encoded with modified version of UTF-8. The difference between UTF-8 and +// Modified UTF-8 is that the latter support only 1-byte, 2-byte, and 3-byte formats. Supplementary +// character (4 bytes in unicode) needs to be represented in the form of surrogate pairs. To create +// a Modified UTF-8 surrogate pair that Dalvik would understand we take 4-byte unicode character, +// encode it with UTF-16 which gives us two 2 byte chars (surrogate pair) and then we encode each +// pair as UTF-8. This result in 2 x 3 byte characters. To convert modified UTF-8 to standard +// UTF-8, this mus tbe reversed. +// +// The second difference is that Modified UTF-8 is encoding NUL byte in 2-byte format. +// +// In order to avoid complex error handling, only a minimum of validity checking is done to avoid +// crashing. If the input is invalid, the output may be invalid as well. +// +// Relevant links: +// - http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html +// - https://docs.oracle.com/javase/6/docs/api/java/io/DataInput.html#modified-utf-8 + +class LocalString { +public: + // Assumes UTF8 encoding and make a required convertion to modified UTF-8 when the string + // contains unicode supplementary characters. + explicit LocalString(const std::string& str); + explicit LocalString(const char* str); + jstring string() const { + return m_string; + } + ~LocalString(); +private: + jstring m_string; +}; + +// The string from JNI is converted to standard UTF-8 if the string contains supplementary +// characters. +std::string fromJString(JNIEnv* env, jstring str); + +} } diff --git a/ReactAndroid/src/main/jni/first-party/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/first-party/jni/OnLoad.cpp new file mode 100644 index 000000000..a4c404090 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/OnLoad.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include +#include +#include +#include + +using namespace facebook::jni; + +JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { + return facebook::jni::initialize(vm, [] { + CountableOnLoad(Environment::current()); + HybridDataOnLoad(); + }); +} diff --git a/ReactAndroid/src/main/jni/first-party/jni/Registration.h b/ReactAndroid/src/main/jni/first-party/jni/Registration.h new file mode 100644 index 000000000..243a94788 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/Registration.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once +#include +#include +#include + +namespace facebook { +namespace jni { + +static inline void registerNatives(JNIEnv* env, jclass cls, std::initializer_list methods) { + auto result = env->RegisterNatives(cls, methods.begin(), methods.size()); + FBASSERT(result == 0); +} + +static inline void registerNatives(JNIEnv* env, const char* cls, std::initializer_list list) { + registerNatives(env, env->FindClass(cls), list); +} + +} } diff --git a/ReactAndroid/src/main/jni/first-party/jni/WeakReference.cpp b/ReactAndroid/src/main/jni/first-party/jni/WeakReference.cpp new file mode 100644 index 000000000..d87ea33c0 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/WeakReference.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include +#include + +namespace facebook { +namespace jni { + +WeakReference::WeakReference(jobject strongRef) : + m_weakReference(Environment::current()->NewWeakGlobalRef(strongRef)) +{ +} + +WeakReference::~WeakReference() { + auto env = Environment::current(); + FBASSERTMSGF(env, "Attempt to delete jni::WeakReference from non-JNI thread"); + env->DeleteWeakGlobalRef(m_weakReference); +} + +ResolvedWeakReference::ResolvedWeakReference(jobject weakRef) : + m_strongReference(Environment::current()->NewLocalRef(weakRef)) +{ +} + +ResolvedWeakReference::ResolvedWeakReference(const RefPtr& weakRef) : + m_strongReference(Environment::current()->NewLocalRef(weakRef->weakRef())) +{ +} + +ResolvedWeakReference::~ResolvedWeakReference() { + if (m_strongReference) + Environment::current()->DeleteLocalRef(m_strongReference); +} + +} } + diff --git a/ReactAndroid/src/main/jni/first-party/jni/WeakReference.h b/ReactAndroid/src/main/jni/first-party/jni/WeakReference.h new file mode 100644 index 000000000..2723155fe --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/WeakReference.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once +#include +#include +#include +#include + +namespace facebook { +namespace jni { + +class WeakReference : public Countable { +public: + typedef RefPtr Ptr; + WeakReference(jobject strongRef); + ~WeakReference(); + jweak weakRef() { + return m_weakReference; + } + +private: + jweak m_weakReference; +}; + +// This class is intended to take a weak reference and turn it into a strong +// local reference. Consequently, it should only be allocated on the stack. +class ResolvedWeakReference : public noncopyable { +public: + ResolvedWeakReference(jobject weakRef); + ResolvedWeakReference(const RefPtr& weakRef); + ~ResolvedWeakReference(); + + operator jobject () { + return m_strongReference; + } + + explicit operator bool () { + return m_strongReference != nullptr; + } + +private: + jobject m_strongReference; +}; + +} } + diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni.cpp b/ReactAndroid/src/main/jni/first-party/jni/fbjni.cpp new file mode 100644 index 000000000..fb80b4553 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni.cpp @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "fbjni.h" + +#include +#include +#include + +namespace facebook { +namespace jni { + +template +static void log(Args... args) { +// TODO (7623232) Migrate to glog +#ifdef __ANDROID__ + facebook::alog::loge("fbjni", args...); +#endif +} + +jint initialize(JavaVM* vm, void(*init_fn)()) noexcept { + static std::once_flag init_flag; + static auto failed = false; + + std::call_once(init_flag, [vm] { + try { + Environment::initialize(vm); + internal::initExceptionHelpers(); + } catch (std::exception& ex) { + log("Failed to initialize fbjni: %s", ex.what()); + failed = true; + } catch (...) { + log("Failed to initialize fbjni"); + failed = true; + } + }); + + if (failed) { + return JNI_ERR; + } + + try { + init_fn(); + } catch (...) { + translatePendingCppExceptionToJavaException(); + // So Java will handle the translated exception, fall through and + // return a good version number. + } + return JNI_VERSION_1_6; +} + +alias_ref findClassStatic(const char* name) { + const auto env = internal::getEnv(); + auto cls = env->FindClass(name); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!cls); + auto leaking_ref = (jclass)env->NewGlobalRef(cls); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!leaking_ref); + return wrap_alias(leaking_ref); +} + +local_ref findClassLocal(const char* name) { + const auto env = internal::getEnv(); + auto cls = env->FindClass(name); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!cls); + return adopt_local(cls); +} + + +// jstring ///////////////////////////////////////////////////////////////////////////////////////// + +std::string JObjectWrapper::toStdString() const { + const auto env = internal::getEnv(); + auto modified = env->GetStringUTFChars(self(), nullptr); + auto length = env->GetStringUTFLength(self()); + auto string = detail::modifiedUTF8ToUTF8(reinterpret_cast(modified), length); + env->ReleaseStringUTFChars(self(), modified); + return string; +} + +local_ref make_jstring(const char* utf8) { + if (!utf8) { + return {}; + } + const auto env = internal::getEnv(); + size_t len; + size_t modlen = detail::modifiedLength(reinterpret_cast(utf8), &len); + jstring result; + if (modlen == len) { + // The only difference between utf8 and modifiedUTF8 is in encoding 4-byte UTF8 chars + // and '\0' that is encoded on 2 bytes. + // + // Since modifiedUTF8-encoded string can be no shorter than it's UTF8 conterpart we + // know that if those two strings are of the same length we don't need to do any + // conversion -> no 4-byte chars nor '\0'. + result = env->NewStringUTF(utf8); + } else { + auto modified = std::vector(modlen + 1); // allocate extra byte for \0 + detail::utf8ToModifiedUTF8( + reinterpret_cast(utf8), len, + reinterpret_cast(modified.data()), modified.size()); + result = env->NewStringUTF(modified.data()); + } + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return adopt_local(result); +} + + +// PinnedPrimitiveArray /////////////////////////////////////////////////////////////////////////// + +// TODO(T7847300): Allow array to be specified as constant so that JNI_ABORT can be passed +// on release, as opposed to 0, which results in unnecessary copying. +#pragma push_macro("DEFINE_PRIMITIVE_METHODS") +#undef DEFINE_PRIMITIVE_METHODS +#define DEFINE_PRIMITIVE_METHODS(TYPE, NAME) \ +template<> \ +TYPE* PinnedPrimitiveArray::get() { \ + FACEBOOK_JNI_THROW_EXCEPTION_IF(array_.get() == nullptr); \ + const auto env = internal::getEnv(); \ + elements_ = env->Get ## NAME ## ArrayElements( \ + static_cast(array_.get()), &isCopy_); \ + size_ = array_->size(); \ + return elements_; \ +} \ +template<> \ +void PinnedPrimitiveArray::release() { \ + FACEBOOK_JNI_THROW_EXCEPTION_IF(array_.get() == nullptr); \ + const auto env = internal::getEnv(); \ + env->Release ## NAME ## ArrayElements( \ + static_cast(array_.get()), elements_, 0); \ + elements_ = nullptr; \ + size_ = 0; \ +} + +DEFINE_PRIMITIVE_METHODS(jboolean, Boolean) +DEFINE_PRIMITIVE_METHODS(jbyte, Byte) +DEFINE_PRIMITIVE_METHODS(jchar, Char) +DEFINE_PRIMITIVE_METHODS(jshort, Short) +DEFINE_PRIMITIVE_METHODS(jint, Int) +DEFINE_PRIMITIVE_METHODS(jlong, Long) +DEFINE_PRIMITIVE_METHODS(jfloat, Float) +DEFINE_PRIMITIVE_METHODS(jdouble, Double) +#pragma pop_macro("DEFINE_PRIMITIVE_METHODS") + + +#define DEFINE_PRIMITIVE_ARRAY_UTILS(TYPE, NAME) \ +local_ref make_ ## TYPE ## _array(jsize size) { \ + auto array = internal::getEnv()->New ## NAME ## Array(size); \ + FACEBOOK_JNI_THROW_EXCEPTION_IF(!array); \ + return adopt_local(array); \ +} \ + \ +j ## TYPE* \ +JObjectWrapper::getRegion(jsize start, jsize length, j ## TYPE* buf) { \ + internal::getEnv()->Get ## NAME ## ArrayRegion(self(), start, length, buf); \ + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); \ + return buf; \ +} \ + \ +std::unique_ptr \ +JObjectWrapper::getRegion(jsize start, jsize length) { \ + auto buf = std::unique_ptr{new j ## TYPE[length]}; \ + internal::getEnv()->Get ## NAME ## ArrayRegion(self(), start, length, buf.get()); \ + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); \ + return buf; \ +} \ + \ +void JObjectWrapper::setRegion(jsize start, jsize length, j ## TYPE* buf) { \ + internal::getEnv()->Set ## NAME ## ArrayRegion(self(), start, length, buf); \ + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); \ +} \ + \ +PinnedPrimitiveArray JObjectWrapper::pin() { \ + return PinnedPrimitiveArray{self()}; \ +} \ + +DEFINE_PRIMITIVE_ARRAY_UTILS(boolean, Boolean) +DEFINE_PRIMITIVE_ARRAY_UTILS(byte, Byte) +DEFINE_PRIMITIVE_ARRAY_UTILS(char, Char) +DEFINE_PRIMITIVE_ARRAY_UTILS(short, Short) +DEFINE_PRIMITIVE_ARRAY_UTILS(int, Int) +DEFINE_PRIMITIVE_ARRAY_UTILS(long, Long) +DEFINE_PRIMITIVE_ARRAY_UTILS(float, Float) +DEFINE_PRIMITIVE_ARRAY_UTILS(double, Double) + + +// Internal debug ///////////////////////////////////////////////////////////////////////////////// + +namespace internal { +ReferenceStats g_reference_stats; + +void facebook::jni::internal::ReferenceStats::reset() noexcept { + locals_deleted = globals_deleted = weaks_deleted = 0; +} +} + +}} + diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni.h new file mode 100644 index 000000000..7ea816b60 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#include + +#include "Environment.h" +#include "ALog.h" +#include "fbjni/Common.h" +#include "fbjni/Exceptions.h" +#include "fbjni/ReferenceAllocators.h" +#include "fbjni/References.h" +#include "fbjni/Meta.h" +#include "fbjni/CoreClasses.h" +#include "fbjni/Hybrid.h" +#include "fbjni/Registration.h" diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Common.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Common.h new file mode 100644 index 000000000..50111ef83 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Common.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** @file Common.h + * + * Defining the stuff that don't deserve headers of their own... + */ + +#pragma once + +#include + +#include "../Environment.h" +#include "../ALog.h" + +/// @cond INTERNAL + +namespace facebook { +namespace jni { + +/** + * This needs to be called at library load time, typically in your JNI_OnLoad method. + * + * The intended use is to return the result of initialize() directly + * from JNI_OnLoad and to do nothing else there. Library specific + * initialization code should go in the function passed to initialize + * (which can be, and probably should be, a C++ lambda). This approach + * provides correct error handling and translation errors during + * initialization into Java exceptions when appropriate. + * + * Failure to call this will cause your code to crash in a remarkably + * unhelpful way (typically a segfault) while trying to handle an exception + * which occurs later. + */ +jint initialize(JavaVM*, void(*)()) noexcept; + +namespace internal { + +/** + * Retrieve a pointer the JNI environment of the current thread. + * + * @pre The current thread must be attached to the VM + */ +inline JNIEnv* getEnv() noexcept { + // TODO(T6594868) Benchmark against raw JNI access + return Environment::current(); +} + +// Define to get extremely verbose logging of references and to enable reference stats +#if defined(__ANDROID__) && defined(FBJNI_DEBUG_REFS) +template +inline void dbglog(Args... args) noexcept { + facebook::alog::logv("fbjni_ref", args...); +} +#else +template +inline void dbglog(Args...) noexcept {} +#endif + +}}} + +/// @endcond diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses-inl.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses-inl.h new file mode 100644 index 000000000..c52c32306 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses-inl.h @@ -0,0 +1,451 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#include +#include + +#include "Common.h" +#include "Exceptions.h" + +namespace facebook { +namespace jni { + +inline bool isSameObject(alias_ref lhs, alias_ref rhs) noexcept { + return internal::getEnv()->IsSameObject(lhs.get(), rhs.get()) != JNI_FALSE; +} + + +// jobject ///////////////////////////////////////////////////////////////////////////////////////// + +inline JObjectWrapper::JObjectWrapper(jobject reference) noexcept + : this_{reference} +{} + +inline JObjectWrapper::JObjectWrapper(const JObjectWrapper& other) noexcept + : this_{other.this_} { + internal::dbglog("wrapper copy from this=%p ref=%p other=%p", this, other.this_, &other); +} + +inline local_ref JObjectWrapper::getClass() const noexcept { + return adopt_local(internal::getEnv()->GetObjectClass(self())); +} + +inline bool JObjectWrapper::isInstanceOf(alias_ref cls) const noexcept { + return internal::getEnv()->IsInstanceOf(self(), cls.get()) != JNI_FALSE; +} + +template +inline T JObjectWrapper::getFieldValue(JField field) const noexcept { + return field.get(self()); +} + +template +inline local_ref JObjectWrapper::getFieldValue(JField field) noexcept { + return adopt_local(field.get(self())); +} + +template +inline void JObjectWrapper::setFieldValue(JField field, T value) noexcept { + field.set(self(), value); +} + +inline std::string JObjectWrapper::toString() const { + static auto method = findClassLocal("java/lang/Object")->getMethod("toString"); + + return method(self())->toStdString(); +} + +inline void JObjectWrapper::set(jobject reference) noexcept { + this_ = reference; +} + +inline jobject JObjectWrapper::get() const noexcept { + return this_; +} + +inline jobject JObjectWrapper::self() const noexcept { + return this_; +} + +inline void swap(JObjectWrapper& a, JObjectWrapper& b) noexcept { + using std::swap; + swap(a.this_, b.this_); +} + + +// jclass ////////////////////////////////////////////////////////////////////////////////////////// + +namespace detail { + +// This is not a real type. It is used so people won't accidentally +// use a void* to initialize a NativeMethod. +struct NativeMethodWrapper; + +}; + +struct NativeMethod { + const char* name; + std::string descriptor; + detail::NativeMethodWrapper* wrapper; +}; + +inline local_ref JObjectWrapper::getSuperclass() const noexcept { + return adopt_local(internal::getEnv()->GetSuperclass(self())); +} + +inline void JObjectWrapper::registerNatives(std::initializer_list methods) { + const auto env = internal::getEnv(); + + JNINativeMethod jnimethods[methods.size()]; + size_t i = 0; + for (auto it = methods.begin(); it < methods.end(); ++it, ++i) { + jnimethods[i].name = it->name; + jnimethods[i].signature = it->descriptor.c_str(); + jnimethods[i].fnPtr = reinterpret_cast(it->wrapper); + } + + auto result = env->RegisterNatives(self(), jnimethods, methods.size()); + FACEBOOK_JNI_THROW_EXCEPTION_IF(result != JNI_OK); +} + +inline bool JObjectWrapper::isAssignableFrom(alias_ref other) const noexcept { + const auto env = internal::getEnv(); + const auto result = env->IsAssignableFrom(self(), other.get()); + return result; +} + +template +inline JConstructor JObjectWrapper::getConstructor() const { + return getConstructor(jmethod_traits::constructor_descriptor().c_str()); +} + +template +inline JConstructor JObjectWrapper::getConstructor(const char* descriptor) const { + constexpr auto constructor_method_name = ""; + return getMethod(constructor_method_name, descriptor); +} + +template +inline JMethod JObjectWrapper::getMethod(const char* name) const { + return getMethod(name, jmethod_traits::descriptor().c_str()); +} + +template +inline JMethod JObjectWrapper::getMethod( + const char* name, + const char* descriptor) const { + const auto env = internal::getEnv(); + const auto method = env->GetMethodID(self(), name, descriptor); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!method); + return JMethod{method}; +} + +template +inline JStaticMethod JObjectWrapper::getStaticMethod(const char* name) const { + return getStaticMethod(name, jmethod_traits::descriptor().c_str()); +} + +template +inline JStaticMethod JObjectWrapper::getStaticMethod( + const char* name, + const char* descriptor) const { + const auto env = internal::getEnv(); + const auto method = env->GetStaticMethodID(self(), name, descriptor); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!method); + return JStaticMethod{method}; +} + +template +inline JNonvirtualMethod JObjectWrapper::getNonvirtualMethod(const char* name) const { + return getNonvirtualMethod(name, jmethod_traits::descriptor().c_str()); +} + +template +inline JNonvirtualMethod JObjectWrapper::getNonvirtualMethod( + const char* name, + const char* descriptor) const { + const auto env = internal::getEnv(); + const auto method = env->GetMethodID(self(), name, descriptor); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!method); + return JNonvirtualMethod{method}; +} + +template +inline JField(), T>> +JObjectWrapper::getField(const char* name) const { + return getField(name, jtype_traits::descriptor().c_str()); +} + +template +inline JField(), T>> JObjectWrapper::getField( + const char* name, + const char* descriptor) const { + const auto env = internal::getEnv(); + auto field = env->GetFieldID(self(), name, descriptor); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!field); + return JField{field}; +} + +template +inline JStaticField(), T>> JObjectWrapper::getStaticField( + const char* name) const { + return getStaticField(name, jtype_traits::descriptor().c_str()); +} + +template +inline JStaticField(), T>> JObjectWrapper::getStaticField( + const char* name, + const char* descriptor) const { + const auto env = internal::getEnv(); + auto field = env->GetStaticFieldID(self(), name, descriptor); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!field); + return JStaticField{field}; +} + +template +inline T JObjectWrapper::getStaticFieldValue(JStaticField field) const noexcept { + return field.get(self()); +} + +template +inline local_ref JObjectWrapper::getStaticFieldValue(JStaticField field) noexcept { + return adopt_local(field.get(self())); +} + +template +inline void JObjectWrapper::setStaticFieldValue(JStaticField field, T value) noexcept { + field.set(self(), value); +} + +template +inline local_ref JObjectWrapper::newObject( + JConstructor constructor, + Args... args) const { + const auto env = internal::getEnv(); + auto object = env->NewObject(self(), constructor.getId(), args...); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!object); + return adopt_local(static_cast(object)); +} + +inline jclass JObjectWrapper::self() const noexcept { + return static_cast(this_); +} + +inline void registerNatives(const char* name, std::initializer_list methods) { + findClassLocal(name)->registerNatives(methods); +} + + +// jstring ///////////////////////////////////////////////////////////////////////////////////////// + +inline local_ref make_jstring(const std::string& modifiedUtf8) { + return make_jstring(modifiedUtf8.c_str()); +} + +inline jstring JObjectWrapper::self() const noexcept { + return static_cast(this_); +} + + +// jthrowable ////////////////////////////////////////////////////////////////////////////////////// + +inline jthrowable JObjectWrapper::self() const noexcept { + return static_cast(this_); +} + + +// jtypeArray ////////////////////////////////////////////////////////////////////////////////////// +template +inline ElementProxy::ElementProxy( + JObjectWrapper<_jtypeArray*>* target, + size_t idx) + : target_{target}, idx_{idx} {} + +template +inline ElementProxy& ElementProxy::operator=(const T& o) { + target_->setElement(idx_, o); + return *this; +} + +template +inline ElementProxy& ElementProxy::operator=(alias_ref& o) { + target_->setElement(idx_, o.get()); + return *this; +} + +template +inline ElementProxy& ElementProxy::operator=(alias_ref&& o) { + target_->setElement(idx_, o.get()); + return *this; +} + +template +inline ElementProxy& ElementProxy::operator=(const ElementProxy& o) { + auto src = o.target_->getElement(o.idx_); + target_->setElement(idx_, src.get()); + return *this; +} + +template +inline ElementProxy::ElementProxy::operator const local_ref () const { + return target_->getElement(idx_); +} + +template +inline ElementProxy::ElementProxy::operator local_ref () { + return target_->getElement(idx_); +} + +template +inline std::string JObjectWrapper>::bareClassName() { + // Use the initializer to strip off the leading and trailing character. + const char* className = JObjectWrapper::kJavaDescriptor; + return std::string(className + 1, strlen(className) - 2); +} + +template +local_ref> JObjectWrapper>::newArray(size_t size) { + static auto elementClass = findClassStatic( + JObjectWrapper>::bareClassName().c_str()); + const auto env = internal::getEnv(); + auto rawArray = env->NewObjectArray(size, elementClass.get(), nullptr); + FACEBOOK_JNI_THROW_EXCEPTION_IF(!rawArray); + return adopt_local(static_cast>(rawArray)); +} + +template +inline void JObjectWrapper>::setElement(size_t idx, const T& value) { + const auto env = internal::getEnv(); + env->SetObjectArrayElement(static_cast(self()), idx, value); +} + +template +inline local_ref JObjectWrapper>::getElement(size_t idx) { + const auto env = internal::getEnv(); + auto rawElement = env->GetObjectArrayElement(static_cast(self()), idx); + return adopt_local(static_cast(rawElement)); +} + +template +inline size_t JObjectWrapper>::size() { + const auto env = internal::getEnv(); + return env->GetArrayLength(static_cast(self())); +} + +template +inline ElementProxy JObjectWrapper>::operator[](size_t index) { + return ElementProxy(this, index); +} + +template +inline jtypeArray JObjectWrapper>::self() const noexcept { + return static_cast>(this_); +} + + +// jarray ///////////////////////////////////////////////////////////////////////////////////////// + +inline size_t JObjectWrapper::size() const noexcept { + const auto env = internal::getEnv(); + return env->GetArrayLength(self()); +} + +inline jarray JObjectWrapper::self() const noexcept { + return static_cast(this_); +} + + +// PinnedPrimitiveArray /////////////////////////////////////////////////////////////////////////// + +template +inline PinnedPrimitiveArray::PinnedPrimitiveArray(alias_ref array) noexcept + : array_{array} { + get(); +} + +template +PinnedPrimitiveArray::PinnedPrimitiveArray(PinnedPrimitiveArray&& o) noexcept { + array_ = std::move(o.array_); + elements_ = o.elements_; + isCopy_ = o.isCopy_; + size_ = o.size_; + o.elements_ = nullptr; + o.isCopy_ = false; + o.size_ = 0; +} + +template +PinnedPrimitiveArray& +PinnedPrimitiveArray::operator=(PinnedPrimitiveArray&& o) noexcept { + array_ = std::move(o.array_); + elements_ = o.elements_; + isCopy_ = o.isCopy_; + size_ = o.size_; + o.elements_ = nullptr; + o.isCopy_ = false; + o.size_ = 0; + return *this; +} + +template +inline T& PinnedPrimitiveArray::operator[](size_t index) { + FACEBOOK_JNI_THROW_EXCEPTION_IF(elements_ == nullptr); + return elements_[index]; +} + +template +inline bool PinnedPrimitiveArray::isCopy() const noexcept { + return isCopy_ == JNI_TRUE; +} + +template +inline size_t PinnedPrimitiveArray::size() const noexcept { + return size_; +} + +template +inline PinnedPrimitiveArray::~PinnedPrimitiveArray() noexcept { + if (elements_) { + release(); + } +} + +#pragma push_macro("DECLARE_PRIMITIVE_METHODS") +#undef DECLARE_PRIMITIVE_METHODS +#define DECLARE_PRIMITIVE_METHODS(TYPE, NAME) \ +template<> TYPE* PinnedPrimitiveArray::get(); \ +template<> void PinnedPrimitiveArray::release(); \ + +DECLARE_PRIMITIVE_METHODS(jboolean, Boolean) +DECLARE_PRIMITIVE_METHODS(jbyte, Byte) +DECLARE_PRIMITIVE_METHODS(jchar, Char) +DECLARE_PRIMITIVE_METHODS(jshort, Short) +DECLARE_PRIMITIVE_METHODS(jint, Int) +DECLARE_PRIMITIVE_METHODS(jlong, Long) +DECLARE_PRIMITIVE_METHODS(jfloat, Float) +DECLARE_PRIMITIVE_METHODS(jdouble, Double) +#pragma pop_macro("DECLARE_PRIMITIVE_METHODS") + + +template +inline alias_ref JavaClass::javaClassStatic() { + static auto cls = findClassStatic( + std::string(T::kJavaDescriptor + 1, strlen(T::kJavaDescriptor) - 2).c_str()); + return cls; +} + +template +inline local_ref JavaClass::javaClassLocal() { + std::string className(T::kJavaDescriptor + 1, strlen(T::kJavaDescriptor) - 2); + return findClassLocal(className.c_str()); +} + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses.h new file mode 100644 index 000000000..33e83b886 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/CoreClasses.h @@ -0,0 +1,488 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +/** @file CoreClasses.h + * + * In CoreClasses.h wrappers for the core classes (jobject, jclass, and jstring) is defined + * to provide access to corresponding JNI functions + some conveniance. + */ + +#include "Meta.h" +#include "References.h" + +#include + +#include + +namespace facebook { +namespace jni { + +/// Lookup a class by name. Note this functions returns an alias_ref that +/// points to a leaked global reference. This is appropriate for classes +/// that are never unloaded (which is any class in an Android app and most +/// Java programs). +/// +/// The most common use case for this is storing the result +/// in a "static auto" variable, or a static global. +/// +/// @return Returns a leaked global reference to the class +alias_ref findClassStatic(const char* name); + +/// Lookup a class by name. Note this functions returns a local reference, +/// which means that it must not be stored in a static variable. +/// +/// The most common use case for this is one-time initialization +/// (like caching method ids). +/// +/// @return Returns a global reference to the class +local_ref findClassLocal(const char* name); + +/// Check to see if two references refer to the same object. Comparison with nullptr +/// returns true if and only if compared to another nullptr. A weak reference that +/// refers to a reclaimed object count as nullptr. +bool isSameObject(alias_ref lhs, alias_ref rhs) noexcept; + + +/// Wrapper to provide functionality to jobject references +template<> +class JObjectWrapper { + public: + /// Java type descriptor + static constexpr const char* kJavaDescriptor = "Ljava/lang/Object;"; + + static constexpr const char* get_instantiated_java_descriptor() { return nullptr; } + + /// Wrap an existing JNI reference + JObjectWrapper(jobject reference = nullptr) noexcept; + + // Copy constructor + JObjectWrapper(const JObjectWrapper& other) noexcept; + + /// Get a @ref local_ref of the object's class + local_ref getClass() const noexcept; + + /// Checks if the object is an instance of a class + bool isInstanceOf(alias_ref cls) const noexcept; + + /// Get the primitive value of a field + template + T getFieldValue(JField field) const noexcept; + + /// Get and wrap the value of a field in a @ref local_ref + template + local_ref getFieldValue(JField field) noexcept; + + /// Set the value of field. Any Java type is accepted, including the primitive types + /// and raw reference types. + template + void setFieldValue(JField field, T value) noexcept; + + /// Convenience method to create a std::string representing the object + std::string toString() const; + + protected: + jobject this_; + + private: + template + friend class base_owned_ref; + + template + friend class alias_ref; + + friend void swap(JObjectWrapper& a, JObjectWrapper& b) noexcept; + + void set(jobject reference) noexcept; + jobject get() const noexcept; + jobject self() const noexcept; +}; + +using JObject = JObjectWrapper; + +void swap(JObjectWrapper& a, JObjectWrapper& b) noexcept; + + +/// Wrapper to provide functionality to jclass references +struct NativeMethod; + +template<> +class JObjectWrapper : public JObjectWrapper { + public: + /// Java type descriptor + static constexpr const char* kJavaDescriptor = "Ljava/lang/Class;"; + + using JObjectWrapper::JObjectWrapper; + + /// Get a @local_ref to the super class of this class + local_ref getSuperclass() const noexcept; + + /// Register native methods for the class. Usage looks like this: + /// + /// classRef->registerNatives({ + /// makeNativeMethod("nativeMethodWithAutomaticDescriptor", + /// methodWithAutomaticDescriptor), + /// makeNativeMethod("nativeMethodWithExplicitDescriptor", + /// "(Lcom/facebook/example/MyClass;)V", + /// methodWithExplicitDescriptor), + /// }); + /// + /// By default, C++ exceptions raised will be converted to Java exceptions. + /// To avoid this and get the "standard" JNI behavior of a crash when a C++ + /// exception is crashing out of the JNI method, declare the method noexcept. + void registerNatives(std::initializer_list methods); + + /// Check to see if the class is assignable from another class + /// @pre cls != nullptr + bool isAssignableFrom(alias_ref cls) const noexcept; + + /// Convenience method to lookup the constructor with descriptor as specified by the + /// type arguments + template + JConstructor getConstructor() const; + + /// Convenience method to lookup the constructor with specified descriptor + template + JConstructor getConstructor(const char* descriptor) const; + + /// Look up the method with given name and descriptor as specified with the type arguments + template + JMethod getMethod(const char* name) const; + + /// Look up the method with given name and descriptor + template + JMethod getMethod(const char* name, const char* descriptor) const; + + /// Lookup the field with the given name and deduced descriptor + template + JField(), T>> getField(const char* name) const; + + /// Lookup the field with the given name and descriptor + template + JField(), T>> getField(const char* name, const char* descriptor) const; + + /// Lookup the static field with the given name and deduced descriptor + template + JStaticField(), T>> getStaticField(const char* name) const; + + /// Lookup the static field with the given name and descriptor + template + JStaticField(), T>> getStaticField( + const char* name, + const char* descriptor) const; + + /// Get the primitive value of a static field + template + T getStaticFieldValue(JStaticField field) const noexcept; + + /// Get and wrap the value of a field in a @ref local_ref + template + local_ref getStaticFieldValue(JStaticField field) noexcept; + + /// Set the value of field. Any Java type is accepted, including the primitive types + /// and raw reference types. + template + void setStaticFieldValue(JStaticField field, T value) noexcept; + + /// Allocates a new object and invokes the specified constructor + template + local_ref newObject(JConstructor constructor, Args... args) const; + + /// Look up the static method with given name and descriptor as specified with the type arguments + template + JStaticMethod getStaticMethod(const char* name) const; + + /// Look up the static method with given name and descriptor + template + JStaticMethod getStaticMethod(const char* name, const char* descriptor) const; + + /// Look up the non virtual method with given name and descriptor as specified with the + /// type arguments + template + JNonvirtualMethod getNonvirtualMethod(const char* name) const; + + /// Look up the non virtual method with given name and descriptor + template + JNonvirtualMethod getNonvirtualMethod(const char* name, const char* descriptor) const; + + private: + jclass self() const noexcept; +}; + +using JClass = JObjectWrapper; + +// Convenience method to register methods on a class without holding +// onto the class object. +void registerNatives(const char* name, std::initializer_list methods); + +/// Wrapper to provide functionality to jstring references +template<> +class JObjectWrapper : public JObjectWrapper { + public: + /// Java type descriptor + static constexpr const char* kJavaDescriptor = "Ljava/lang/String;"; + + using JObjectWrapper::JObjectWrapper; + + /// Convenience method to convert a jstring object to a std::string + std::string toStdString() const; + + private: + jstring self() const noexcept; +}; + +/// Convenience functions to convert a std::string or const char* into a @ref local_ref to a +/// jstring +local_ref make_jstring(const char* modifiedUtf8); +local_ref make_jstring(const std::string& modifiedUtf8); + +using JString = JObjectWrapper; + +/// Wrapper to provide functionality to jthrowable references +template<> +class JObjectWrapper : public JObjectWrapper { + public: + /// Java type descriptor + static constexpr const char* kJavaDescriptor = "Ljava/lang/Throwable;"; + + using JObjectWrapper::JObjectWrapper; + + private: + jthrowable self() const noexcept; +}; + + +/// @cond INTERNAL +template class _jtypeArray : public _jobjectArray {}; +// @endcond +/// Wrapper to provide functionality for arrays of j-types +template using jtypeArray = _jtypeArray*; + +template +class ElementProxy { + private: + JObjectWrapper<_jtypeArray*>* target_; + size_t idx_; + + public: + ElementProxy(JObjectWrapper<_jtypeArray*>* target, size_t idx); + + ElementProxy& operator=(const T& o); + + ElementProxy& operator=(alias_ref& o); + + ElementProxy& operator=(alias_ref&& o); + + ElementProxy& operator=(const ElementProxy& o); + + operator const local_ref () const; + + operator local_ref (); + }; + +template +class JObjectWrapper> : public JObjectWrapper { + public: + static constexpr const char* kJavaDescriptor = nullptr; + static std::string get_instantiated_java_descriptor() { + return jtype_traits::array_descriptor(); + }; + + using JObjectWrapper::JObjectWrapper; + + /// Allocate a new array from Java heap, for passing as a JNI parameter or return value. + /// NOTE: if using as a return value, you want to call release() instead of get() on the + /// smart pointer. + static local_ref> newArray(size_t count); + + /// Assign an object to the array. + /// Typically you will use the shorthand (*ref)[idx]=value; + void setElement(size_t idx, const T& value); + + /// Read an object from the array. + /// Typically you will use the shorthand + /// T value = (*ref)[idx]; + /// If you use auto, you'll get an ElementProxy, which may need to be cast. + local_ref getElement(size_t idx); + + /// Get the size of the array. + size_t size(); + + /// EXPERIMENTAL SUBSCRIPT SUPPORT + /// This implementation of [] returns a proxy object which then has a bunch of specializations + /// (adopt_local free function, operator= and casting overloads on the ElementProxy) that can + /// make code look like it is dealing with a T rather than an obvious proxy. In particular, the + /// proxy in this iteration does not read a value and therefore does not create a LocalRef + /// until one of these other operators is used. There are certainly holes that you may find + /// by using idioms that haven't been tried yet. Consider yourself warned. On the other hand, + /// it does make for some idiomatic assignment code; see TestBuildStringArray in fbjni_tests + /// for some examples. + ElementProxy operator[](size_t idx); + + private: + jtypeArray self() const noexcept; + static std::string bareClassName(); +}; + +template +using JArrayClass = JObjectWrapper>; + +template +local_ref> adopt_local_array(jobjectArray ref) { + return adopt_local(static_cast>(ref)); +} + +template +local_ref adopt_local(ElementProxy elementProxy) { + return static_cast>(elementProxy); +} + +/// Wrapper to provide functionality to jarray references. +/// This is an empty holder by itself. Construct a PinnedPrimitiveArray to actually interact with +/// the elements of the array. +template<> +class JObjectWrapper : public JObjectWrapper { + public: + static constexpr const char* kJavaDescriptor = nullptr; + + using JObjectWrapper::JObjectWrapper; + size_t size() const noexcept; + + private: + jarray self() const noexcept; +}; + +using JArray = JObjectWrapper; + +template +class PinnedPrimitiveArray; + +#pragma push_macro("DECLARE_PRIMITIVE_ARRAY_UTILS") +#undef DECLARE_PRIMITIVE_ARRAY_UTILS +#define DECLARE_PRIMITIVE_ARRAY_UTILS(TYPE, DESC) \ +local_ref make_ ## TYPE ## _array(jsize size); \ + \ +template<> class JObjectWrapper : public JArray { \ + public: \ + static constexpr const char* kJavaDescriptor = "[" # DESC; \ + \ + using JArray::JArray; \ + \ + j ## TYPE* getRegion(jsize start, jsize length, j ## TYPE* buf); \ + std::unique_ptr getRegion(jsize start, jsize length); \ + void setRegion(jsize start, jsize length, j ## TYPE* buf); \ + PinnedPrimitiveArray pin(); \ + \ + private: \ + j ## TYPE ## Array self() const noexcept { \ + return static_cast(this_); \ + } \ +} \ + +DECLARE_PRIMITIVE_ARRAY_UTILS(boolean, "Z"); +DECLARE_PRIMITIVE_ARRAY_UTILS(byte, "B"); +DECLARE_PRIMITIVE_ARRAY_UTILS(char, "C"); +DECLARE_PRIMITIVE_ARRAY_UTILS(short, "S"); +DECLARE_PRIMITIVE_ARRAY_UTILS(int, "I"); +DECLARE_PRIMITIVE_ARRAY_UTILS(long, "J"); +DECLARE_PRIMITIVE_ARRAY_UTILS(float, "F"); +DECLARE_PRIMITIVE_ARRAY_UTILS(double, "D"); + +#pragma pop_macro("DECLARE_PRIMITIVE_ARRAY_UTILS") + + +/// RAII class for pinned primitive arrays +/// This currently only supports read/write access to existing java arrays. You can't create a +/// primitive array this way yet. This class also pins the entire array into memory during the +/// lifetime of the PinnedPrimitiveArray. If you need to unpin the array manually, call the +/// release() function. During a long-running block of code, you should unpin the array as soon +/// as you're done with it, to avoid holding up the Java garbage collector. +template +class PinnedPrimitiveArray { + public: + static_assert(is_jni_primitive::value, + "PinnedPrimitiveArray requires primitive jni type."); + + PinnedPrimitiveArray(PinnedPrimitiveArray&&) noexcept; + PinnedPrimitiveArray(const PinnedPrimitiveArray&) = delete; + ~PinnedPrimitiveArray() noexcept; + + PinnedPrimitiveArray& operator=(PinnedPrimitiveArray&&) noexcept; + PinnedPrimitiveArray& operator=(const PinnedPrimitiveArray&) = delete; + + T* get(); + void release(); + + const T& operator[](size_t index) const; + T& operator[](size_t index); + bool isCopy() const noexcept; + size_t size() const noexcept; + + private: + alias_ref array_; + T* elements_; + jboolean isCopy_; + size_t size_; + + PinnedPrimitiveArray(alias_ref) noexcept; + + friend class JObjectWrapper; + friend class JObjectWrapper; + friend class JObjectWrapper; + friend class JObjectWrapper; + friend class JObjectWrapper; + friend class JObjectWrapper; + friend class JObjectWrapper; + friend class JObjectWrapper; +}; + + +// Together, these classes allow convenient use of any class with the fbjni +// helpers. To use: +// +// struct MyClass : public JavaClass { +// constexpr static auto kJavaDescriptor = "Lcom/example/package/MyClass;"; +// }; +// +// alias_ref myClass = foo(); + +template +class JavaClass { +public: + // JNI pattern for jobject assignable pointer + struct _javaobject : public _jobject { + typedef T javaClass; + }; + typedef _javaobject* javaobject; + + static alias_ref javaClassStatic(); + static local_ref javaClassLocal(); +}; + +template +class JObjectWrapper::value && + std::is_class::type::javaClass>::value + >::type> + : public JObjectWrapper { +public: + static constexpr const char* kJavaDescriptor = + std::remove_pointer::type::javaClass::kJavaDescriptor; + + using JObjectWrapper::JObjectWrapper; +}; + +}} + +#include "CoreClasses-inl.h" +// This is here because code in Meta-inl.h uses alias_ref, which +// requires JObjectWrapper to be concrete before it can work. +#include "Meta-inl.h" diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.cpp b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.cpp new file mode 100644 index 000000000..c5dfb3f14 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.cpp @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "Exceptions.h" +#include "CoreClasses.h" +#include "../ALog.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace facebook { +namespace jni { + +// CommonJniExceptions ///////////////////////////////////////////////////////////////////////////// + +class CommonJniExceptions { + public: + static void init(); + + static jclass getThrowableClass() { + return throwableClass_; + } + + static jclass getUnknownCppExceptionClass() { + return unknownCppExceptionClass_; + } + + static jthrowable getUnknownCppExceptionObject() { + return unknownCppExceptionObject_; + } + + static jthrowable getRuntimeExceptionObject() { + return runtimeExceptionObject_; + } + + private: + static jclass throwableClass_; + static jclass unknownCppExceptionClass_; + static jthrowable unknownCppExceptionObject_; + static jthrowable runtimeExceptionObject_; +}; + +// The variables in this class are all JNI global references and are intentionally leaked because +// we assume this library cannot be unloaded. These global references are created manually instead +// of using global_ref from References.h to avoid circular dependency. +jclass CommonJniExceptions::throwableClass_ = nullptr; +jclass CommonJniExceptions::unknownCppExceptionClass_ = nullptr; +jthrowable CommonJniExceptions::unknownCppExceptionObject_ = nullptr; +jthrowable CommonJniExceptions::runtimeExceptionObject_ = nullptr; + + +// Variable to guarantee that fallback exceptions have been initialized early. We don't want to +// do pure dynamic initialization -- we want to warn programmers early that they need to run the +// helpers at library load time instead of lazily getting them when the exception helpers are +// first used. +static std::atomic gIsInitialized(false); + +void CommonJniExceptions::init() { + JNIEnv* env = internal::getEnv(); + FBASSERTMSGF(env, "Could not get JNI Environment"); + + // Throwable class + jclass localThrowableClass = env->FindClass("java/lang/Throwable"); + FBASSERT(localThrowableClass); + throwableClass_ = static_cast(env->NewGlobalRef(localThrowableClass)); + FBASSERT(throwableClass_); + env->DeleteLocalRef(localThrowableClass); + + // UnknownCppException class + jclass localUnknownCppExceptionClass = env->FindClass("com/facebook/jni/UnknownCppException"); + FBASSERT(localUnknownCppExceptionClass); + jmethodID unknownCppExceptionConstructorMID = env->GetMethodID( + localUnknownCppExceptionClass, + "", + "()V"); + FBASSERT(unknownCppExceptionConstructorMID); + unknownCppExceptionClass_ = static_cast(env->NewGlobalRef(localUnknownCppExceptionClass)); + FBASSERT(unknownCppExceptionClass_); + env->DeleteLocalRef(localUnknownCppExceptionClass); + + // UnknownCppException object + jthrowable localUnknownCppExceptionObject = static_cast(env->NewObject( + unknownCppExceptionClass_, + unknownCppExceptionConstructorMID)); + FBASSERT(localUnknownCppExceptionObject); + unknownCppExceptionObject_ = static_cast(env->NewGlobalRef( + localUnknownCppExceptionObject)); + FBASSERT(unknownCppExceptionObject_); + env->DeleteLocalRef(localUnknownCppExceptionObject); + + // RuntimeException object + jclass localRuntimeExceptionClass = env->FindClass("java/lang/RuntimeException"); + FBASSERT(localRuntimeExceptionClass); + + jmethodID runtimeExceptionConstructorMID = env->GetMethodID( + localRuntimeExceptionClass, + "", + "()V"); + FBASSERT(runtimeExceptionConstructorMID); + jthrowable localRuntimeExceptionObject = static_cast(env->NewObject( + localRuntimeExceptionClass, + runtimeExceptionConstructorMID)); + FBASSERT(localRuntimeExceptionObject); + runtimeExceptionObject_ = static_cast(env->NewGlobalRef(localRuntimeExceptionObject)); + FBASSERT(runtimeExceptionObject_); + + env->DeleteLocalRef(localRuntimeExceptionClass); + env->DeleteLocalRef(localRuntimeExceptionObject); +} + + +// initExceptionHelpers() ////////////////////////////////////////////////////////////////////////// + +void internal::initExceptionHelpers() { + CommonJniExceptions::init(); + gIsInitialized.store(true, std::memory_order_seq_cst); +} + +void assertIfExceptionsNotInitialized() { + // Use relaxed memory order because we don't need memory barriers. + // The real init-once enforcement is done by the compiler for the + // "static" in initExceptionHelpers. + FBASSERTMSGF(gIsInitialized.load(std::memory_order_relaxed), + "initExceptionHelpers was never called!"); +} + +// Exception throwing & translating functions ////////////////////////////////////////////////////// + +// Functions that throw Java exceptions + +namespace { + +void setJavaExceptionAndAbortOnFailure(jthrowable throwable) noexcept { + assertIfExceptionsNotInitialized(); + JNIEnv* env = internal::getEnv(); + if (throwable) { + env->Throw(throwable); + } + if (env->ExceptionCheck() != JNI_TRUE) { + std::abort(); + } +} + +void setDefaultException() noexcept { + assertIfExceptionsNotInitialized(); + setJavaExceptionAndAbortOnFailure(CommonJniExceptions::getRuntimeExceptionObject()); +} + +void setCppSystemErrorExceptionInJava(const std::system_error& ex) noexcept { + assertIfExceptionsNotInitialized(); + JNIEnv* env = internal::getEnv(); + jclass cppSystemErrorExceptionClass = env->FindClass( + "com/facebook/jni/CppSystemErrorException"); + if (!cppSystemErrorExceptionClass) { + setDefaultException(); + return; + } + jmethodID constructorMID = env->GetMethodID( + cppSystemErrorExceptionClass, + "", + "(Ljava/lang/String;I)V"); + if (!constructorMID) { + setDefaultException(); + return; + } + jthrowable cppSystemErrorExceptionObject = static_cast(env->NewObject( + cppSystemErrorExceptionClass, + constructorMID, + env->NewStringUTF(ex.what()), + ex.code().value())); + setJavaExceptionAndAbortOnFailure(cppSystemErrorExceptionObject); +} + +template +void setNewJavaException(jclass exceptionClass, const char* fmt, ARGS... args) { + assertIfExceptionsNotInitialized(); + int msgSize = snprintf(nullptr, 0, fmt, args...); + JNIEnv* env = internal::getEnv(); + + try { + char *msg = (char*) alloca(msgSize); + snprintf(msg, kMaxExceptionMessageBufferSize, fmt, args...); + env->ThrowNew(exceptionClass, msg); + } catch (...) { + env->ThrowNew(exceptionClass, ""); + } + + if (env->ExceptionCheck() != JNI_TRUE) { + setDefaultException(); + } +} + +void setNewJavaException(jclass exceptionClass, const char* msg) { + assertIfExceptionsNotInitialized(); + setNewJavaException(exceptionClass, "%s", msg); +} + +template +void setNewJavaException(const char* className, const char* fmt, ARGS... args) { + assertIfExceptionsNotInitialized(); + JNIEnv* env = internal::getEnv(); + jclass exceptionClass = env->FindClass(className); + if (env->ExceptionCheck() != JNI_TRUE && !exceptionClass) { + // If FindClass() has failed but no exception has been thrown, throw a default exception. + setDefaultException(); + return; + } + setNewJavaException(exceptionClass, fmt, args...); +} + +} + +// Functions that throw C++ exceptions + +// TODO(T6618159) Take a stack dump here to save context if it results in a crash when propagated +void throwPendingJniExceptionAsCppException() { + assertIfExceptionsNotInitialized(); + JNIEnv* env = internal::getEnv(); + if (env->ExceptionCheck() == JNI_FALSE) { + return; + } + + jthrowable throwable = env->ExceptionOccurred(); + if (!throwable) { + throw std::runtime_error("Unable to get pending JNI exception."); + } + + env->ExceptionClear(); + throw JniException(throwable); +} + +void throwCppExceptionIf(bool condition) { + assertIfExceptionsNotInitialized(); + if (!condition) { + return; + } + + JNIEnv* env = internal::getEnv(); + if (env->ExceptionCheck() == JNI_TRUE) { + throwPendingJniExceptionAsCppException(); + return; + } + + throw JniException(); +} + +void throwNewJavaException(jthrowable throwable) { + throw JniException(throwable); +} + +void throwNewJavaException(const char* throwableName, const char* msg) { + // If anything of the fbjni calls fail, an exception of a suitable + // form will be thrown, which is what we want. + auto throwableClass = findClassLocal(throwableName); + auto throwable = throwableClass->newObject( + throwableClass->getConstructor(), + make_jstring(msg).release()); + throwNewJavaException(throwable.get()); +} + +// Translate C++ to Java Exception + +void translatePendingCppExceptionToJavaException() noexcept { + assertIfExceptionsNotInitialized(); + try { + try { + throw; + } catch(const JniException& ex) { + ex.setJavaException(); + } catch(const std::ios_base::failure& ex) { + setNewJavaException("java/io/IOException", ex.what()); + } catch(const std::bad_alloc& ex) { + setNewJavaException("java/lang/OutOfMemoryError", ex.what()); + } catch(const std::out_of_range& ex) { + setNewJavaException("java/lang/ArrayIndexOutOfBoundsException", ex.what()); + } catch(const std::system_error& ex) { + setCppSystemErrorExceptionInJava(ex); + } catch(const std::runtime_error& ex) { + setNewJavaException("java/lang/RuntimeException", ex.what()); + } catch(const std::exception& ex) { + setNewJavaException("com/facebook/jni/CppException", ex.what()); + } catch(const char* msg) { + setNewJavaException(CommonJniExceptions::getUnknownCppExceptionClass(), msg); + } catch(...) { + setJavaExceptionAndAbortOnFailure(CommonJniExceptions::getUnknownCppExceptionObject()); + } + } catch(...) { + // This block aborts the program, if something bad happens when handling exceptions, thus + // keeping this function noexcept. + std::abort(); + } +} + +// JniException //////////////////////////////////////////////////////////////////////////////////// + +const std::string JniException::kExceptionMessageFailure_ = "Unable to get exception message."; + +JniException::JniException() : JniException(CommonJniExceptions::getRuntimeExceptionObject()) { } + +JniException::JniException(jthrowable throwable) : isMessageExtracted_(false) { + assertIfExceptionsNotInitialized(); + throwableGlobalRef_ = static_cast(internal::getEnv()->NewGlobalRef(throwable)); + if (!throwableGlobalRef_) { + throw std::bad_alloc(); + } +} + +JniException::JniException(JniException &&rhs) + : throwableGlobalRef_(std::move(rhs.throwableGlobalRef_)), + what_(std::move(rhs.what_)), + isMessageExtracted_(rhs.isMessageExtracted_) { + rhs.throwableGlobalRef_ = nullptr; +} + +JniException::JniException(const JniException &rhs) + : what_(rhs.what_), isMessageExtracted_(rhs.isMessageExtracted_) { + JNIEnv* env = internal::getEnv(); + if (rhs.getThrowable()) { + throwableGlobalRef_ = static_cast(env->NewGlobalRef(rhs.getThrowable())); + if (!throwableGlobalRef_) { + throw std::bad_alloc(); + } + } else { + throwableGlobalRef_ = nullptr; + } +} + +JniException::~JniException() noexcept { + if (throwableGlobalRef_) { + internal::getEnv()->DeleteGlobalRef(throwableGlobalRef_); + } +} + +jthrowable JniException::getThrowable() const noexcept { + return throwableGlobalRef_; +} + +// TODO 6900503: consider making this thread-safe. +void JniException::populateWhat() const noexcept { + JNIEnv* env = internal::getEnv(); + + jmethodID toStringMID = env->GetMethodID( + CommonJniExceptions::getThrowableClass(), + "toString", + "()Ljava/lang/String;"); + jstring messageJString = (jstring) env->CallObjectMethod( + throwableGlobalRef_, + toStringMID); + + isMessageExtracted_ = true; + + if (env->ExceptionCheck()) { + env->ExceptionClear(); + what_ = kExceptionMessageFailure_; + return; + } + + const char* chars = env->GetStringUTFChars(messageJString, nullptr); + if (!chars) { + what_ = kExceptionMessageFailure_; + return; + } + + try { + what_ = std::string(chars); + } catch(...) { + what_ = kExceptionMessageFailure_; + } + + env->ReleaseStringUTFChars(messageJString, chars); +} + +const char* JniException::what() const noexcept { + if (!isMessageExtracted_) { + populateWhat(); + } + return what_.c_str(); +} + +void JniException::setJavaException() const noexcept { + setJavaExceptionAndAbortOnFailure(throwableGlobalRef_); +} + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.h new file mode 100644 index 000000000..9ba9367f6 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Exceptions.h @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + * @file Exceptions.h + * + * After invoking a JNI function that can throw a Java exception, the macro + * @ref FACEBOOK_JNI_THROW_PENDING_EXCEPTION() or @ref FACEBOOK_JNI_THROW_EXCEPTION_IF() + * should be invoked. + * + * IMPORTANT! IMPORTANT! IMPORTANT! IMPORTANT! IMPORTANT! IMPORTANT! IMPORTANT! IMPORTANT! + * To use these methods you MUST call initExceptionHelpers() when your library is loaded. + */ + +#pragma once + +#include +#include +#include + +#include + +#include "Common.h" + +// If a pending JNI Java exception is found, wraps it in a JniException object and throws it as +// a C++ exception. +#define FACEBOOK_JNI_THROW_PENDING_EXCEPTION() \ + ::facebook::jni::throwPendingJniExceptionAsCppException() + +// If the condition is true, throws a JniException object, which wraps the pending JNI Java +// exception if any. If no pending exception is found, throws a JniException object that wraps a +// RuntimeException throwable.  +#define FACEBOOK_JNI_THROW_EXCEPTION_IF(CONDITION) \ + ::facebook::jni::throwCppExceptionIf(CONDITION) + +namespace facebook { +namespace jni { + +namespace internal { + void initExceptionHelpers(); +} + +/** + * Before using any of the state initialized above, call this. It + * will assert if initialization has not yet occurred. + */ +void assertIfExceptionsNotInitialized(); + +// JniException //////////////////////////////////////////////////////////////////////////////////// + +/** + * This class wraps a Java exception into a C++ exception; if the exception is routed back + * to the Java side, it can be unwrapped and just look like a pure Java interaction. The class + * is resilient to errors while creating the exception, falling back to some pre-allocated + * exceptions if a new one cannot be allocated or populated. + * + * Note: the what() method of this class is not thread-safe (t6900503). + */ +class JniException : public std::exception { + public: + JniException(); + + explicit JniException(jthrowable throwable); + + JniException(JniException &&rhs); + + JniException(const JniException &other); + + ~JniException() noexcept; + + jthrowable getThrowable() const noexcept; + + virtual const char* what() const noexcept; + + void setJavaException() const noexcept; + + private: + jthrowable throwableGlobalRef_; + mutable std::string what_; + mutable bool isMessageExtracted_; + const static std::string kExceptionMessageFailure_; + + void populateWhat() const noexcept; +}; + +// Exception throwing & translating functions ////////////////////////////////////////////////////// + +// Functions that throw C++ exceptions + +void throwPendingJniExceptionAsCppException(); + +void throwCppExceptionIf(bool condition); + +static const int kMaxExceptionMessageBufferSize = 512; + +[[noreturn]] void throwNewJavaException(jthrowable); + +[[noreturn]] void throwNewJavaException(const char* throwableName, const char* msg); + +// These methods are the preferred way to throw a Java exception from +// a C++ function. They create and throw a C++ exception which wraps +// a Java exception, so the C++ flow is interrupted. Then, when +// translatePendingCppExceptionToJavaException is called at the +// topmost level of the native stack, the wrapped Java exception is +// thrown to the java caller. +template +[[noreturn]] void throwNewJavaException(const char* throwableName, const char* fmt, Args... args) { + assertIfExceptionsNotInitialized(); + int msgSize = snprintf(nullptr, 0, fmt, args...); + + char *msg = (char*) alloca(msgSize); + snprintf(msg, kMaxExceptionMessageBufferSize, fmt, args...); + throwNewJavaException(throwableName, msg); +} + +// Identifies any pending C++ exception and throws it as a Java exception. If the exception can't +// be thrown, it aborts the program. This is a noexcept function at C++ level. +void translatePendingCppExceptionToJavaException() noexcept; + +// For convenience, some exception names in java.lang are available here. + +const char* const gJavaLangIllegalArgumentException = "java/lang/IllegalArgumentException"; + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.cpp b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.cpp new file mode 100644 index 000000000..ebcb778de --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "Hybrid.h" + +#include "Exceptions.h" +#include "Registration.h" + +namespace facebook { +namespace jni { + +namespace detail { + +void setNativePointer(alias_ref hybridData, + std::unique_ptr new_value) { + static auto pointerField = hybridData->getClass()->getField("mNativePointer"); + auto* old_value = reinterpret_cast(hybridData->getFieldValue(pointerField)); + if (new_value) { + // Modify should only ever be called once with a non-null + // new_value. If this happens again it's a programmer error, so + // blow up. + FBASSERTMSGF(old_value == 0, "Attempt to set C++ native pointer twice"); + } else if (old_value == 0) { + return; + } + // delete on a null pointer is defined to be a noop. + delete old_value; + // This releases ownership from the unique_ptr, and passes the pointer, and + // ownership of it, to HybridData which is managed by the java GC. The + // finalizer on hybridData calls resetNative which will delete the object, if + // reseetNative has not already been called. + hybridData->setFieldValue(pointerField, reinterpret_cast(new_value.release())); +} + +BaseHybridClass* getNativePointer(alias_ref hybridData) { + static auto pointerField = hybridData->getClass()->getField("mNativePointer"); + auto* value = reinterpret_cast(hybridData->getFieldValue(pointerField)); + if (!value) { + throwNewJavaException("java/lang/NullPointerException", "java.lang.NullPointerException"); + } + return value; +} + +local_ref getHybridData(alias_ref jthis, + JField field) { + auto hybridData = jthis->getFieldValue(field); + if (!hybridData) { + throwNewJavaException("java/lang/NullPointerException", "java.lang.NullPointerException"); + } + return hybridData; +} + +} + +namespace { + +void resetNative(alias_ref jthis) { + detail::setNativePointer(jthis, nullptr); +} + +} + +void HybridDataOnLoad() { + registerNatives("com/facebook/jni/HybridData", { + makeNativeMethod("resetNative", resetNative), + }); +} + +}} + diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.h new file mode 100644 index 000000000..cfc4b03e8 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Hybrid.h @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#include +#include +#include +#include "CoreClasses.h" + +namespace facebook { +namespace jni { + +class BaseHybridClass { +public: + virtual ~BaseHybridClass() {} +}; + +namespace detail { + +struct HybridData : public JavaClass { + constexpr static auto kJavaDescriptor = "Lcom/facebook/jni/HybridData;"; +}; + +void setNativePointer(alias_ref hybridData, + std::unique_ptr new_value); +BaseHybridClass* getNativePointer(alias_ref hybridData); +local_ref getHybridData(alias_ref jthis, + JField field); + +// Normally, pass through types unmolested. +template +struct Convert { + typedef T jniType; + static jniType fromJni(jniType t) { + return t; + } + static jniType toJniRet(jniType t) { + return t; + } + static jniType toCall(jniType t) { + return t; + } +}; + +// This is needed for return conversion +template <> +struct Convert { + typedef void jniType; +}; + +// convert to std::string from jstring +template <> +struct Convert { + typedef jstring jniType; + static std::string fromJni(jniType t) { + return wrap_alias(t)->toStdString(); + } + static jniType toJniRet(const std::string& t) { + return make_jstring(t).release(); + } + static local_ref toCall(const std::string& t) { + return make_jstring(t); + } +}; + +// convert return from const char* +template <> +struct Convert { + typedef jstring jniType; + // no automatic synthesis of const char*. (It can't be freed.) + static jniType toJniRet(const char* t) { + return make_jstring(t).release(); + } + static local_ref toCall(const char* t) { + return make_jstring(t); + } +}; + +// convert to alias_ref from T +template +struct Convert> { + typedef T jniType; + static alias_ref fromJni(jniType t) { + return wrap_alias(t); + } + static jniType toJniRet(alias_ref t) { + return t.get(); + } + static jniType toCall(alias_ref t) { + return t.get(); + } +}; + +// convert return from local_ref +template +struct Convert> { + typedef T jniType; + // No automatic synthesis of local_ref + static jniType toJniRet(local_ref t) { + return t.release(); + } + static jniType toCall(local_ref t) { + return t.get(); + } +}; + +// convert return from global_ref +template +struct Convert> { + typedef T jniType; + // No automatic synthesis of global_ref + static jniType toJniRet(global_ref t) { + return t.get(); + } + static jniType toCall(global_ref t) { + return t.get(); + } +}; + +// In order to avoid potentially filling the jni locals table, +// temporary objects (right now, this is just jstrings) need to be +// released. This is done by returning a holder which autoconverts to +// jstring. This is only relevant when the jniType is passed down, as +// in newObjectJavaArgs. + +template +inline T callToJni(T&& t) { + return t; +} + +inline jstring callToJni(local_ref&& sref) { + return sref.get(); +} + +struct jstring_holder { + local_ref s_; + jstring_holder(const char* s) : s_(make_jstring(s)) {} + operator jstring() { return s_.get(); } +}; + +} + +template +class HybridClass : public BaseHybridClass + , public JavaClass { +public: + typedef detail::HybridData::javaobject jhybriddata; + typedef typename JavaClass::javaobject jhybridobject; + + // I'm not sure why I need this, but I get errors without it. + using JavaClass::javaClassStatic; + +protected: + typedef HybridClass HybridBase; + + // This ensures that a C++ hybrid part cannot be created on its own + // by default. If a hybrid wants to enable this, it can provide its + // own public ctor, or change the accessibility of this to public. + HybridClass() = default; + + static void registerHybrid(std::initializer_list methods) { + javaClassStatic()->registerNatives(methods); + } + + static local_ref makeHybridData(std::unique_ptr cxxPart) { + static auto dataCtor = detail::HybridData::javaClassStatic()->getConstructor(); + auto hybridData = detail::HybridData::javaClassStatic()->newObject(dataCtor); + detail::setNativePointer(hybridData, std::move(cxxPart)); + return hybridData; + } + + template + static local_ref makeCxxInstance(Args&&... args) { + return makeHybridData(std::unique_ptr(new T(std::forward(args)...))); + } + +public: + // Factory method for creating a hybrid object where the arguments + // are used to initialize the C++ part directly without passing them + // through java. This method requires the Java part to have a ctor + // which takes a HybridData, and for the C++ part to have a ctor + // compatible with the arguments passed here. For safety, the ctor + // can be private, and the hybrid declared a friend of its base, so + // the hybrid can only be created from here. + // + // Exception behavior: This can throw an exception if creating the + // C++ object fails, or any JNI methods throw. + template + static local_ref newObjectCxxArgs(Args&&... args) { + auto hybridData = makeCxxInstance(std::forward(args)...); + static auto ctor = javaClassStatic()->template getConstructor(); + return javaClassStatic()->newObject(ctor, hybridData.get()); + } + + // Factory method for creating a hybrid object where the arguments + // are passed to the java ctor. + template + static local_ref newObjectJavaArgs(Args&&... args) { + static auto ctor = + javaClassStatic()->template getConstructor< + jhybridobject(typename detail::Convert::type>::jniType...)>(); + // This can't use the same impl as Convert::toJniRet because that + // function sometimes creates and then releases local_refs, which + // could potentially cause the locals table to fill. Instead, we + // use two calls, one which can return a local_ref if needed, and + // a second which extracts its value. The lifetime of the + // local_ref is the expression, after which it is destroyed and + // the local_ref is cleaned up. + auto lref = + javaClassStatic()->newObject( + ctor, detail::callToJni( + detail::Convert::type>::toCall(args))...); + return lref; + } + + // If a hybrid class throws an exception which derives from + // std::exception, it will be passed to mapException on the hybrid + // class, or nearest ancestor. This allows boilerplate exception + // translation code (for example, calling throwNewJavaException on a + // particular java class) to be hoisted to a common function. If + // mapException returns, then the std::exception will be translated + // to Java. + static void mapException(const std::exception& ex) {} +}; + +// Given a *_ref object which refers to a hybrid class, this will reach inside +// of it, find the mHybridData, extract the C++ instance pointer, cast it to +// the appropriate type, and return it. +template +inline typename std::remove_pointer::type::javaClass* cthis(T jthis) { + static auto dataField = + jthis->getClass()->template getField("mHybridData"); + // I'd like to use dynamic_cast here, but -fno-rtti is the default. + auto* value = static_cast::type::javaClass*>( + detail::getNativePointer(detail::getHybridData(jthis, dataField))); + // This would require some serious programmer error. + FBASSERTMSGF(value != 0, "Incorrect C++ type in hybrid field"); + return value; +} + +void HybridDataOnLoad(); + +} +} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta-inl.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta-inl.h new file mode 100644 index 000000000..cc2d8383d --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta-inl.h @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#include + +#include "Common.h" +#include "Exceptions.h" + +namespace facebook { +namespace jni { + +// JMethod ///////////////////////////////////////////////////////////////////////////////////////// + +inline JMethodBase::JMethodBase(jmethodID method_id) noexcept + : method_id_{method_id} +{} + +inline JMethodBase::operator bool() const noexcept { + return method_id_ != nullptr; +} + +inline jmethodID JMethodBase::getId() const noexcept { + return method_id_; +} + +template +inline void JMethod::operator()(alias_ref self, Args... args) { + const auto env = internal::getEnv(); + env->CallVoidMethod(self.get(), getId(), args...); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); +} + +#pragma push_macro("DEFINE_PRIMITIVE_CALL") +#undef DEFINE_PRIMITIVE_CALL +#define DEFINE_PRIMITIVE_CALL(TYPE, METHOD) \ +template \ +inline TYPE JMethod::operator()(alias_ref self, Args... args) { \ + const auto env = internal::getEnv(); \ + auto result = env->Call ## METHOD ## Method(self.get(), getId(), args...); \ + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); \ + return result; \ +} + +DEFINE_PRIMITIVE_CALL(jboolean, Boolean) +DEFINE_PRIMITIVE_CALL(jbyte, Byte) +DEFINE_PRIMITIVE_CALL(jchar, Char) +DEFINE_PRIMITIVE_CALL(jshort, Short) +DEFINE_PRIMITIVE_CALL(jint, Int) +DEFINE_PRIMITIVE_CALL(jlong, Long) +DEFINE_PRIMITIVE_CALL(jfloat, Float) +DEFINE_PRIMITIVE_CALL(jdouble, Double) +#pragma pop_macro("DEFINE_PRIMITIVE_CALL") + +template +inline local_ref JMethod::operator()(alias_ref self, Args... args) { + const auto env = internal::getEnv(); + auto result = env->CallObjectMethod(self.get(), getId(), args...); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return adopt_local(static_cast(result)); +} + +template +inline void JStaticMethod::operator()(alias_ref cls, Args... args) { + const auto env = internal::getEnv(); + env->CallStaticVoidMethod(cls.get(), getId(), args...); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); +} + +#pragma push_macro("DEFINE_PRIMITIVE_STATIC_CALL") +#undef DEFINE_PRIMITIVE_STATIC_CALL +#define DEFINE_PRIMITIVE_STATIC_CALL(TYPE, METHOD) \ +template \ +inline TYPE JStaticMethod::operator()(alias_ref cls, Args... args) { \ + const auto env = internal::getEnv(); \ + auto result = env->CallStatic ## METHOD ## Method(cls.get(), getId(), args...); \ + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); \ + return result; \ +} + +DEFINE_PRIMITIVE_STATIC_CALL(jboolean, Boolean) +DEFINE_PRIMITIVE_STATIC_CALL(jbyte, Byte) +DEFINE_PRIMITIVE_STATIC_CALL(jchar, Char) +DEFINE_PRIMITIVE_STATIC_CALL(jshort, Short) +DEFINE_PRIMITIVE_STATIC_CALL(jint, Int) +DEFINE_PRIMITIVE_STATIC_CALL(jlong, Long) +DEFINE_PRIMITIVE_STATIC_CALL(jfloat, Float) +DEFINE_PRIMITIVE_STATIC_CALL(jdouble, Double) +#pragma pop_macro("DEFINE_PRIMITIVE_STATIC_CALL") + +template +inline local_ref JStaticMethod::operator()(alias_ref cls, Args... args) { + const auto env = internal::getEnv(); + auto result = env->CallStaticObjectMethod(cls.get(), getId(), args...); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return adopt_local(static_cast(result)); +} + + +template +inline void +JNonvirtualMethod::operator()(alias_ref self, jclass cls, Args... args) { + const auto env = internal::getEnv(); + env->CallNonvirtualVoidMethod(self.get(), cls, getId(), args...); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); +} + +#pragma push_macro("DEFINE_PRIMITIVE_NON_VIRTUAL_CALL") +#undef DEFINE_PRIMITIVE_NON_VIRTUAL_CALL +#define DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(TYPE, METHOD) \ +template \ +inline TYPE \ +JNonvirtualMethod::operator()(alias_ref self, jclass cls, Args... args) { \ + const auto env = internal::getEnv(); \ + auto result = env->CallNonvirtual ## METHOD ## Method(self.get(), cls, getId(), args...); \ + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); \ + return result; \ +} + +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jboolean, Boolean) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jbyte, Byte) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jchar, Char) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jshort, Short) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jint, Int) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jlong, Long) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jfloat, Float) +DEFINE_PRIMITIVE_NON_VIRTUAL_CALL(jdouble, Double) +#pragma pop_macro("DEFINE_PRIMITIVE_NON_VIRTUAL_CALL") + +template +inline local_ref JNonvirtualMethod::operator()( + alias_ref self, + jclass cls, + Args... args) { + const auto env = internal::getEnv(); + auto result = env->CallNonvirtualObjectMethod(self.get(), cls, getId(), args...); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return adopt_local(static_cast(result)); +} + + +// jtype_traits //////////////////////////////////////////////////////////////////////////////////// + +/// The generic way to associate a descriptor to a type is to look it up in the +/// corresponding @ref JObjectWrapper specialization. This makes it easy to add +/// support for your user defined type. +template +struct jtype_traits { + static std::string descriptor() { + if (JObjectWrapper::kJavaDescriptor != nullptr) { + return std::string{JObjectWrapper::kJavaDescriptor}; + }; + return JObjectWrapper::get_instantiated_java_descriptor(); + } + + static std::string array_descriptor() { + return '[' + std::string{JObjectWrapper::kJavaDescriptor}; + } +}; + +#pragma push_macro("DEFINE_FIELD_AND_ARRAY_TRAIT") +#undef DEFINE_FIELD_AND_ARRAY_TRAIT + +#define DEFINE_FIELD_AND_ARRAY_TRAIT(TYPE, DSC) \ +template<> \ +struct jtype_traits { \ + static std::string descriptor() { return std::string{#DSC}; } \ +}; \ +template<> \ +struct jtype_traits { \ + static std::string descriptor() { return std::string{"[" #DSC}; } \ +}; + +// There is no voidArray, handle that without the macro. +template<> +struct jtype_traits { + static std::string descriptor() { return std::string{"V"}; }; +}; + +DEFINE_FIELD_AND_ARRAY_TRAIT(jboolean, Z) +DEFINE_FIELD_AND_ARRAY_TRAIT(jbyte, B) +DEFINE_FIELD_AND_ARRAY_TRAIT(jchar, C) +DEFINE_FIELD_AND_ARRAY_TRAIT(jshort, S) +DEFINE_FIELD_AND_ARRAY_TRAIT(jint, I) +DEFINE_FIELD_AND_ARRAY_TRAIT(jlong, J) +DEFINE_FIELD_AND_ARRAY_TRAIT(jfloat, F) +DEFINE_FIELD_AND_ARRAY_TRAIT(jdouble, D) + +#pragma pop_macro("DEFINE_FIELD_AND_ARRAY_TRAIT") + + +// JField /////////////////////////////////////////////////////////////////////////////////////// + +template +inline JField::JField(jfieldID field) noexcept + : field_id_{field} +{} + +template +inline JField::operator bool() const noexcept { + return field_id_ != nullptr; +} + +template +inline jfieldID JField::getId() const noexcept { + return field_id_; +} + +#pragma push_macro("DEFINE_FIELD_PRIMITIVE_GET_SET") +#undef DEFINE_FIELD_PRIMITIVE_GET_SET +#define DEFINE_FIELD_PRIMITIVE_GET_SET(TYPE, METHOD) \ +template<> \ +inline TYPE JField::get(jobject object) const noexcept { \ + const auto env = internal::getEnv(); \ + return env->Get ## METHOD ## Field(object, field_id_); \ +} \ + \ +template<> \ +inline void JField::set(jobject object, TYPE value) noexcept { \ + const auto env = internal::getEnv(); \ + env->Set ## METHOD ## Field(object, field_id_, value); \ +} + +DEFINE_FIELD_PRIMITIVE_GET_SET(jboolean, Boolean) +DEFINE_FIELD_PRIMITIVE_GET_SET(jbyte, Byte) +DEFINE_FIELD_PRIMITIVE_GET_SET(jchar, Char) +DEFINE_FIELD_PRIMITIVE_GET_SET(jshort, Short) +DEFINE_FIELD_PRIMITIVE_GET_SET(jint, Int) +DEFINE_FIELD_PRIMITIVE_GET_SET(jlong, Long) +DEFINE_FIELD_PRIMITIVE_GET_SET(jfloat, Float) +DEFINE_FIELD_PRIMITIVE_GET_SET(jdouble, Double) +#pragma pop_macro("DEFINE_FIELD_PRIMITIVE_GET_SET") + +template +inline T JField::get(jobject object) const noexcept { + return static_cast(internal::getEnv()->GetObjectField(object, field_id_)); +} + +template +inline void JField::set(jobject object, T value) noexcept { + internal::getEnv()->SetObjectField(object, field_id_, static_cast(value)); +} + +// JStaticField ///////////////////////////////////////////////////////////////////////////////// + +template +inline JStaticField::JStaticField(jfieldID field) noexcept + : field_id_{field} +{} + +template +inline JStaticField::operator bool() const noexcept { + return field_id_ != nullptr; +} + +template +inline jfieldID JStaticField::getId() const noexcept { + return field_id_; +} + +#pragma push_macro("DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET") +#undef DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET +#define DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(TYPE, METHOD) \ +template<> \ +inline TYPE JStaticField::get(jclass jcls) const noexcept { \ + const auto env = internal::getEnv(); \ + return env->GetStatic ## METHOD ## Field(jcls, field_id_); \ +} \ + \ +template<> \ +inline void JStaticField::set(jclass jcls, TYPE value) noexcept { \ + const auto env = internal::getEnv(); \ + env->SetStatic ## METHOD ## Field(jcls, field_id_, value); \ +} + +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jboolean, Boolean) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jbyte, Byte) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jchar, Char) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jshort, Short) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jint, Int) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jlong, Long) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jfloat, Float) +DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET(jdouble, Double) +#pragma pop_macro("DEFINE_STATIC_FIELD_PRIMITIVE_GET_SET") + +template +inline T JStaticField::get(jclass jcls) const noexcept { + const auto env = internal::getEnv(); + return static_cast(env->GetStaticObjectField(jcls, field_id_)); +} + +template +inline void JStaticField::set(jclass jcls, T value) noexcept { + internal::getEnv()->SetStaticObjectField(jcls, field_id_, value); +} + + +// jmethod_traits ////////////////////////////////////////////////////////////////////////////////// + +// TODO(T6608405) Adapt this to implement a register natives method that requires no descriptor +namespace internal { + +template +inline std::string JavaDescriptor() { + return jtype_traits::descriptor(); +} + +template +inline std::string JavaDescriptor() { + return JavaDescriptor() + JavaDescriptor(); +} + +template +inline std::string JMethodDescriptor() { + return "(" + JavaDescriptor() + ")" + JavaDescriptor(); +} + +template +inline std::string JMethodDescriptor() { + return "()" + JavaDescriptor(); +} + +} // internal + +template +inline std::string jmethod_traits::descriptor() { + return internal::JMethodDescriptor(); +} + +template +inline std::string jmethod_traits::constructor_descriptor() { + return internal::JMethodDescriptor(); +} + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta.h new file mode 100644 index 000000000..de1bde0d5 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Meta.h @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** @file meta.h + * + * Provides wrappers for meta data such as methods and fields. + */ + +#pragma once + +#include +#include + +#include + +#include "References.h" + +namespace facebook { +namespace jni { + +/// Wrapper of a jmethodID. Provides a common base for JMethod specializations +class JMethodBase { + public: + /// Verify that the method is valid + explicit operator bool() const noexcept; + + /// Access the wrapped id + jmethodID getId() const noexcept; + + protected: + /// Create a wrapper of a method id + explicit JMethodBase(jmethodID method_id = nullptr) noexcept; + + private: + jmethodID method_id_; +}; + + +/// Representation of a jmethodID +template +class JMethod; + +/// @cond INTERNAL +#pragma push_macro("DEFINE_PRIMITIVE_METHOD_CLASS") + +#undef DEFINE_PRIMITIVE_METHOD_CLASS + +// Defining JMethod specializations based on return value +#define DEFINE_PRIMITIVE_METHOD_CLASS(TYPE) \ +template \ +class JMethod : public JMethodBase { \ + public: \ + static_assert(std::is_void::value || IsJniPrimitive(), \ + "TYPE must be primitive or void"); \ + \ + using JMethodBase::JMethodBase; \ + JMethod() noexcept {}; \ + JMethod(const JMethod& other) noexcept = default; \ + \ + TYPE operator()(alias_ref self, Args... args); \ + \ + friend class JObjectWrapper; \ +} + +DEFINE_PRIMITIVE_METHOD_CLASS(void); +DEFINE_PRIMITIVE_METHOD_CLASS(jboolean); +DEFINE_PRIMITIVE_METHOD_CLASS(jbyte); +DEFINE_PRIMITIVE_METHOD_CLASS(jchar); +DEFINE_PRIMITIVE_METHOD_CLASS(jshort); +DEFINE_PRIMITIVE_METHOD_CLASS(jint); +DEFINE_PRIMITIVE_METHOD_CLASS(jlong); +DEFINE_PRIMITIVE_METHOD_CLASS(jfloat); +DEFINE_PRIMITIVE_METHOD_CLASS(jdouble); + +#pragma pop_macro("DEFINE_PRIMITIVE_METHOD_CLASS") +/// @endcond + + +/// JMethod specialization for references that wraps the return value in a @ref local_ref +template +class JMethod : public JMethodBase { + public: + static_assert(IsPlainJniReference(), "T* must be a JNI reference"); + + using JMethodBase::JMethodBase; + JMethod() noexcept {}; + JMethod(const JMethod& other) noexcept = default; + + /// Invoke a method and return a local reference wrapping the result + local_ref operator()(alias_ref self, Args... args); + + friend class JObjectWrapper; +}; + + +/// Convenience type representing constructors +template +using JConstructor = JMethod; + +/// Representation of a jStaticMethodID +template +class JStaticMethod; + +/// @cond INTERNAL +#pragma push_macro("DEFINE_PRIMITIVE_STATIC_METHOD_CLASS") + +#undef DEFINE_PRIMITIVE_STATIC_METHOD_CLASS + +// Defining JStaticMethod specializations based on return value +#define DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(TYPE) \ +template \ +class JStaticMethod : public JMethodBase { \ + static_assert(std::is_void::value || IsJniPrimitive(), \ + "T must be a JNI primitive or void"); \ + \ + public: \ + using JMethodBase::JMethodBase; \ + JStaticMethod() noexcept {}; \ + JStaticMethod(const JStaticMethod& other) noexcept = default; \ + \ + TYPE operator()(alias_ref cls, Args... args); \ + \ + friend class JObjectWrapper; \ +} + +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(void); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jboolean); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jbyte); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jchar); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jshort); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jint); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jlong); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jfloat); +DEFINE_PRIMITIVE_STATIC_METHOD_CLASS(jdouble); + +#pragma pop_macro("DEFINE_PRIMITIVE_STATIC_METHOD_CLASS") +/// @endcond + + +/// JStaticMethod specialization for references that wraps the return value in a @ref local_ref +template +class JStaticMethod : public JMethodBase { + static_assert(IsPlainJniReference(), "T* must be a JNI reference"); + + public: + using JMethodBase::JMethodBase; + JStaticMethod() noexcept {}; + JStaticMethod(const JStaticMethod& other) noexcept = default; + + /// Invoke a method and return a local reference wrapping the result + local_ref operator()(alias_ref cls, Args... args); + + friend class JObjectWrapper; +}; + +/// Representation of a jNonvirtualMethodID +template +class JNonvirtualMethod; + +/// @cond INTERNAL +#pragma push_macro("DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS") + +#undef DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS + +// Defining JNonvirtualMethod specializations based on return value +#define DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(TYPE) \ +template \ +class JNonvirtualMethod : public JMethodBase { \ + static_assert(std::is_void::value || IsJniPrimitive(), \ + "T must be a JNI primitive or void"); \ + \ + public: \ + using JMethodBase::JMethodBase; \ + JNonvirtualMethod() noexcept {}; \ + JNonvirtualMethod(const JNonvirtualMethod& other) noexcept = default; \ + \ + TYPE operator()(alias_ref self, jclass cls, Args... args); \ + \ + friend class JObjectWrapper; \ +} + +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(void); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jboolean); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jbyte); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jchar); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jshort); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jint); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jlong); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jfloat); +DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS(jdouble); + +#pragma pop_macro("DEFINE_PRIMITIVE_NON_VIRTUAL_METHOD_CLASS") +/// @endcond + + +/// JNonvirtualMethod specialization for references that wraps the return value in a @ref local_ref +template +class JNonvirtualMethod : public JMethodBase { + static_assert(IsPlainJniReference(), "T* must be a JNI reference"); + + public: + using JMethodBase::JMethodBase; + JNonvirtualMethod() noexcept {}; + JNonvirtualMethod(const JNonvirtualMethod& other) noexcept = default; + + /// Invoke a method and return a local reference wrapping the result + local_ref operator()(alias_ref self, jclass cls, Args... args); + + friend class JObjectWrapper; +}; + + +/** + * JField represents typed fields and simplifies their access. Note that object types return + * raw pointers which generally should promptly get a wrap_local treatment. + */ +template +class JField { + static_assert(IsJniScalar(), "T must be a JNI scalar"); + + public: + /// Wraps an existing field id + explicit JField(jfieldID field = nullptr) noexcept; + + /// Verify that the id is valid + explicit operator bool() const noexcept; + + /// Access the wrapped id + jfieldID getId() const noexcept; + + private: + jfieldID field_id_; + + /// Get field value + /// @pre object != nullptr + T get(jobject object) const noexcept; + + /// Set field value + /// @pre object != nullptr + void set(jobject object, T value) noexcept; + + friend class JObjectWrapper; +}; + + +/** + * JStaticField represents typed fields and simplifies their access. Note that object types + * return raw pointers which generally should promptly get a wrap_local treatment. + */ +template +class JStaticField { + static_assert(IsJniScalar(), "T must be a JNI scalar"); + + public: + /// Wraps an existing field id + explicit JStaticField(jfieldID field = nullptr) noexcept; + + /// Verify that the id is valid + explicit operator bool() const noexcept; + + /// Access the wrapped id + jfieldID getId() const noexcept; + + private: + jfieldID field_id_; + + /// Get field value + /// @pre object != nullptr + T get(jclass jcls) const noexcept; + + /// Set field value + /// @pre object != nullptr + void set(jclass jcls, T value) noexcept; + + friend class JObjectWrapper; + +}; + + +/// Type traits for Java types (currently providing Java type descriptors) +template +struct jtype_traits; + + +/// Type traits for Java methods (currently providing Java type descriptors) +template +struct jmethod_traits; + +/// Template magic to provide @ref jmethod_traits +template +struct jmethod_traits { + static std::string descriptor(); + static std::string constructor_descriptor(); +}; + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators-inl.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators-inl.h new file mode 100644 index 000000000..d60c90022 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators-inl.h @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#include +#include +#include + +#include "Exceptions.h" +#include "References.h" + +namespace facebook { +namespace jni { + +/// @cond INTERNAL +namespace internal { + +// Statistics mostly provided for test (only updated if FBJNI_DEBUG_REFS is defined) +struct ReferenceStats { + std::atomic_uint locals_deleted, globals_deleted, weaks_deleted; + + void reset() noexcept; +}; + +extern ReferenceStats g_reference_stats; +} +/// @endcond + + +// LocalReferenceAllocator ///////////////////////////////////////////////////////////////////////// + +inline jobject LocalReferenceAllocator::newReference(jobject original) const { + internal::dbglog("Local new: %p", original); + auto ref = internal::getEnv()->NewLocalRef(original); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return ref; +} + +inline void LocalReferenceAllocator::deleteReference(jobject reference) const noexcept { + internal::dbglog("Local release: %p", reference); + + if (reference) { + #ifdef FBJNI_DEBUG_REFS + ++internal::g_reference_stats.locals_deleted; + #endif + assert(verifyReference(reference)); + internal::getEnv()->DeleteLocalRef(reference); + } +} + +inline bool LocalReferenceAllocator::verifyReference(jobject reference) const noexcept { + if (!reference || !internal::doesGetObjectRefTypeWork()) { + return true; + } + return internal::getEnv()->GetObjectRefType(reference) == JNILocalRefType; +} + + +// GlobalReferenceAllocator //////////////////////////////////////////////////////////////////////// + +inline jobject GlobalReferenceAllocator::newReference(jobject original) const { + internal::dbglog("Global new: %p", original); + auto ref = internal::getEnv()->NewGlobalRef(original); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return ref; +} + +inline void GlobalReferenceAllocator::deleteReference(jobject reference) const noexcept { + internal::dbglog("Global release: %p", reference); + + if (reference) { + #ifdef FBJNI_DEBUG_REFS + ++internal::g_reference_stats.globals_deleted; + #endif + assert(verifyReference(reference)); + internal::getEnv()->DeleteGlobalRef(reference); + } +} + +inline bool GlobalReferenceAllocator::verifyReference(jobject reference) const noexcept { + if (!reference || !internal::doesGetObjectRefTypeWork()) { + return true; + } + return internal::getEnv()->GetObjectRefType(reference) == JNIGlobalRefType; +} + + +// WeakGlobalReferenceAllocator //////////////////////////////////////////////////////////////////// + +inline jobject WeakGlobalReferenceAllocator::newReference(jobject original) const { + internal::dbglog("Weak global new: %p", original); + auto ref = internal::getEnv()->NewWeakGlobalRef(original); + FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); + return ref; +} + +inline void WeakGlobalReferenceAllocator::deleteReference(jobject reference) const noexcept { + internal::dbglog("Weak Global release: %p", reference); + + if (reference) { + #ifdef FBJNI_DEBUG_REFS + ++internal::g_reference_stats.weaks_deleted; + #endif + assert(verifyReference(reference)); + internal::getEnv()->DeleteWeakGlobalRef(reference); + } +} + +inline bool WeakGlobalReferenceAllocator::verifyReference(jobject reference) const noexcept { + if (!reference || !internal::doesGetObjectRefTypeWork()) { + return true; + } + return internal::getEnv()->GetObjectRefType(reference) == JNIWeakGlobalRefType; +} + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators.h new file mode 100644 index 000000000..ee328e071 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/ReferenceAllocators.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + * @file ReferenceAllocators.h + * + * Reference allocators are used to create and delete various classes of JNI references (local, + * global, and weak global). + */ + +#pragma once + +#include "Common.h" + +namespace facebook { namespace jni { + +/// Allocator that handles local references +class LocalReferenceAllocator { + public: + jobject newReference(jobject original) const; + void deleteReference(jobject reference) const noexcept; + bool verifyReference(jobject reference) const noexcept; +}; + +/// Allocator that handles global references +class GlobalReferenceAllocator { + public: + jobject newReference(jobject original) const; + void deleteReference(jobject reference) const noexcept; + bool verifyReference(jobject reference) const noexcept; +}; + +/// Allocator that handles weak global references +class WeakGlobalReferenceAllocator { + public: + jobject newReference(jobject original) const; + void deleteReference(jobject reference) const noexcept; + bool verifyReference(jobject reference) const noexcept; +}; + +/// @cond INTERNAL +namespace internal { + +/** + * @return true iff env->GetObjectRefType is expected to work properly. + */ +bool doesGetObjectRefTypeWork(); + +} +/// @endcond + +}} + +#include "ReferenceAllocators-inl.h" diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/References-inl.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/References-inl.h new file mode 100644 index 000000000..32f23493b --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/References-inl.h @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#include +#include "CoreClasses.h" + +namespace facebook { +namespace jni { + +template +inline enable_if_t(), local_ref> adopt_local(T ref) noexcept { + return local_ref{ref}; +} + +template +inline enable_if_t(), global_ref> adopt_global(T ref) noexcept { + return global_ref{ref}; +} + +template +inline enable_if_t(), weak_ref> adopt_weak_global(T ref) noexcept { + return weak_ref{ref}; +} + + +template +inline enable_if_t(), alias_ref> wrap_alias(T ref) noexcept { + return alias_ref(ref); +} + + +template +enable_if_t(), alias_ref> wrap_alias(T ref) noexcept; + + +template +inline enable_if_t(), T> getPlainJniReference(T ref) { + return ref; +} + +template +inline T getPlainJniReference(alias_ref ref) { + return ref.get(); +} + +template +inline T getPlainJniReference(const base_owned_ref& ref) { + return ref.getPlainJniReference(); +} + + +namespace internal { + +template +enable_if_t(), plain_jni_reference_t> make_ref(const T& reference) { + auto old_reference = getPlainJniReference(reference); + if (!old_reference) { + return nullptr; + } + + auto ref = Alloc{}.newReference(old_reference); + if (!ref) { + // Note that we end up here if we pass a weak ref that refers to a collected object. + // Thus, it's hard to come up with a reason why this function should be used with + // weak references. + throw std::bad_alloc{}; + } + + return static_cast>(ref); +} + +} + +template +enable_if_t(), local_ref>> +make_local(const T& ref) { + return adopt_local(internal::make_ref(ref)); +} + +template +enable_if_t(), global_ref>> +make_global(const T& ref) { + return adopt_global(internal::make_ref(ref)); +} + +template +enable_if_t(), weak_ref>> +make_weak(const T& ref) { + return adopt_weak_global(internal::make_ref(ref)); +} + +template +inline enable_if_t() && IsNonWeakReference(), bool> +operator==(const T1& a, const T2& b) { + return isSameObject(getPlainJniReference(a), getPlainJniReference(b)); +} + +template +inline enable_if_t() && IsNonWeakReference(), bool> +operator!=(const T1& a, const T2& b) { + return !(a == b); +} + + +// base_owned_ref /////////////////////////////////////////////////////////////////////// + +template +inline constexpr base_owned_ref::base_owned_ref() noexcept + : object_{nullptr} +{} + +template +inline constexpr base_owned_ref::base_owned_ref( + std::nullptr_t t) noexcept + : object_{nullptr} +{} + +template +inline base_owned_ref::base_owned_ref( + const base_owned_ref& other) + : object_{Alloc{}.newReference(other.getPlainJniReference())} +{} + +template +inline facebook::jni::base_owned_ref::base_owned_ref( + T reference) noexcept + : object_{reference} { + assert(Alloc{}.verifyReference(reference)); + internal::dbglog("New wrapped ref=%p this=%p", getPlainJniReference(), this); +} + +template +inline base_owned_ref::base_owned_ref( + base_owned_ref&& other) noexcept + : object_{other.object_} { + internal::dbglog("New move from ref=%p other=%p", other.getPlainJniReference(), &other); + internal::dbglog("New move to ref=%p this=%p", getPlainJniReference(), this); + // JObjectWrapper is a simple type and does not support move semantics so we explicitly + // clear other + other.object_.set(nullptr); +} + +template +inline base_owned_ref::~base_owned_ref() noexcept { + reset(); + internal::dbglog("Ref destruct ref=%p this=%p", getPlainJniReference(), this); +} + +template +inline T base_owned_ref::release() noexcept { + auto value = getPlainJniReference(); + internal::dbglog("Ref release ref=%p this=%p", value, this); + object_.set(nullptr); + return value; +} + +template +inline void base_owned_ref::reset() noexcept { + reset(nullptr); +} + +template +inline void base_owned_ref::reset(T reference) noexcept { + if (getPlainJniReference()) { + assert(Alloc{}.verifyReference(reference)); + Alloc{}.deleteReference(getPlainJniReference()); + } + object_.set(reference); +} + +template +inline T base_owned_ref::getPlainJniReference() const noexcept { + return static_cast(object_.get()); +} + + +// weak_ref /////////////////////////////////////////////////////////////////////// + +template +inline weak_ref& weak_ref::operator=( + const weak_ref& other) { + auto otherCopy = other; + swap(*this, otherCopy); + return *this; +} + +template +inline weak_ref& weak_ref::operator=( + weak_ref&& other) noexcept { + internal::dbglog("Op= move ref=%p this=%p oref=%p other=%p", + getPlainJniReference(), this, other.getPlainJniReference(), &other); + reset(other.release()); + return *this; +} + +template +local_ref weak_ref::lockLocal() { + return adopt_local(static_cast(LocalReferenceAllocator{}.newReference(getPlainJniReference()))); +} + +template +global_ref weak_ref::lockGlobal() { + return adopt_global(static_cast(GlobalReferenceAllocator{}.newReference(getPlainJniReference()))); +} + +template +inline void swap( + weak_ref& a, + weak_ref& b) noexcept { + internal::dbglog("Ref swap a.ref=%p a=%p b.ref=%p b=%p", + a.getPlainJniReference(), &a, b.getPlainJniReference(), &b); + using std::swap; + swap(a.object_, b.object_); +} + + +// basic_strong_ref //////////////////////////////////////////////////////////////////////////// + +template +inline basic_strong_ref& basic_strong_ref::operator=( + const basic_strong_ref& other) { + auto otherCopy = other; + swap(*this, otherCopy); + return *this; +} + +template +inline basic_strong_ref& basic_strong_ref::operator=( + basic_strong_ref&& other) noexcept { + internal::dbglog("Op= move ref=%p this=%p oref=%p other=%p", + getPlainJniReference(), this, other.getPlainJniReference(), &other); + reset(other.release()); + return *this; +} + +template +inline alias_ref basic_strong_ref::releaseAlias() noexcept { + return wrap_alias(release()); +} + +template +inline basic_strong_ref::operator bool() const noexcept { + return get() != nullptr; +} + +template +inline T basic_strong_ref::get() const noexcept { + return getPlainJniReference(); +} + +template +inline JObjectWrapper* basic_strong_ref::operator->() noexcept { + return &object_; +} + +template +inline const JObjectWrapper* basic_strong_ref::operator->() const noexcept { + return &object_; +} + +template +inline JObjectWrapper& basic_strong_ref::operator*() noexcept { + return object_; +} + +template +inline const JObjectWrapper& basic_strong_ref::operator*() const noexcept { + return object_; +} + +template +inline void swap( + basic_strong_ref& a, + basic_strong_ref& b) noexcept { + internal::dbglog("Ref swap a.ref=%p a=%p b.ref=%p b=%p", + a.getPlainJniReference(), &a, b.getPlainJniReference(), &b); + using std::swap; + swap(a.object_, b.object_); +} + + +// alias_ref ////////////////////////////////////////////////////////////////////////////// + +template +inline constexpr alias_ref::alias_ref() noexcept + : object_{nullptr} +{} + +template +inline constexpr alias_ref::alias_ref(std::nullptr_t) noexcept + : object_{nullptr} +{} + +template +inline alias_ref::alias_ref(const alias_ref& other) noexcept + : object_{other.object_} +{} + + +template +inline alias_ref::alias_ref(T ref) noexcept + : object_{ref} { + assert( + LocalReferenceAllocator{}.verifyReference(ref) || + GlobalReferenceAllocator{}.verifyReference(ref)); +} + +template +template +inline alias_ref::alias_ref(alias_ref other) noexcept + : object_{other.get()} +{} + +template +template +inline alias_ref::alias_ref(const basic_strong_ref& other) noexcept + : object_{other.get()} +{} + +template +inline alias_ref& alias_ref::operator=(alias_ref other) noexcept { + swap(*this, other); + return *this; +} + +template +inline alias_ref::operator bool() const noexcept { + return get() != nullptr; +} + +template +inline T facebook::jni::alias_ref::get() const noexcept { + return static_cast(object_.get()); +} + +template +inline JObjectWrapper* alias_ref::operator->() noexcept { + return &object_; +} + +template +inline const JObjectWrapper* alias_ref::operator->() const noexcept { + return &object_; +} + +template +inline JObjectWrapper& alias_ref::operator*() noexcept { + return object_; +} + +template +inline const JObjectWrapper& alias_ref::operator*() const noexcept { + return object_; +} + +template +inline void swap(alias_ref& a, alias_ref& b) noexcept { + using std::swap; + swap(a.object_, b.object_); +} + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/References.cpp b/ReactAndroid/src/main/jni/first-party/jni/fbjni/References.cpp new file mode 100644 index 000000000..0ee4c9e8f --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/References.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "References.h" + +namespace facebook { +namespace jni { + +JniLocalScope::JniLocalScope(JNIEnv* env, jint capacity) + : env_(env) { + hasFrame_ = false; + auto pushResult = env->PushLocalFrame(capacity); + FACEBOOK_JNI_THROW_EXCEPTION_IF(pushResult < 0); + hasFrame_ = true; +} + +JniLocalScope::~JniLocalScope() { + if (hasFrame_) { + env_->PopLocalFrame(nullptr); + } +} + +namespace internal { + +// Default implementation always returns true. +// Platform-specific sources can override this. +bool doesGetObjectRefTypeWork() __attribute__ ((weak)); +bool doesGetObjectRefTypeWork() { + return true; +} + +} + +} +} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/References.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/References.h new file mode 100644 index 000000000..c7576b7d1 --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/References.h @@ -0,0 +1,506 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + + +/** @file References.h + * + * Functionality similar to smart pointers, but for references into the VM. Four main reference + * types are provided: local_ref, global_ref, weak_ref, and alias_ref. All are generic + * templates that and refer to objects in the jobject hierarchy. The type of the referred objects + * are specified using the template parameter. All reference types except alias_ref own their + * underlying reference, just as a std smart pointer owns the underlying raw pointer. In the context + * of std smart pointers, these references behave like unique_ptr, and have basically the same + * interface. Thus, when the reference is destructed, the plain JNI reference, i.e. the underlying + * JNI reference (like the parameters passed directly to JNI functions), is released. The alias + * references provides no ownership and is a simple wrapper for plain JNI references. + * + * All but the weak references provides access to the underlying object using dereferencing, and a + * get() method. It is also possible to convert these references to booleans to test for nullity. + * To access the underlying object of a weak reference, the reference must either be released, or + * the weak reference can be used to create a local or global reference. + * + * An owning reference is created either by moving the reference from an existing owned reference, + * by copying an existing owned reference (which creates a new underlying reference), by using the + * default constructor which initialize the reference to nullptr, or by using a helper function. The + * helper function exist in two flavors: make_XXX or adopt_XXX. + * + * Adopting takes a plain JNI reference and wrap it in an owned reference. It takes ownership of the + * plain JNI reference so be sure that no one else owns the reference when you adopt it, and make + * sure that you know what kind of reference it is. + * + * New owned references can be created from existing plain JNI references, alias references, local + * references, and global references (i.e. non-weak references) using the make_local, make_global, + * and make_weak functions. + * + * Alias references can be implicitly initialized using global, local and plain JNI references using + * the wrap_alias function. Here, we don't assume ownership of the passed-in reference, but rather + * create a separate reference that we do own, leaving the passed-in reference to its fate. + * + * Similar rules apply for assignment. An owned reference can be copy or move assigned using a smart + * reference of the same type. In the case of copy assignment a new reference is created. Alias + * reference can also be assigned new values, but since they are simple wrappers of plain JNI + * references there is no move semantics involved. + * + * Alias references are special in that they do not own the object and can therefore safely be + * converted to and from its corresponding plain JNI reference. They are useful as parameters of + * functions that do not affect the lifetime of a reference. Usage can be compared with using plain + * JNI pointers as parameters where a function does not take ownership of the underlying object. + * + * The local, global, and alias references makes it possible to access methods in the underlying + * objects. A core set of classes are implemented in CoreClasses.h, and user defined wrappers are + * supported (see example below). The wrappers also supports inheritance so a wrapper can inherit + * from another wrapper to gain access to its functionality. As an example the jstring wrapper + * inherits from the jobject wrapper, so does the jclass wrapper. That means that you can for + * example call the toString() method using the jclass wrapper, or any other class that inherits + * from the jobject wrapper. + * + * Note that the wrappers are parameterized on the static type of your (jobject) pointer, thus if + * you have a jobject that refers to a Java String you will need to cast it to jstring to get the + * jstring wrapper. This also mean that if you make a down cast that is invalid there will be no one + * stopping you and the wrappers currently does not detect this which can cause crashes. Thus, cast + * wisely. + * + * @include WrapperSample.cpp + */ + +#pragma once + +#include +#include +#include + +#include + +#include "ReferenceAllocators.h" +#include "TypeTraits.h" + +namespace facebook { +namespace jni { + +/** + * The JObjectWrapper is specialized to provide functionality for various Java classes, some + * specializations are provided, and it is easy to add your own. See example + * @sample WrapperSample.cpp + */ +template +class JObjectWrapper; + + +template +class base_owned_ref; + +template +class basic_strong_ref; + +template +class weak_ref; + +template +class alias_ref; + + +/// A smart unique reference owning a local JNI reference +template +using local_ref = basic_strong_ref; + +/// A smart unique reference owning a global JNI reference +template +using global_ref = basic_strong_ref; + + +/// Convenience function to wrap an existing local reference +template +enable_if_t(), local_ref> adopt_local(T ref) noexcept; + +/// Convenience function to wrap an existing global reference +template +enable_if_t(), global_ref> adopt_global(T ref) noexcept; + +/// Convenience function to wrap an existing weak reference +template +enable_if_t(), weak_ref> adopt_weak_global(T ref) noexcept; + + +/** + * Create a new local reference from an existing reference + * + * @param ref a plain JNI, alias, or strong reference + * @return an owned local reference (referring to null if the input does) + * @throws std::bad_alloc if the JNI reference could not be created + */ +template +enable_if_t(), local_ref>> +make_local(const T& r); + +/** + * Create a new global reference from an existing reference + * + * @param ref a plain JNI, alias, or strong reference + * @return an owned global reference (referring to null if the input does) + * @throws std::bad_alloc if the JNI reference could not be created + */ +template +enable_if_t(), global_ref>> +make_global(const T& r); + +/** + * Create a new weak global reference from an existing reference + * + * @param ref a plain JNI, alias, or strong reference + * @return an owned weak global reference (referring to null if the input does) + * @throws std::bad_alloc if the returned reference is null + */ +template +enable_if_t(), weak_ref>> +make_weak(const T& r); + + +/// Swaps two owning references of the same type +template +void swap(weak_ref& a, weak_ref& b) noexcept; + +/// Swaps two owning references of the same type +template +void swap(basic_strong_ref& a, basic_strong_ref& b) noexcept; + +/** + * Retrieve the plain reference from a plain reference. + */ +template +enable_if_t(), T> getPlainJniReference(T ref); + +/** + * Retrieve the plain reference from an alias reference. + */ +template +T getPlainJniReference(alias_ref ref); + +/** + * Retrieve the plain JNI reference from any reference owned reference. + */ +template +T getPlainJniReference(const base_owned_ref& ref); + +/** + * Compare two references to see if they refer to the same object + */ +template +enable_if_t() && IsNonWeakReference(), bool> +operator==(const T1& a, const T2& b); + +/** + * Compare two references to see if they don't refer to the same object + */ +template +enable_if_t() && IsNonWeakReference(), bool> +operator!=(const T1& a, const T2& b); + + +template +class base_owned_ref { + + static_assert(IsPlainJniReference(), "T must be a JNI reference"); + + public: + + /** + * Release the ownership and set the reference to null. Thus no deleter is invoked. + * @return Returns the reference + */ + T release() noexcept; + + /** + * Reset the reference to refer to nullptr. + */ + void reset() noexcept; + + protected: + + JObjectWrapper object_; + + /* + * Wrap an existing reference and transfers its ownership to the newly created unique reference. + * NB! Does not create a new reference + */ + explicit base_owned_ref(T reference) noexcept; + + /// Create a null reference + constexpr base_owned_ref() noexcept; + + /// Create a null reference + constexpr explicit base_owned_ref(std::nullptr_t) noexcept; + + /// Copy constructor (note creates a new reference) + base_owned_ref(const base_owned_ref& other); + + /// Transfers ownership of an underlying reference from one unique reference to another + base_owned_ref(base_owned_ref&& other) noexcept; + + /// The delete the underlying reference if applicable + ~base_owned_ref() noexcept; + + + /// Assignment operator (note creates a new reference) + base_owned_ref& operator=(const base_owned_ref& other); + + /// Assignment by moving a reference thus not creating a new reference + base_owned_ref& operator=(base_owned_ref&& rhs) noexcept; + + + T getPlainJniReference() const noexcept; + + void reset(T reference) noexcept; + + + friend T jni::getPlainJniReference<>(const base_owned_ref& ref); +}; + + +/** + * A smart reference that owns its underlying JNI reference. The class provides basic + * functionality to handle a reference but gives no access to it unless the reference is + * released, thus no longer owned. The API is stolen with pride from unique_ptr and the + * semantics should be basically the same. This class should not be used directly, instead use + * @ref weak_ref + */ +template +class weak_ref : public base_owned_ref { + + static_assert(IsPlainJniReference(), "T must be a JNI reference"); + + public: + + using PlainJniType = T; + using Allocator = WeakGlobalReferenceAllocator; + + + /// Create a null reference + constexpr weak_ref() noexcept + : base_owned_ref{} {} + + /// Create a null reference + constexpr explicit weak_ref(std::nullptr_t) noexcept + : base_owned_ref{nullptr} {} + + /// Copy constructor (note creates a new reference) + weak_ref(const weak_ref& other) + : base_owned_ref{other} {} + + /// Transfers ownership of an underlying reference from one unique reference to another + weak_ref(weak_ref&& other) noexcept + : base_owned_ref{std::move(other)} {} + + + /// Assignment operator (note creates a new reference) + weak_ref& operator=(const weak_ref& other); + + /// Assignment by moving a reference thus not creating a new reference + weak_ref& operator=(weak_ref&& rhs) noexcept; + + + // Creates an owned local reference to the referred object or to null if the object is reclaimed + local_ref lockLocal(); + + // Creates an owned global reference to the referred object or to null if the object is reclaimed + global_ref lockGlobal(); + + private: + + using base_owned_ref::getPlainJniReference; + + /* + * Wrap an existing reference and transfers its ownership to the newly created unique reference. + * NB! Does not create a new reference + */ + explicit weak_ref(T reference) noexcept + : base_owned_ref{reference} {} + + + template friend class weak_ref; + friend weak_ref(), T>> + adopt_weak_global(T ref) noexcept; + friend void swap(weak_ref& a, weak_ref& b) noexcept; +}; + + +/** + * A class representing owned strong references to Java objects. This class + * should not be used directly, instead use @ref local_ref, or @ref global_ref. + */ +template +class basic_strong_ref : public base_owned_ref { + + static_assert(IsPlainJniReference(), "T must be a JNI reference"); + + public: + + using PlainJniType = T; + using Allocator = Alloc; + + using base_owned_ref::release; + using base_owned_ref::reset; + + + /// Create a null reference + constexpr basic_strong_ref() noexcept + : base_owned_ref{} {} + + /// Create a null reference + constexpr explicit basic_strong_ref(std::nullptr_t) noexcept + : base_owned_ref{nullptr} {} + + /// Copy constructor (note creates a new reference) + basic_strong_ref(const basic_strong_ref& other) + : base_owned_ref{other} {} + + /// Transfers ownership of an underlying reference from one unique reference to another + basic_strong_ref(basic_strong_ref&& other) noexcept + : base_owned_ref{std::move(other)} {} + + + /// Assignment operator (note creates a new reference) + basic_strong_ref& operator=(const basic_strong_ref& other); + + /// Assignment by moving a reference thus not creating a new reference + basic_strong_ref& operator=(basic_strong_ref&& rhs) noexcept; + + + /// Release the ownership of the reference and return the wrapped reference in an alias + alias_ref releaseAlias() noexcept; + + /// Checks if the reference points to a non-null object + explicit operator bool() const noexcept; + + /// Get the plain JNI reference + T get() const noexcept; + + /// Access the functionality provided by the object wrappers + JObjectWrapper* operator->() noexcept; + + /// Access the functionality provided by the object wrappers + const JObjectWrapper* operator->() const noexcept; + + /// Provide a reference to the underlying wrapper (be sure that it is non-null before invoking) + JObjectWrapper& operator*() noexcept; + + /// Provide a const reference to the underlying wrapper (be sure that it is non-null + /// before invoking) + const JObjectWrapper& operator*() const noexcept; + + private: + + using base_owned_ref::object_; + using base_owned_ref::getPlainJniReference; + + /* + * Wrap an existing reference and transfers its ownership to the newly created unique reference. + * NB! Does not create a new reference + */ + explicit basic_strong_ref(T reference) noexcept + : base_owned_ref{reference} {} + + + friend enable_if_t(), local_ref> adopt_local(T ref) noexcept; + friend enable_if_t(), global_ref> adopt_global(T ref) noexcept; + friend void swap(basic_strong_ref& a, basic_strong_ref& b) noexcept; +}; + + +template +enable_if_t(), alias_ref> wrap_alias(T ref) noexcept; + +/// Swaps to alias referencec of the same type +template +void swap(alias_ref& a, alias_ref& b) noexcept; + +/** + * A non-owning variant of the smart references (a dumb reference). These references still provide + * access to the functionality of the @ref JObjectWrapper specializations including exception + * handling and ease of use. Use this representation when you don't want to claim ownership of the + * underlying reference (compare to using raw pointers instead of smart pointers.) For symmetry use + * @ref alias_ref instead of this class. + */ +template +class alias_ref { + + static_assert(IsPlainJniReference(), "T must be a JNI reference"); + + public: + + using PlainJniType = T; + + + /// Create a null reference + constexpr alias_ref() noexcept; + + /// Create a null reference + constexpr alias_ref(std::nullptr_t) noexcept; + + /// Copy constructor + alias_ref(const alias_ref& other) noexcept; + + /// Wrap an existing plain JNI reference + alias_ref(T ref) noexcept; + + /// Wrap an existing smart reference of any type convertible to T + template(), T>> + alias_ref(alias_ref other) noexcept; + + /// Wrap an existing alias reference of a type convertible to T + template(), T>> + alias_ref(const basic_strong_ref& other) noexcept; + + + /// Assignment operator + alias_ref& operator=(alias_ref other) noexcept; + + /// Checks if the reference points to a non-null object + explicit operator bool() const noexcept; + + /// Converts back to a plain JNI reference + T get() const noexcept; + + /// Access the functionality provided by the object wrappers + JObjectWrapper* operator->() noexcept; + + /// Access the functionality provided by the object wrappers + const JObjectWrapper* operator->() const noexcept; + + /// Provide a guaranteed non-null reference (be sure that it is non-null before invoking) + JObjectWrapper& operator*() noexcept; + + /// Provide a guaranteed non-null reference (be sure that it is non-null before invoking) + const JObjectWrapper& operator*() const noexcept; + + private: + JObjectWrapper object_; + + friend void swap(alias_ref& a, alias_ref& b) noexcept; +}; + + +/** + * RAII object to create a local JNI frame, using PushLocalFrame/PopLocalFrame. + * + * This is useful when you have a call which is initiated from C++-land, and therefore + * doesn't automatically get a local JNI frame managed for you by the JNI framework. + */ +class JniLocalScope { +public: + JniLocalScope(JNIEnv* p_env, jint capacity); + ~JniLocalScope(); + +private: + JNIEnv* env_; + bool hasFrame_; +}; + +}} + +#include "References-inl.h" diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration-inl.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration-inl.h new file mode 100644 index 000000000..d0c31579e --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration-inl.h @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#include "Exceptions.h" +#include "Hybrid.h" + +namespace facebook { +namespace jni { + +namespace detail { + +// convert to HybridClass* from jhybridobject +template +struct Convert< + T, typename std::enable_if< + std::is_base_of::type>::value>::type> { + typedef typename std::remove_pointer::type::jhybridobject jniType; + static T fromJni(jniType t) { + if (t == nullptr) { + return nullptr; + } + return facebook::jni::cthis(wrap_alias(t)); + } + // There is no automatic return conversion for objects. +}; + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(void (*)(JNIEnv*, C, Args... args)) { + struct funcWrapper { + static void call(JNIEnv* env, jobject obj, Args... args) { + // Note that if func was declared noexcept, then both gcc and clang are smart + // enough to elide the try/catch. + try { + (*func)(env, static_cast(obj), args...); + } catch (...) { + translatePendingCppExceptionToJavaException(); + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(void (*)(C, Args... args)) { + struct funcWrapper { + static void call(JNIEnv* env, jobject obj, Args... args) { + // Note that if func was declared noexcept, then both gcc and clang are smart + // enough to elide the try/catch. + try { + (void) env; + (*func)(static_cast(obj), args...); + } catch (...) { + translatePendingCppExceptionToJavaException(); + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(R (*)(JNIEnv*, C, Args... args)) { + struct funcWrapper { + static R call(JNIEnv* env, jobject obj, Args... args) { + try { + return (*func)(env, static_cast(obj), args...); + } catch (...) { + translatePendingCppExceptionToJavaException(); + return R{}; + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(void (*)(alias_ref, Args... args)) { + struct funcWrapper { + static void call(JNIEnv*, jobject obj, + typename Convert::type>::jniType... args) { + try { + (*func)(static_cast(obj), Convert::type>::fromJni(args)...); + } catch (...) { + translatePendingCppExceptionToJavaException(); + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(R (*)(alias_ref, Args... args)) { + struct funcWrapper { + typedef typename Convert::type>::jniType jniRet; + + static jniRet call(JNIEnv*, jobject obj, + typename Convert::type>::jniType... args) { + try { + return Convert::type>::toJniRet( + (*func)(static_cast(obj), Convert::type>::fromJni(args)...)); + } catch (...) { + translatePendingCppExceptionToJavaException(); + return jniRet{}; + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(void (C::*method0)(Args... args)) { + struct funcWrapper { + static void call(JNIEnv* env, jobject obj, + typename Convert::type>::jniType... args) { + try { + try { + auto aref = wrap_alias(static_cast(obj)); + // This is usually a noop, but if the hybrid object is a + // base class of other classes which register JNI methods, + // this will get the right type for the registered method. + auto cobj = static_cast(facebook::jni::cthis(aref)); + (cobj->*method)(Convert::type>::fromJni(args)...); + } catch (const std::exception& ex) { + C::mapException(ex); + throw; + } + } catch (...) { + translatePendingCppExceptionToJavaException(); + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline NativeMethodWrapper* exceptionWrapJNIMethod(R (C::*method0)(Args... args)) { + struct funcWrapper { + typedef typename Convert::type>::jniType jniRet; + + static jniRet call(JNIEnv* env, jobject obj, + typename Convert::type>::jniType... args) { + try { + try { + auto aref = wrap_alias(static_cast(obj)); + // This is usually a noop, but if the hybrid object is a + // base class of other classes which register JNI methods, + // this will get the right type for the registered method. + auto cobj = static_cast(facebook::jni::cthis(aref)); + return Convert::type>::toJniRet( + (cobj->*method)(Convert::type>::fromJni(args)...)); + } catch (const std::exception& ex) { + C::mapException(ex); + throw; + } + } catch (...) { + translatePendingCppExceptionToJavaException(); + return jniRet{}; + } + } + }; + + // This intentionally erases the real type; JNI will do it anyway + return reinterpret_cast(&(funcWrapper::call)); +} + +template +inline std::string makeDescriptor(R (*)(JNIEnv*, C, Args... args)) { + return jmethod_traits::descriptor(); +} + +template +inline std::string makeDescriptor(R (*)(alias_ref, Args... args)) { + typedef typename Convert::type>::jniType jniRet; + return jmethod_traits::type>::jniType...)> + ::descriptor(); +} + +template +inline std::string makeDescriptor(R (C::*)(Args... args)) { + typedef typename Convert::type>::jniType jniRet; + return jmethod_traits::type>::jniType...)> + ::descriptor(); +} + +} + +}} diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration.h new file mode 100644 index 000000000..e690f96ba --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/Registration.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#include +#include "References.h" + +namespace facebook { +namespace jni { + +namespace detail { + +// This uses the real JNI function as a non-type template parameter to +// cause a (static member) function to exist with the same signature, +// but with try/catch exception translation. +template +NativeMethodWrapper* exceptionWrapJNIMethod(void (*func0)(JNIEnv*, jobject, Args... args)); + +// Same as above, but for non-void return types. +template +NativeMethodWrapper* exceptionWrapJNIMethod(R (*func0)(JNIEnv*, jobject, Args... args)); + +// Automatically wrap object argument, and don't take env explicitly. +template +NativeMethodWrapper* exceptionWrapJNIMethod(void (*func0)(alias_ref, Args... args)); + +// Automatically wrap object argument, and don't take env explicitly, +// non-void return type. +template +NativeMethodWrapper* exceptionWrapJNIMethod(R (*func0)(alias_ref, Args... args)); + +// Extract C++ instance from object, and invoke given method on it. +template +NativeMethodWrapper* exceptionWrapJNIMethod(void (C::*method0)(Args... args)); + +// Extract C++ instance from object, and invoke given method on it, +// non-void return type +template +NativeMethodWrapper* exceptionWrapJNIMethod(R (C::*method0)(Args... args)); + +// This uses deduction to figure out the descriptor name if the types +// are primitive or have JObjectWrapper specializations. +template +std::string makeDescriptor(R (*func)(JNIEnv*, C, Args... args)); + +// This uses deduction to figure out the descriptor name if the types +// are primitive or have JObjectWrapper specializations. +template +std::string makeDescriptor(R (*func)(alias_ref, Args... args)); + +// This uses deduction to figure out the descriptor name if the types +// are primitive or have JObjectWrapper specializations. +template +std::string makeDescriptor(R (C::*method0)(Args... args)); + +} + +// We have to use macros here, because the func needs to be used +// as both a decltype expression argument and as a non-type template +// parameter, since C++ provides no way for translateException +// to deduce the type of its non-type template parameter. +// The empty string in the macros below ensures that name +// is always a string literal (because that syntax is only +// valid when name is a string literal). +#define makeNativeMethod2(name, func) \ + { name "", ::facebook::jni::detail::makeDescriptor(&func), \ + ::facebook::jni::detail::exceptionWrapJNIMethod(&func) } + +#define makeNativeMethod3(name, desc, func) \ + { name "", desc, \ + ::facebook::jni::detail::exceptionWrapJNIMethod(&func) } + +// Variadic template hacks to get macros with different numbers of +// arguments. Usage instructions are in CoreClasses.h. +#define makeNativeMethodN(a, b, c, count, ...) makeNativeMethod ## count +#define makeNativeMethod(...) makeNativeMethodN(__VA_ARGS__, 3, 2)(__VA_ARGS__) + +}} + +#include "Registration-inl.h" diff --git a/ReactAndroid/src/main/jni/first-party/jni/fbjni/TypeTraits.h b/ReactAndroid/src/main/jni/first-party/jni/fbjni/TypeTraits.h new file mode 100644 index 000000000..b4bdd15ea --- /dev/null +++ b/ReactAndroid/src/main/jni/first-party/jni/fbjni/TypeTraits.h @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#include + +namespace facebook { +namespace jni { + +/// Generic std::enable_if helper +template +using enable_if_t = typename std::enable_if::type; + +/// Generic std::is_convertible helper +template +constexpr bool IsConvertible() { + return std::is_convertible::value; +} + +template class TT, typename T> +struct is_instantiation_of : std::false_type {}; + +template class TT, typename... Ts> +struct is_instantiation_of> : std::true_type {}; + +template class TT, typename... Ts> +constexpr bool IsInstantiationOf() { + return is_instantiation_of::value; +} + +/// Metafunction to determine whether a type is a JNI reference or not +template +struct is_plain_jni_reference : + std::integral_constant::value && + std::is_base_of< + typename std::remove_pointer::type, + typename std::remove_pointer::type>::value> {}; + +/// Helper to simplify use of is_plain_jni_reference +template +constexpr bool IsPlainJniReference() { + return is_plain_jni_reference::value; +} + +/// Metafunction to determine whether a type is a primitive JNI type or not +template +struct is_jni_primitive : + std::integral_constant::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value> {}; + +/// Helper to simplify use of is_jni_primitive +template +constexpr bool IsJniPrimitive() { + return is_jni_primitive::value; +} + +/// Metafunction to determine if a type is a scalar (primitive or reference) JNI type +template +struct is_jni_scalar : + std::integral_constant::value || + is_jni_primitive::value> {}; + +/// Helper to simplify use of is_jni_scalar +template +constexpr bool IsJniScalar() { + return is_jni_scalar::value; +} + +// Metafunction to determine if a type is a JNI type +template +struct is_jni_type : + std::integral_constant::value || + std::is_void::value> {}; + +/// Helper to simplify use of is_jni_type +template +constexpr bool IsJniType() { + return is_jni_type::value; +} + +template +class weak_global_ref; + +template +class basic_strong_ref; + +template +class alias_ref; + +template +struct is_non_weak_reference : + std::integral_constant() || + IsInstantiationOf() || + IsInstantiationOf()> {}; + +template +constexpr bool IsNonWeakReference() { + return is_non_weak_reference::value; +} + +template +struct is_any_reference : + std::integral_constant() || + IsInstantiationOf() || + IsInstantiationOf() || + IsInstantiationOf()> {}; + +template +constexpr bool IsAnyReference() { + return is_any_reference::value; +} + +template +struct reference_traits { + static_assert(IsPlainJniReference(), "Need a plain JNI reference"); + using plain_jni_reference_t = T; +}; + +template