From a76c6c01a82aa54f8ccbe809e888944c8e6bc99b Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Sat, 13 Feb 2016 09:28:56 +0900 Subject: [PATCH 01/37] Add Share module --- Examples/UIExplorer/ShareExample.js | 111 ++++++++++++++++++ Examples/UIExplorer/UIExplorerList.android.js | 1 + Examples/UIExplorer/UIExplorerList.ios.js | 1 + Libraries/Share/Share.js | 81 +++++++++++++ Libraries/react-native/react-native.js | 1 + .../react/modules/share/ShareModule.java | 90 ++++++++++++++ .../react/shell/MainReactPackage.java | 2 + 7 files changed, 287 insertions(+) create mode 100644 Examples/UIExplorer/ShareExample.js create mode 100644 Libraries/Share/Share.js create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java diff --git a/Examples/UIExplorer/ShareExample.js b/Examples/UIExplorer/ShareExample.js new file mode 100644 index 00000000000000..0bafc64c5af49e --- /dev/null +++ b/Examples/UIExplorer/ShareExample.js @@ -0,0 +1,111 @@ +/** + * 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, + Text, + TextInput, + TouchableHighlight, + Share, +} = React; + +exports.framework = 'React'; +exports.title = 'Share'; +exports.description = 'Share data with other Apps.'; +exports.examples = [{ + title: 'Share Text Content', + render() { + return ; + } +}]; + +class ShareMessageExample extends React.Component { + + constructor(props) { + super(props); + + this.shareMessage = this.shareMessage.bind(this); + this.shareTextContent = this.shareTextContent.bind(this); + + this.state = { + result: '' + }; + } + + render() { + return ( + + + + Click to share message + + + + + Click to share message, URL and subject + + + {this.state.result} + + ); + } + + shareMessage() { + Share.shareTextContent({ + message: 'React Native | A framework for building native apps using React' + }) + .then((result) => { + if(!result) { + this.setState({result:'Canceled'}) + } else { + this.setState({result:result}) + } + }) + .catch((err) => console.error(err)) + } + shareTextContent() { + Share.shareTextContent({ + message: 'A framework for building native apps using React', + url: 'http://facebook.github.io/react-native/', + subject: 'React Native' + }, { + dialogTitle: 'Share React Native website', + excludedActivityTypes: [ + 'com.apple.UIKit.activity.PostToTwitter' + ] + }) + .then((result) => this.setState({result:result})) + .catch(()=> this.setState({result:'Canceled'})) + } + +} + + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + backgroundColor: '#eeeeee', + padding: 10, + }, +}); diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index 804ed69d4c59c5..ec3d27f0045faa 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -59,6 +59,7 @@ var APIS = [ require('./NetInfoExample'), require('./PanResponderExample'), require('./PointerEventsExample'), + require('./ShareExample'), require('./TimePickerAndroidExample'), require('./TimerExample'), require('./ToastAndroidExample.android'), diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index 5c33d7f645e539..36c26e8722c411 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -80,6 +80,7 @@ var APIS = [ require('./PointerEventsExample'), require('./PushNotificationIOSExample'), require('./RCTRootViewIOSExample'), + require('./ShareExample'), require('./StatusBarIOSExample'), require('./TimerExample'), require('./TransformExample'), diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js new file mode 100644 index 00000000000000..b92ea9b0d87e85 --- /dev/null +++ b/Libraries/Share/Share.js @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 Share + * @flow + */ +'use strict'; + +const Platform = require('Platform'); +const { + ActionSheetManager, + ShareModule +} = require('NativeModules'); +const invariant = require('invariant'); +const processColor = require('processColor'); + +class Share { + + /** + * Open a dialog to share text contents. + * + * ### Contents + * + * - `message` - a message to share + * - `url` - an URL to share. In Android, this will overwrite message + * - `subject` - subject of the message + * + * At least one of URL and message is required. + * + * ### Options + * + * #### iOS + * + * - `excludedActivityTypes` + * - `tintColor` + * + * #### Android + * + * - `dialogTitle` + */ + static shareTextContent(contents: Object, options?: Object): Promise { + invariant( + typeof contents === 'object' && contents !== null, + 'Contents must a valid object' + ); + invariant( + contents.url || contents.message, + 'At least one of URL and message is required' + ); + for(let content in contents) { + invariant( + typeof content === 'string', + 'Invalid Content: should be a string. Was: ' + content + ); + } + options = options || {}; + return Platform.OS === 'android' + ? ShareModule.shareTextContent(contents, options.dialogTitle) + : new Promise((resolve, reject) => { + ActionSheetManager.showShareActionSheetWithOptions( + {...contents, tintColor: processColor(options.tintColor)}, + console.error, + (success, activityType) => { + if(success) { + resolve(activityType) + } else { + reject() + } + } + ); + }); + } + +} + +module.exports = Share; \ No newline at end of file diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index e8eca3c72a5ad9..a41556a4d15581 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -82,6 +82,7 @@ var ReactNative = { get PixelRatio() { return require('PixelRatio'); }, get PushNotificationIOS() { return require('PushNotificationIOS'); }, get Settings() { return require('Settings'); }, + get Share() { return require('Share'); }, get StatusBarIOS() { return require('StatusBarIOS'); }, get StyleSheet() { return require('StyleSheet'); }, get TimePickerAndroid() { return require('TimePickerAndroid'); }, diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java new file mode 100644 index 00000000000000..71f227fd980e71 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of 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.share; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; + +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; + +/** + * Intent module. Launch other activities or open URLs. + */ +public class ShareModule extends ReactContextBaseJavaModule { + + private Promise mPromise; + + public ShareModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "ShareModule"; + } + + /** + * Open a chooser dialog to send text contents to other apps. + * + * Refer http://developer.android.com/intl/ko/training/sharing/send.html + * + * @param contents the data to send + * @param title the title of the chooser dialog + */ + @ReactMethod + public void shareTextContent(ReadableMap contents, String title, final Promise promise) { + Activity currentActivity = getCurrentActivity(); + + if (currentActivity == null) { + promise.reject("Activity doesn't exist"); + return; + } + + if (contents == null) { + throw new JSApplicationIllegalArgumentException("Invalid contents"); + } + + mPromise = promise; + + try { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setTypeAndNormalize("text/plain"); + + if(contents.hasKey("subject")) { + intent.putExtra(Intent.EXTRA_SUBJECT, contents.getString("subject")); + } + + if(contents.hasKey("message")) { + intent.putExtra(Intent.EXTRA_TEXT, contents.getString("message")); + } + + if(contents.hasKey("url")) { + intent.putExtra(Intent.EXTRA_TEXT, contents.getString("url")); // this will overwrite message + } + + currentActivity.startActivity(Intent.createChooser(intent, title)); + //TODO: use createChooser (Intent target, CharSequence title, IntentSender sender) after API level 22 + mPromise.resolve(true); + mPromise = null; + } catch (Exception e) { + mPromise.reject("Failed to open share dialog"); + mPromise = null; + } + + } + + +} \ No newline at end of file diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index d3ec2880ff71af..9b408dd8956751 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -29,6 +29,7 @@ import com.facebook.react.modules.location.LocationModule; import com.facebook.react.modules.netinfo.NetInfoModule; import com.facebook.react.modules.network.NetworkingModule; +import com.facebook.react.modules.share.ShareModule; import com.facebook.react.modules.statusbar.StatusBarModule; import com.facebook.react.modules.storage.AsyncStorageModule; import com.facebook.react.modules.timepicker.TimePickerDialogModule; @@ -78,6 +79,7 @@ public List createNativeModules(ReactApplicationContext reactConte new LocationModule(reactContext), new NetworkingModule(reactContext), new NetInfoModule(reactContext), + new ShareModule(reactContext), new StatusBarModule(reactContext), new TimePickerDialogModule(reactContext), new ToastModule(reactContext), From 424a665a2a329e4195b770aa9c9bf83c2841d7e6 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Sat, 13 Feb 2016 14:25:23 +0900 Subject: [PATCH 02/37] change the way handling promise reject --- Examples/UIExplorer/ShareExample.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Examples/UIExplorer/ShareExample.js b/Examples/UIExplorer/ShareExample.js index 0bafc64c5af49e..4e822dbd6b6c54 100644 --- a/Examples/UIExplorer/ShareExample.js +++ b/Examples/UIExplorer/ShareExample.js @@ -72,14 +72,8 @@ class ShareMessageExample extends React.Component { Share.shareTextContent({ message: 'React Native | A framework for building native apps using React' }) - .then((result) => { - if(!result) { - this.setState({result:'Canceled'}) - } else { - this.setState({result:result}) - } - }) - .catch((err) => console.error(err)) + .then((result) => this.setState({result:result})) + .catch(()=> this.setState({result:'Canceled'})) } shareTextContent() { Share.shareTextContent({ From 5da16c792097deba71613b22dd3d2cea4dabc261 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Sun, 14 Feb 2016 02:05:30 +0900 Subject: [PATCH 03/37] buck file added --- .../com/facebook/react/modules/share/BUCK | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK new file mode 100644 index 00000000000000..7802dfb629ad74 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK @@ -0,0 +1,19 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'share', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':share', +) From 8c3bece19603a688ab3118bfc30cd00c21252fff Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Sun, 14 Feb 2016 02:18:51 +0900 Subject: [PATCH 04/37] added buck target --- ReactAndroid/src/main/java/com/facebook/react/shell/BUCK | 1 + 1 file changed, 1 insertion(+) diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index 88b94ebdaae30a..096fa11e16e2f3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -36,6 +36,7 @@ android_library( react_native_target('java/com/facebook/react/modules/location:location'), react_native_target('java/com/facebook/react/modules/netinfo:netinfo'), react_native_target('java/com/facebook/react/modules/network:network'), + react_native_target('java/com/facebook/react/modules/share:share'), react_native_target('java/com/facebook/react/modules/statusbar:statusbar'), react_native_target('java/com/facebook/react/modules/storage:storage'), react_native_target('java/com/facebook/react/modules/timepicker:timepicker'), From 1f5f0c0ee62a5eeaabdabb1c60322b643079a8ca Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Sun, 14 Feb 2016 02:50:04 +0900 Subject: [PATCH 05/37] add share module to flow file --- Libraries/react-native/react-native.js.flow | 1 + 1 file changed, 1 insertion(+) diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 6b4f333c03d2c6..3c6b3ed4559978 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -93,6 +93,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { PixelRatio: require('PixelRatio'), PushNotificationIOS: require('PushNotificationIOS'), Settings: require('Settings'), + Share: require('Share'), StatusBarIOS: require('StatusBarIOS'), StyleSheet: require('StyleSheet'), TimePickerAndroid: require('TimePickerAndroid'), From f21d70942164f801842183690cc10a99acc42740 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Sun, 14 Feb 2016 03:58:57 +0900 Subject: [PATCH 06/37] options handling fixed --- Examples/UIExplorer/ShareExample.js | 3 ++- Libraries/Share/Share.js | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Examples/UIExplorer/ShareExample.js b/Examples/UIExplorer/ShareExample.js index 4e822dbd6b6c54..0c8ea91a4d049a 100644 --- a/Examples/UIExplorer/ShareExample.js +++ b/Examples/UIExplorer/ShareExample.js @@ -84,7 +84,8 @@ class ShareMessageExample extends React.Component { dialogTitle: 'Share React Native website', excludedActivityTypes: [ 'com.apple.UIKit.activity.PostToTwitter' - ] + ], + tintColor: 'green' }) .then((result) => this.setState({result:result})) .catch(()=> this.setState({result:'Canceled'})) diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index b92ea9b0d87e85..af003e95df448b 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -58,12 +58,15 @@ class Share { 'Invalid Content: should be a string. Was: ' + content ); } - options = options || {}; - return Platform.OS === 'android' - ? ShareModule.shareTextContent(contents, options.dialogTitle) + return Platform.OS === 'android' + ? ShareModule.shareTextContent(contents, typeof options === 'object' && options.dialogTitle ? options.dialogTitle : null) : new Promise((resolve, reject) => { + let actionSheetOptions = {...contents, ...options}; + if(typeof options === 'object' && options.tintColor) { + actionSheetOptions.tintColor = processColor(options.tintColor); + } ActionSheetManager.showShareActionSheetWithOptions( - {...contents, tintColor: processColor(options.tintColor)}, + actionSheetOptions, console.error, (success, activityType) => { if(success) { From 89177884afdae0d7cf500ab7ae1e2825cdae3366 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Sun, 14 Feb 2016 09:20:06 +0900 Subject: [PATCH 07/37] Keyword "if" must be followed by whitespace. --- Libraries/Share/Share.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index af003e95df448b..003e28ead0f87c 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -62,14 +62,14 @@ class Share { ? ShareModule.shareTextContent(contents, typeof options === 'object' && options.dialogTitle ? options.dialogTitle : null) : new Promise((resolve, reject) => { let actionSheetOptions = {...contents, ...options}; - if(typeof options === 'object' && options.tintColor) { + if (typeof options === 'object' && options.tintColor) { actionSheetOptions.tintColor = processColor(options.tintColor); } ActionSheetManager.showShareActionSheetWithOptions( actionSheetOptions, console.error, (success, activityType) => { - if(success) { + if (success) { resolve(activityType) } else { reject() From 68804ca26590bb0730445101596587c69bbea06a Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Thu, 7 Apr 2016 19:00:55 +0900 Subject: [PATCH 08/37] refactoring --- Examples/UIExplorer/ShareExample.js | 10 ++--- Libraries/Share/Share.js | 42 ++++++++++++------- .../react/modules/share/ShareModule.java | 34 +++++++-------- 3 files changed, 45 insertions(+), 41 deletions(-) diff --git a/Examples/UIExplorer/ShareExample.js b/Examples/UIExplorer/ShareExample.js index 0c8ea91a4d049a..58e600ef01c8d4 100644 --- a/Examples/UIExplorer/ShareExample.js +++ b/Examples/UIExplorer/ShareExample.js @@ -41,7 +41,7 @@ class ShareMessageExample extends React.Component { super(props); this.shareMessage = this.shareMessage.bind(this); - this.shareTextContent = this.shareTextContent.bind(this); + this.shareText = this.shareText.bind(this); this.state = { result: '' @@ -58,7 +58,7 @@ class ShareMessageExample extends React.Component { + onPress={this.shareText}> Click to share message, URL and subject @@ -69,14 +69,14 @@ class ShareMessageExample extends React.Component { } shareMessage() { - Share.shareTextContent({ + Share.shareText({ message: 'React Native | A framework for building native apps using React' }) .then((result) => this.setState({result:result})) .catch(()=> this.setState({result:'Canceled'})) } - shareTextContent() { - Share.shareTextContent({ + shareText() { + Share.shareText({ message: 'A framework for building native apps using React', url: 'http://facebook.github.io/react-native/', subject: 'React Native' diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index 003e28ead0f87c..20dd851b6f3c02 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -22,9 +22,9 @@ const processColor = require('processColor'); class Share { /** - * Open a dialog to share text contents. + * Open a dialog to share text content. * - * ### Contents + * ### Content * * - `message` - a message to share * - `url` - an URL to share. In Android, this will overwrite message @@ -43,25 +43,34 @@ class Share { * * - `dialogTitle` */ - static shareTextContent(contents: Object, options?: Object): Promise { + static shareText(content: Object, options?: Object): Promise { invariant( - typeof contents === 'object' && contents !== null, - 'Contents must a valid object' + typeof content === 'object' && content !== null, + 'Content must a valid object' ); invariant( - contents.url || contents.message, + content.url || content.message, 'At least one of URL and message is required' ); - for(let content in contents) { - invariant( - typeof content === 'string', - 'Invalid Content: should be a string. Was: ' + content - ); - } - return Platform.OS === 'android' - ? ShareModule.shareTextContent(contents, typeof options === 'object' && options.dialogTitle ? options.dialogTitle : null) - : new Promise((resolve, reject) => { - let actionSheetOptions = {...contents, ...options}; + invariant( + !content.message || typeof content.message === 'string', + 'Invalid message: message should be a string. Was: ' + content.message + ); + invariant( + !content.url || typeof content.url === 'string', + 'Invalid url: url should be a string. Was: ' + content.url + ); + invariant( + !content.subject || typeof content.subject === 'string', + 'Invalid subject: subject should be a string. Was: ' + content.subject + ); + + if(Platform.OS === 'android') { + let dialogTitle = typeof options === 'object' && options.dialogTitle ? options.dialogTitle : null; + return ShareModule.shareText(content, dialogTitle); + } else { + return new Promise((resolve, reject) => { + let actionSheetOptions = {...content, ...options}; if (typeof options === 'object' && options.tintColor) { actionSheetOptions.tintColor = processColor(options.tintColor); } @@ -77,6 +86,7 @@ class Share { } ); }); + } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java index 71f227fd980e71..bc03cb9b04ae6a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -25,8 +25,6 @@ */ public class ShareModule extends ReactContextBaseJavaModule { - private Promise mPromise; - public ShareModule(ReactApplicationContext reactContext) { super(reactContext); } @@ -37,15 +35,15 @@ public String getName() { } /** - * Open a chooser dialog to send text contents to other apps. + * Open a chooser dialog to send text content to other apps. * * Refer http://developer.android.com/intl/ko/training/sharing/send.html * - * @param contents the data to send + * @param content the data to send * @param title the title of the chooser dialog */ @ReactMethod - public void shareTextContent(ReadableMap contents, String title, final Promise promise) { + public void shareText(ReadableMap content, String title, Promise promise) { Activity currentActivity = getCurrentActivity(); if (currentActivity == null) { @@ -53,38 +51,34 @@ public void shareTextContent(ReadableMap contents, String title, final Promise p return; } - if (contents == null) { - throw new JSApplicationIllegalArgumentException("Invalid contents"); + if (content == null) { + promise.reject("Invalid content"); + return; } - mPromise = promise; - try { Intent intent = new Intent(Intent.ACTION_SEND); intent.setTypeAndNormalize("text/plain"); - if(contents.hasKey("subject")) { - intent.putExtra(Intent.EXTRA_SUBJECT, contents.getString("subject")); + if(content.hasKey("subject")) { + intent.putExtra(Intent.EXTRA_SUBJECT, content.getString("subject")); } - if(contents.hasKey("message")) { - intent.putExtra(Intent.EXTRA_TEXT, contents.getString("message")); + if(content.hasKey("message")) { + intent.putExtra(Intent.EXTRA_TEXT, content.getString("message")); } - if(contents.hasKey("url")) { - intent.putExtra(Intent.EXTRA_TEXT, contents.getString("url")); // this will overwrite message + if(content.hasKey("url")) { + intent.putExtra(Intent.EXTRA_TEXT, content.getString("url")); // this will overwrite message } currentActivity.startActivity(Intent.createChooser(intent, title)); //TODO: use createChooser (Intent target, CharSequence title, IntentSender sender) after API level 22 - mPromise.resolve(true); - mPromise = null; + promise.resolve(true); } catch (Exception e) { - mPromise.reject("Failed to open share dialog"); - mPromise = null; + promise.reject("Failed to open share dialog"); } } - } \ No newline at end of file From fb88c942eadb1d30ced8f638f54604ecbf927323 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Thu, 7 Apr 2016 22:13:50 +0900 Subject: [PATCH 09/37] fix lint errors --- Examples/UIExplorer/ShareExample.js | 1 + Libraries/Share/Share.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Examples/UIExplorer/ShareExample.js b/Examples/UIExplorer/ShareExample.js index 58e600ef01c8d4..3d35183e74d562 100644 --- a/Examples/UIExplorer/ShareExample.js +++ b/Examples/UIExplorer/ShareExample.js @@ -75,6 +75,7 @@ class ShareMessageExample extends React.Component { .then((result) => this.setState({result:result})) .catch(()=> this.setState({result:'Canceled'})) } + shareText() { Share.shareText({ message: 'A framework for building native apps using React', diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index 20dd851b6f3c02..cb751584ea1cd3 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -64,8 +64,8 @@ class Share { !content.subject || typeof content.subject === 'string', 'Invalid subject: subject should be a string. Was: ' + content.subject ); - - if(Platform.OS === 'android') { + + if (Platform.OS === 'android') { let dialogTitle = typeof options === 'object' && options.dialogTitle ? options.dialogTitle : null; return ShareModule.shareText(content, dialogTitle); } else { From 72c6d506efb92cc4fad486d2d672f3760fa229e4 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Fri, 8 Apr 2016 17:24:27 +0900 Subject: [PATCH 10/37] improves platform check --- Libraries/Share/Share.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index cb751584ea1cd3..7b0e929a2f80b5 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -68,7 +68,7 @@ class Share { if (Platform.OS === 'android') { let dialogTitle = typeof options === 'object' && options.dialogTitle ? options.dialogTitle : null; return ShareModule.shareText(content, dialogTitle); - } else { + } else if (Platform.OS === 'ios') { return new Promise((resolve, reject) => { let actionSheetOptions = {...content, ...options}; if (typeof options === 'object' && options.tintColor) { @@ -86,6 +86,8 @@ class Share { } ); }); + } else { + console.warn('Share.shareText is not supported on this platform'); } } From 2678aeee92a9d3135d82f839221409cb5b66b5af Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Fri, 8 Apr 2016 17:27:49 +0900 Subject: [PATCH 11/37] show chooser when could not get current activity --- .../react/modules/share/ShareModule.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java index bc03cb9b04ae6a..2db391ff934335 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -44,13 +44,6 @@ public String getName() { */ @ReactMethod public void shareText(ReadableMap content, String title, Promise promise) { - Activity currentActivity = getCurrentActivity(); - - if (currentActivity == null) { - promise.reject("Activity doesn't exist"); - return; - } - if (content == null) { promise.reject("Invalid content"); return; @@ -60,25 +53,33 @@ public void shareText(ReadableMap content, String title, Promise promise) { Intent intent = new Intent(Intent.ACTION_SEND); intent.setTypeAndNormalize("text/plain"); - if(content.hasKey("subject")) { + if (content.hasKey("subject")) { intent.putExtra(Intent.EXTRA_SUBJECT, content.getString("subject")); } - if(content.hasKey("message")) { + if (content.hasKey("message")) { intent.putExtra(Intent.EXTRA_TEXT, content.getString("message")); } - if(content.hasKey("url")) { + if (content.hasKey("url")) { intent.putExtra(Intent.EXTRA_TEXT, content.getString("url")); // this will overwrite message } - currentActivity.startActivity(Intent.createChooser(intent, title)); //TODO: use createChooser (Intent target, CharSequence title, IntentSender sender) after API level 22 + Intent chooser = Intent.createChooser(intent, title); + + Activity currentActivity = getCurrentActivity(); + if (currentActivity != null) { + currentActivity.startActivity(chooser); + } else { + getReactApplicationContext().startActivity(chooser); + } promise.resolve(true); + } catch (Exception e) { promise.reject("Failed to open share dialog"); } - + } } \ No newline at end of file From 32847bbea50bc7928cb318c7096478a47b65a9f8 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Fri, 8 Apr 2016 17:28:50 +0900 Subject: [PATCH 12/37] logs when exception --- .../java/com/facebook/react/modules/share/ShareModule.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java index 2db391ff934335..87e3675949cdc9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -13,12 +13,14 @@ import android.content.Intent; import android.net.Uri; +import com.facebook.common.logging.FLog; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.common.ReactConstants; /** * Intent module. Launch other activities or open URLs. @@ -75,8 +77,8 @@ public void shareText(ReadableMap content, String title, Promise promise) { getReactApplicationContext().startActivity(chooser); } promise.resolve(true); - } catch (Exception e) { + FLog.e(ReactConstants.TAG, "Failed to open share dialog", e); promise.reject("Failed to open share dialog"); } From 03717501d54c71e02d59af52dce5d1f5a5d8d02a Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Fri, 8 Apr 2016 18:26:44 +0900 Subject: [PATCH 13/37] fix CI errors --- Examples/UIExplorer/ShareExample.js | 15 +++++++++------ Libraries/Share/Share.js | 1 + .../java/com/facebook/react/modules/share/BUCK | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Examples/UIExplorer/ShareExample.js b/Examples/UIExplorer/ShareExample.js index 3d35183e74d562..ec9dd327baf40b 100644 --- a/Examples/UIExplorer/ShareExample.js +++ b/Examples/UIExplorer/ShareExample.js @@ -36,12 +36,15 @@ exports.examples = [{ }]; class ShareMessageExample extends React.Component { + _shareMessage: Function; + _shareText: Function; + state: any; constructor(props) { super(props); - this.shareMessage = this.shareMessage.bind(this); - this.shareText = this.shareText.bind(this); + this._shareMessage = this._shareMessage.bind(this); + this._shareText = this._shareText.bind(this); this.state = { result: '' @@ -52,13 +55,13 @@ class ShareMessageExample extends React.Component { return ( + onPress={this._shareMessage}> Click to share message + onPress={this._shareText}> Click to share message, URL and subject @@ -68,7 +71,7 @@ class ShareMessageExample extends React.Component { ); } - shareMessage() { + _shareMessage() { Share.shareText({ message: 'React Native | A framework for building native apps using React' }) @@ -76,7 +79,7 @@ class ShareMessageExample extends React.Component { .catch(()=> this.setState({result:'Canceled'})) } - shareText() { + _shareText() { Share.shareText({ message: 'A framework for building native apps using React', url: 'http://facebook.github.io/react-native/', diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index 7b0e929a2f80b5..e4e2e1e581d170 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -88,6 +88,7 @@ class Share { }); } else { console.warn('Share.shareText is not supported on this platform'); + return Promise.reject(); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK index 7802dfb629ad74..4e95826b0dda6c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK @@ -6,6 +6,7 @@ android_library( deps = [ react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/common:common'), + react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/jsr-305:jsr-305'), ], From dfe1f63dc14b00ff3048f513577a17ec4f29bf40 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Sat, 9 Apr 2016 03:16:32 +0900 Subject: [PATCH 14/37] change invariant module path --- Libraries/Share/Share.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index cb751584ea1cd3..9a41199381739a 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -16,7 +16,7 @@ const { ActionSheetManager, ShareModule } = require('NativeModules'); -const invariant = require('invariant'); +const invariant = require('fbjs/lib/invariant'); const processColor = require('processColor'); class Share { From 43209dcf2348f7c22fe3c353d6b98b6d9c9a2689 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Tue, 19 Apr 2016 09:37:40 +0900 Subject: [PATCH 15/37] update example (+3 squashed commits) Squashed commits: [c434c10] improve promise handling [2e813a5] `promise.reject(string)` is deprecated. [6210f50] change subject to title --- Examples/UIExplorer/ShareExample.js | 28 +++++++++++++------ Libraries/Share/Share.js | 25 +++++++++++------ .../react/modules/share/ShareModule.java | 21 ++++++++------ 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/Examples/UIExplorer/ShareExample.js b/Examples/UIExplorer/ShareExample.js index ec9dd327baf40b..f85f07312dfb7a 100644 --- a/Examples/UIExplorer/ShareExample.js +++ b/Examples/UIExplorer/ShareExample.js @@ -63,7 +63,7 @@ class ShareMessageExample extends React.Component { - Click to share message, URL and subject + Click to share message, URL and title {this.state.result} @@ -72,18 +72,24 @@ class ShareMessageExample extends React.Component { } _shareMessage() { - Share.shareText({ + Share.share({ message: 'React Native | A framework for building native apps using React' }) - .then((result) => this.setState({result:result})) - .catch(()=> this.setState({result:'Canceled'})) + .then((result) => { + if(result && result.activityType) { + this.setState({result: 'success: shared with ' + result.activityType}) + } else { + this.setState({result: 'success'}) + } + }) + .catch((error) => this.setState({result: 'error: ' + error.message})) } _shareText() { - Share.shareText({ + Share.share({ message: 'A framework for building native apps using React', url: 'http://facebook.github.io/react-native/', - subject: 'React Native' + title: 'React Native' }, { dialogTitle: 'Share React Native website', excludedActivityTypes: [ @@ -91,8 +97,14 @@ class ShareMessageExample extends React.Component { ], tintColor: 'green' }) - .then((result) => this.setState({result:result})) - .catch(()=> this.setState({result:'Canceled'})) + .then((result) => { + if(result && result.activityType) { + this.setState({result: 'success: shared with ' + result.activityType}) + } else { + this.setState({result: 'success'}) + } + }) + .catch((error) => this.setState({result: 'error: ' + error.message})) } } diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index d3e59e062c8d7a..61040a2753ec4e 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -19,6 +19,9 @@ const { const invariant = require('fbjs/lib/invariant'); const processColor = require('processColor'); +type Content = { title: string, message: string } | { title: string, url: string }; +type Options = { dialogTitle?: string, excludeActivityTypes?: Array, tintColor?: string }; + class Share { /** @@ -28,7 +31,7 @@ class Share { * * - `message` - a message to share * - `url` - an URL to share. In Android, this will overwrite message - * - `subject` - subject of the message + * - `title` - title of the message * * At least one of URL and message is required. * @@ -43,7 +46,7 @@ class Share { * * - `dialogTitle` */ - static shareText(content: Object, options?: Object): Promise { + static share(content: Content, options: ?Options): Promise { invariant( typeof content === 'object' && content !== null, 'Content must a valid object' @@ -61,13 +64,13 @@ class Share { 'Invalid url: url should be a string. Was: ' + content.url ); invariant( - !content.subject || typeof content.subject === 'string', - 'Invalid subject: subject should be a string. Was: ' + content.subject + !content.title || typeof content.title === 'string', + 'Invalid title: title should be a string. Was: ' + content.title ); if (Platform.OS === 'android') { let dialogTitle = typeof options === 'object' && options.dialogTitle ? options.dialogTitle : null; - return ShareModule.shareText(content, dialogTitle); + return ShareModule.share(content, dialogTitle); } else if (Platform.OS === 'ios') { return new Promise((resolve, reject) => { let actionSheetOptions = {...content, ...options}; @@ -76,18 +79,22 @@ class Share { } ActionSheetManager.showShareActionSheetWithOptions( actionSheetOptions, - console.error, + (error) => { + reject(new Error(error.message)) + }, (success, activityType) => { if (success) { - resolve(activityType) + resolve({ + 'activityType': activityType + }) } else { - reject() + reject(new Error('User canceled')) } } ); }); } else { - console.warn('Share.shareText is not supported on this platform'); + console.warn('Share.share is not supported on this platform'); return Promise.reject(); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java index 87e3675949cdc9..4dd15de66d17a4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -27,6 +27,9 @@ */ public class ShareModule extends ReactContextBaseJavaModule { + private static final String ERROR_INVALID_CONTENT = "E_INVALID_CONTENT"; + private static final String ERROR_UNABLE_TO_OPEN_DIALOG = "E_UNABLE_TO_OPEN_DIALOG"; + public ShareModule(ReactApplicationContext reactContext) { super(reactContext); } @@ -42,12 +45,12 @@ public String getName() { * Refer http://developer.android.com/intl/ko/training/sharing/send.html * * @param content the data to send - * @param title the title of the chooser dialog + * @param dialogTitle the title of the chooser dialog */ @ReactMethod - public void shareText(ReadableMap content, String title, Promise promise) { + public void share(ReadableMap content, String dialogTitle, Promise promise) { if (content == null) { - promise.reject("Invalid content"); + promise.reject(ERROR_INVALID_CONTENT, "Content cannot be null"); return; } @@ -55,8 +58,8 @@ public void shareText(ReadableMap content, String title, Promise promise) { Intent intent = new Intent(Intent.ACTION_SEND); intent.setTypeAndNormalize("text/plain"); - if (content.hasKey("subject")) { - intent.putExtra(Intent.EXTRA_SUBJECT, content.getString("subject")); + if (content.hasKey("title")) { + intent.putExtra(Intent.EXTRA_SUBJECT, content.getString("title")); } if (content.hasKey("message")) { @@ -67,8 +70,8 @@ public void shareText(ReadableMap content, String title, Promise promise) { intent.putExtra(Intent.EXTRA_TEXT, content.getString("url")); // this will overwrite message } - //TODO: use createChooser (Intent target, CharSequence title, IntentSender sender) after API level 22 - Intent chooser = Intent.createChooser(intent, title); + //TODO: use createChooser (Intent target, CharSequence dialogTitle, IntentSender sender) after API level 22 + Intent chooser = Intent.createChooser(intent, dialogTitle); Activity currentActivity = getCurrentActivity(); if (currentActivity != null) { @@ -76,10 +79,10 @@ public void shareText(ReadableMap content, String title, Promise promise) { } else { getReactApplicationContext().startActivity(chooser); } - promise.resolve(true); + promise.resolve(null); } catch (Exception e) { FLog.e(ReactConstants.TAG, "Failed to open share dialog", e); - promise.reject("Failed to open share dialog"); + promise.reject(ERROR_UNABLE_TO_OPEN_DIALOG, "Failed to open share dialog"); } } From 74e69664c16fc4757f8a058c2d8915663688f72a Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Wed, 20 Apr 2016 14:18:25 +0900 Subject: [PATCH 16/37] uses legacy http library for robolectric 3.0 (see https://github.com/robolectric/robolectric/issues/1862) --- ReactAndroid/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index 022b1580112f15..0f72498000dd32 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -213,6 +213,8 @@ android { compileSdkVersion 23 buildToolsVersion "23.0.1" + useLibrary 'org.apache.http.legacy' + defaultConfig { minSdkVersion 16 targetSdkVersion 22 From dc7fdaaaa5de6b4036630fb06153af225e66126f Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Wed, 20 Apr 2016 14:24:26 +0900 Subject: [PATCH 17/37] add a unit test for share module --- .../test/java/com/facebook/react/modules/BUCK | 1 + .../react/modules/share/ShareModuleTest.java | 89 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK index 5e9df2b0b99d4f..3f8b9436e74f1a 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK @@ -17,6 +17,7 @@ robolectric3_test( react_native_target('java/com/facebook/react/modules/debug:debug'), react_native_target('java/com/facebook/react/modules/dialog:dialog'), react_native_target('java/com/facebook/react/modules/network:network'), + react_native_target('java/com/facebook/react/modules/share:share'), react_native_target('java/com/facebook/react/modules/storage:storage'), react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), react_native_target('java/com/facebook/react/uimanager:uimanager'), diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java new file mode 100644 index 00000000000000..7353591b80426a --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.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.modules.share; + +import android.app.Activity; +import android.content.Intent; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.JavaOnlyMap; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.robolectric.Robolectric; +import org.robolectric.Shadows; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.util.ActivityController; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class ShareModuleTest { + + private ActivityController mActivityController; + private Activity mActivity; + private ShareModule mShareModule; + + @Before + public void setUp() throws Exception { + mActivityController = Robolectric.buildActivity(Activity.class); + mActivity = mActivityController + .create() + .start() + .resume() + .get(); + + final ReactApplicationContext context = PowerMockito.mock(ReactApplicationContext.class); + PowerMockito.when(context.hasActiveCatalystInstance()).thenReturn(true); + PowerMockito.when(context, "getCurrentActivity").thenReturn(mActivity); + + mShareModule = new ShareModule(context); + } + + @After + public void tearDown() { + mActivityController.pause().stop().destroy(); + + mActivityController = null; + mShareModule = null; + } + + @Test + public void testShareDialog() { + final String title = "Title"; + final String message = "Message"; + final String dialogTitle = "Dialog Title"; + + JavaOnlyMap content = new JavaOnlyMap(); + content.putString("title", title); + content.putString("message", message); + + mShareModule.share(content, dialogTitle, PowerMockito.mock(Promise.class)); + + final Intent chooserIntent = Shadows.shadowOf(mActivity).peekNextStartedActivity(); + assertNotNull("Dialog was not displayed", chooserIntent); + assertEquals(Intent.ACTION_CHOOSER, chooserIntent.getAction()); + assertEquals(dialogTitle, chooserIntent.getExtras().get(Intent.EXTRA_TITLE)); + + final Intent contentIntent = (Intent)chooserIntent.getExtras().get(Intent.EXTRA_INTENT); + assertNotNull("Intent was not built correctly", contentIntent); + assertEquals(Intent.ACTION_SEND, contentIntent.getAction()); + assertEquals(title, contentIntent.getExtras().get(Intent.EXTRA_SUBJECT)); + assertEquals(message, contentIntent.getExtras().get(Intent.EXTRA_TEXT)); + } + +} From 7f39ae34a3aa383fd21b70ee3569b6177e53ee5b Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Wed, 20 Apr 2016 17:23:22 +0900 Subject: [PATCH 18/37] fix flow errors --- Libraries/Share/Share.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index 61040a2753ec4e..e9f78e5694fca9 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -19,7 +19,7 @@ const { const invariant = require('fbjs/lib/invariant'); const processColor = require('processColor'); -type Content = { title: string, message: string } | { title: string, url: string }; +type Content = { title?: string, message: string } | { title?: string, url: string }; type Options = { dialogTitle?: string, excludeActivityTypes?: Array, tintColor?: string }; class Share { @@ -46,7 +46,7 @@ class Share { * * - `dialogTitle` */ - static share(content: Content, options: ?Options): Promise { + static share(content: Content, options: ?Options): Promise { invariant( typeof content === 'object' && content !== null, 'Content must a valid object' @@ -57,24 +57,24 @@ class Share { ); invariant( !content.message || typeof content.message === 'string', - 'Invalid message: message should be a string. Was: ' + content.message + 'Invalid message: message should be a string.' ); invariant( !content.url || typeof content.url === 'string', - 'Invalid url: url should be a string. Was: ' + content.url + 'Invalid url: url should be a string.' ); invariant( !content.title || typeof content.title === 'string', - 'Invalid title: title should be a string. Was: ' + content.title + 'Invalid title: title should be a string.' ); if (Platform.OS === 'android') { - let dialogTitle = typeof options === 'object' && options.dialogTitle ? options.dialogTitle : null; + let dialogTitle = (options !== null && typeof options === 'object' && options.dialogTitle) ? options.dialogTitle : null; return ShareModule.share(content, dialogTitle); } else if (Platform.OS === 'ios') { return new Promise((resolve, reject) => { let actionSheetOptions = {...content, ...options}; - if (typeof options === 'object' && options.tintColor) { + if (options !== null && typeof options === 'object' && options.tintColor) { actionSheetOptions.tintColor = processColor(options.tintColor); } ActionSheetManager.showShareActionSheetWithOptions( From 375ee94e9d3b8e1704e7dd64d18e0bec9d7e194a Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Tue, 26 Apr 2016 11:34:55 +0900 Subject: [PATCH 19/37] better invariant --- Libraries/Share/Share.js | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index e9f78e5694fca9..7eb4e67507efa4 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -46,42 +46,31 @@ class Share { * * - `dialogTitle` */ - static share(content: Content, options: ?Options): Promise { + static share(content: Content, options: Options = {}): Promise { invariant( typeof content === 'object' && content !== null, 'Content must a valid object' ); invariant( - content.url || content.message, - 'At least one of URL and message is required' + typeof content.url === 'string' || typeof content.message === 'string', + 'At least one of URL and message is required' ); invariant( - !content.message || typeof content.message === 'string', - 'Invalid message: message should be a string.' - ); - invariant( - !content.url || typeof content.url === 'string', - 'Invalid url: url should be a string.' - ); - invariant( - !content.title || typeof content.title === 'string', - 'Invalid title: title should be a string.' + typeof options === 'object' && options !== null, + 'Options must be a valid object' ); if (Platform.OS === 'android') { - let dialogTitle = (options !== null && typeof options === 'object' && options.dialogTitle) ? options.dialogTitle : null; - return ShareModule.share(content, dialogTitle); + invariant( + !content.title || typeof content.title === 'string', + 'Invalid title: title should be a string.' + ); + return ShareModule.share(content, options.dialogTitle); } else if (Platform.OS === 'ios') { return new Promise((resolve, reject) => { - let actionSheetOptions = {...content, ...options}; - if (options !== null && typeof options === 'object' && options.tintColor) { - actionSheetOptions.tintColor = processColor(options.tintColor); - } ActionSheetManager.showShareActionSheetWithOptions( - actionSheetOptions, - (error) => { - reject(new Error(error.message)) - }, + {...content, ...options, tintColor: processColor(options.tintColor)}, + (error) => reject(error), (success, activityType) => { if (success) { resolve({ @@ -95,7 +84,7 @@ class Share { }); } else { console.warn('Share.share is not supported on this platform'); - return Promise.reject(); + return Promise.reject(new Error('Unsupported platform')); } } From 4d6add874e68f85bcbc2b8d070164b1ceab8c735 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Mon, 2 May 2016 14:45:01 +0900 Subject: [PATCH 20/37] indent fix --- Libraries/Share/Share.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index 7eb4e67507efa4..66c68c1e7b507e 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -52,8 +52,8 @@ class Share { 'Content must a valid object' ); invariant( - typeof content.url === 'string' || typeof content.message === 'string', - 'At least one of URL and message is required' + typeof content.url === 'string' || typeof content.message === 'string', + 'At least one of URL and message is required' ); invariant( typeof options === 'object' && options !== null, From e398fd90db0a6afea6637e05a0410ac754e1011e Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Mon, 2 May 2016 14:46:14 +0900 Subject: [PATCH 21/37] Fix up this pattern var React = require('react-native'); --- Examples/UIExplorer/ShareExample.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Examples/UIExplorer/ShareExample.js b/Examples/UIExplorer/ShareExample.js index f85f07312dfb7a..b8d30bdc4d4b3a 100644 --- a/Examples/UIExplorer/ShareExample.js +++ b/Examples/UIExplorer/ShareExample.js @@ -15,7 +15,8 @@ */ 'use strict'; -var React = require('react-native'); +var React = require('react'); +var ReactNative = require('react-native'); var { StyleSheet, View, @@ -23,7 +24,7 @@ var { TextInput, TouchableHighlight, Share, -} = React; +} = ReactNative; exports.framework = 'React'; exports.title = 'Share'; From 71a67fbcae76059b25e83efb62a71ae9e7fc3d68 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Mon, 2 May 2016 15:26:18 +0900 Subject: [PATCH 22/37] It's enough to use a mock Activity --- .../react/modules/share/ShareModuleTest.java | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java index 7353591b80426a..139c31090581c0 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java @@ -25,7 +25,6 @@ import org.robolectric.Robolectric; import org.robolectric.Shadows; import org.robolectric.RobolectricTestRunner; -import org.robolectric.util.ActivityController; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -34,31 +33,20 @@ @PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) public class ShareModuleTest { - private ActivityController mActivityController; private Activity mActivity; private ShareModule mShareModule; @Before public void setUp() throws Exception { - mActivityController = Robolectric.buildActivity(Activity.class); - mActivity = mActivityController - .create() - .start() - .resume() - .get(); - + mActivity = Robolectric.setupActivity(Activity.class); final ReactApplicationContext context = PowerMockito.mock(ReactApplicationContext.class); - PowerMockito.when(context.hasActiveCatalystInstance()).thenReturn(true); PowerMockito.when(context, "getCurrentActivity").thenReturn(mActivity); - mShareModule = new ShareModule(context); } @After public void tearDown() { - mActivityController.pause().stop().destroy(); - - mActivityController = null; + mActivity = null; mShareModule = null; } @@ -74,7 +62,7 @@ public void testShareDialog() { mShareModule.share(content, dialogTitle, PowerMockito.mock(Promise.class)); - final Intent chooserIntent = Shadows.shadowOf(mActivity).peekNextStartedActivity(); + final Intent chooserIntent = Shadows.shadowOf(mActivity).getNextStartedActivity(); assertNotNull("Dialog was not displayed", chooserIntent); assertEquals(Intent.ACTION_CHOOSER, chooserIntent.getAction()); assertEquals(dialogTitle, chooserIntent.getExtras().get(Intent.EXTRA_TITLE)); From 4351eecaac7785dd0ca553f15d9650df2e9b2394 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Mon, 2 May 2016 16:53:24 +0900 Subject: [PATCH 23/37] promise check and test case for call with an invalid content --- .../react/modules/share/ShareModule.java | 4 +- .../react/modules/share/ShareModuleTest.java | 84 ++++++++++++++++++- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java index 4dd15de66d17a4..274c495e84f582 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -27,8 +27,8 @@ */ public class ShareModule extends ReactContextBaseJavaModule { - private static final String ERROR_INVALID_CONTENT = "E_INVALID_CONTENT"; - private static final String ERROR_UNABLE_TO_OPEN_DIALOG = "E_UNABLE_TO_OPEN_DIALOG"; + /* package */ static final String ERROR_INVALID_CONTENT = "E_INVALID_CONTENT"; + /* package */ static final String ERROR_UNABLE_TO_OPEN_DIALOG = "E_UNABLE_TO_OPEN_DIALOG"; public ShareModule(ReactApplicationContext reactContext) { super(reactContext); diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java index 139c31090581c0..bfd2cc1d4b555e 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java @@ -36,10 +36,74 @@ public class ShareModuleTest { private Activity mActivity; private ShareModule mShareModule; + final static class SimplePromise implements Promise { + private static final String DEFAULT_ERROR = "EUNSPECIFIED"; + + private int mResolved; + private int mRejected; + private Object mValue; + private String mErrorCode; + private String mErrorMessage; + + public int getResolved() { + return mResolved; + } + + public int getRejected() { + return mRejected; + } + + public Object getValue() { + return mValue; + } + + public String getErrorCode() { + return mErrorCode; + } + + public String getErrorMessage() { + return mErrorMessage; + } + + @Override + public void resolve(Object value) { + mResolved++; + mValue = value; + } + + @Override + public void reject(String code, String message) { + reject(code, message, /*Throwable*/null); + } + + @Override + @Deprecated + public void reject(String message) { + reject(DEFAULT_ERROR, message, /*Throwable*/null); + } + + @Override + public void reject(String code, Throwable e) { + reject(code, e.getMessage(), e); + } + + @Override + public void reject(Throwable e) { + reject(DEFAULT_ERROR, e.getMessage(), e); + } + + @Override + public void reject(String code, String message, @Nullable Throwable e) { + mRejected++; + mErrorCode = code; + mErrorMessage = message; + } + } + @Before public void setUp() throws Exception { mActivity = Robolectric.setupActivity(Activity.class); - final ReactApplicationContext context = PowerMockito.mock(ReactApplicationContext.class); + ReactApplicationContext context = PowerMockito.mock(ReactApplicationContext.class); PowerMockito.when(context, "getCurrentActivity").thenReturn(mActivity); mShareModule = new ShareModule(context); } @@ -60,7 +124,9 @@ public void testShareDialog() { content.putString("title", title); content.putString("message", message); - mShareModule.share(content, dialogTitle, PowerMockito.mock(Promise.class)); + final SimplePromise promise = new SimplePromise(); + + mShareModule.share(content, dialogTitle, promise); final Intent chooserIntent = Shadows.shadowOf(mActivity).getNextStartedActivity(); assertNotNull("Dialog was not displayed", chooserIntent); @@ -72,6 +138,20 @@ public void testShareDialog() { assertEquals(Intent.ACTION_SEND, contentIntent.getAction()); assertEquals(title, contentIntent.getExtras().get(Intent.EXTRA_SUBJECT)); assertEquals(message, contentIntent.getExtras().get(Intent.EXTRA_TEXT)); + + assertEquals(1, promise.getResolved()); + } + + @Test + public void testInvalidContent() { + final String dialogTitle = "Dialog Title"; + + final SimplePromise promise = new SimplePromise(); + + mShareModule.share(null, dialogTitle, promise); + + assertEquals(1, promise.getRejected()); + assertEquals(ShareModule.ERROR_INVALID_CONTENT, promise.getErrorCode()); } } From 92a793b3b509c6c993810f9b0399051ff8c6a7f2 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Mon, 2 May 2016 17:01:36 +0900 Subject: [PATCH 24/37] import missed --- .../java/com/facebook/react/modules/share/ShareModuleTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java index bfd2cc1d4b555e..4496d752f4a215 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java @@ -16,6 +16,8 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.JavaOnlyMap; +import javax.annotation.Nullable; + import org.junit.After; import org.junit.Before; import org.junit.Test; From 68ca817b6cca0cf74dc96c2d3ea1600c275d9e25 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Mon, 2 May 2016 17:24:01 +0900 Subject: [PATCH 25/37] replace `shadowOf` with `ShadowExtractor.extract` --- ReactAndroid/build.gradle | 2 -- .../com/facebook/react/modules/share/ShareModuleTest.java | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index 295bfa76e5315f..abd0fec39039bc 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -218,8 +218,6 @@ android { compileSdkVersion 23 buildToolsVersion "23.0.1" - useLibrary 'org.apache.http.legacy' - defaultConfig { minSdkVersion 16 targetSdkVersion 22 diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java index 4496d752f4a215..027171848ce831 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java @@ -24,9 +24,10 @@ import org.junit.runner.RunWith; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.robolectric.internal.ShadowExtractor; import org.robolectric.Robolectric; -import org.robolectric.Shadows; import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowActivity; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -130,7 +131,7 @@ public void testShareDialog() { mShareModule.share(content, dialogTitle, promise); - final Intent chooserIntent = Shadows.shadowOf(mActivity).getNextStartedActivity(); + final Intent chooserIntent = ((ShadowActivity)ShadowExtractor.extract(mActivity)).getNextStartedActivity(); assertNotNull("Dialog was not displayed", chooserIntent); assertEquals(Intent.ACTION_CHOOSER, chooserIntent.getAction()); assertEquals(dialogTitle, chooserIntent.getExtras().get(Intent.EXTRA_TITLE)); From ae290f03caf6a3735e64eb3d83043e72c16f464d Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Fri, 6 May 2016 19:48:25 +0900 Subject: [PATCH 26/37] add jsr dep for @Nullable annotation --- ReactAndroid/src/test/java/com/facebook/react/modules/BUCK | 1 + 1 file changed, 1 insertion(+) diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK index 3f8b9436e74f1a..906f58da31ade0 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK @@ -31,6 +31,7 @@ robolectric3_test( react_native_dep('third-party/java/okio:okio'), react_native_dep('third-party/java/mockito:mockito'), react_native_dep('third-party/java/okhttp:okhttp'), + react_native_dep('third-party/java/jsr-305:jsr-305'), ], visibility = [ 'PUBLIC' From f4d7d3a4609e1dfdcca9dfe15c270263f6b22b84 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Tue, 12 Jul 2016 19:05:28 +0900 Subject: [PATCH 27/37] replace `Intent.createChooser` with custom share dialog --- Examples/UIExplorer/ShareExample.js | 29 +++-- Libraries/Share/Share.js | 17 ++- .../com/facebook/react/modules/share/BUCK | 1 + .../modules/share/ChooserArrayAdapter.java | 60 +++++++++ .../modules/share/ShareDialogFragment.java | 93 ++++++++++++++ .../react/modules/share/ShareModule.java | 116 ++++++++++++++++-- .../share/SupportShareDialogFragment.java | 81 ++++++++++++ 7 files changed, 373 insertions(+), 24 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/share/ChooserArrayAdapter.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareDialogFragment.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/share/SupportShareDialogFragment.java diff --git a/Examples/UIExplorer/ShareExample.js b/Examples/UIExplorer/ShareExample.js index b8d30bdc4d4b3a..0d8a98293e4dc2 100644 --- a/Examples/UIExplorer/ShareExample.js +++ b/Examples/UIExplorer/ShareExample.js @@ -46,6 +46,7 @@ class ShareMessageExample extends React.Component { this._shareMessage = this._shareMessage.bind(this); this._shareText = this._shareText.bind(this); + this._showResult = this._showResult.bind(this); this.state = { result: '' @@ -76,13 +77,7 @@ class ShareMessageExample extends React.Component { Share.share({ message: 'React Native | A framework for building native apps using React' }) - .then((result) => { - if(result && result.activityType) { - this.setState({result: 'success: shared with ' + result.activityType}) - } else { - this.setState({result: 'success'}) - } - }) + .then(this._showResult) .catch((error) => this.setState({result: 'error: ' + error.message})) } @@ -98,14 +93,22 @@ class ShareMessageExample extends React.Component { ], tintColor: 'green' }) - .then((result) => { - if(result && result.activityType) { - this.setState({result: 'success: shared with ' + result.activityType}) + .then(this._showResult) + .catch((error) => this.setState({result: 'error: ' + error.message})) + } + + _showResult(result) { + if(result.action === Share.sharedAction) { + if(result.packageName) { + this.setState({result: 'shared with a packageName: ' + result.packageName}) + } else if(result.activityType) { + this.setState({result: 'shared with an activityType: ' + result.activityType}) } else { - this.setState({result: 'success'}) + this.setState({result: 'shared'}) } - }) - .catch((error) => this.setState({result: 'error: ' + error.message})) + } else if(result.action === Share.dismissedAction) { + this.setState({result: 'dismissed'}) + } } } diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index 66c68c1e7b507e..fd79465c017aff 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -23,7 +23,7 @@ type Content = { title?: string, message: string } | { title?: string, url: stri type Options = { dialogTitle?: string, excludeActivityTypes?: Array, tintColor?: string }; class Share { - + /** * Open a dialog to share text content. * @@ -74,10 +74,13 @@ class Share { (success, activityType) => { if (success) { resolve({ + 'action': 'sharedAction', 'activityType': activityType }) } else { - reject(new Error('User canceled')) + resolve({ + 'action': 'dismissedAction' + }) } } ); @@ -88,6 +91,16 @@ class Share { } } + /** + * The content was successfully shared. + */ + static get sharedAction() { return 'sharedAction'; } + + /** + * The dialog has been dismissed. + */ + static get dismissedAction() { return 'dismissedAction'; } + } module.exports = Share; \ No newline at end of file diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK index 4e95826b0dda6c..b5cdf103b35c83 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK @@ -7,6 +7,7 @@ android_library( react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/common:common'), react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), + react_native_dep('third-party/android/support/v4:lib-support-v4'), react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/jsr-305:jsr-305'), ], diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ChooserArrayAdapter.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ChooserArrayAdapter.java new file mode 100644 index 00000000000000..2c6cc95ed65028 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ChooserArrayAdapter.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.modules.share; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.List; + +public class ChooserArrayAdapter extends ArrayAdapter { + PackageManager mPm; + int mTextViewResourceId; + List mPackages; + + public ChooserArrayAdapter(Context context, int resource, int textViewResourceId, List packages) { + super(context, resource, textViewResourceId, packages); + mPm = context.getPackageManager(); + mTextViewResourceId = textViewResourceId; + mPackages = packages; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + String pkg = mPackages.get(position); + View view = super.getView(position, convertView, parent); + + try { + ApplicationInfo ai = mPm.getApplicationInfo(pkg, 0); + + CharSequence appName = mPm.getApplicationLabel(ai); + Drawable appIcon = mPm.getApplicationIcon(pkg); + + TextView textView = (TextView) view.findViewById(mTextViewResourceId); + textView.setText(appName); + textView.setCompoundDrawablesWithIntrinsicBounds(appIcon, null, null, null); + textView.setCompoundDrawablePadding((int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 12, getContext().getResources().getDisplayMetrics()) + ); + } catch (NameNotFoundException e) { + e.printStackTrace(); + } + + return view; + } + } \ No newline at end of file diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareDialogFragment.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareDialogFragment.java new file mode 100644 index 00000000000000..2ef860bf14cb42 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareDialogFragment.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.modules.share; + +import javax.annotation.Nullable; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.widget.ArrayAdapter; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("ValidFragment") +public class ShareDialogFragment extends DialogFragment { + + /* package */ static final String ARG_TITLE = "title"; + + @Nullable ShareModule.ShareDialogListener mListener; + private Intent mIntent; + + public ShareDialogFragment(Intent intent) { + mIntent = intent; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle args = getArguments(); + + final List packages = new ArrayList(); + final List resInfos = getActivity().getPackageManager().queryIntentActivities(mIntent, 0); + for (ResolveInfo resInfo : resInfos) { + String packageName = resInfo.activityInfo.packageName; + packages.add(packageName); + } + + ArrayAdapter adapter = new ChooserArrayAdapter( + getActivity(), android.R.layout.select_dialog_item, android.R.id.text1, packages); + + final OnClickListener onClickItem = new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item ) { + String packageName = packages.get(item); + mIntent.setComponent(new ComponentName(packageName, resInfos.get(item).activityInfo.name)); + mIntent.setPackage(packageName); + getActivity().startActivity(mIntent); + if (mListener != null) { + mListener.onClick(dialog, packageName); + } + } + }; + + return createDialog(args, getActivity(), onClickItem, adapter); + } + + /* package */ static Dialog createDialog( + Bundle args, Context activityContext, @Nullable OnClickListener onClickListener, ArrayAdapter adapter + ) { + + AlertDialog.Builder builder = new AlertDialog.Builder(activityContext) + .setTitle(args.getString(ARG_TITLE)) + .setAdapter(adapter, onClickListener); + return builder.create(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (mListener != null) { + mListener.onDismiss(dialog); + } + } + + public void setListener(@Nullable ShareModule.ShareDialogListener listener) { + mListener = listener; + } + + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java index 274c495e84f582..eaab9be5449588 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -9,9 +9,22 @@ package com.facebook.react.modules.share; +import java.util.List; +import java.util.ArrayList; + import android.app.Activity; +import android.app.AlertDialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.content.ComponentName; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; import android.content.Intent; +import android.content.pm.ResolveInfo; import android.net.Uri; +import android.widget.ArrayAdapter; +import android.os.Bundle; import com.facebook.common.logging.FLog; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; @@ -20,18 +33,32 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.annotations.VisibleForTesting; /** * Intent module. Launch other activities or open URLs. */ public class ShareModule extends ReactContextBaseJavaModule { - /* package */ static final String ERROR_INVALID_CONTENT = "E_INVALID_CONTENT"; - /* package */ static final String ERROR_UNABLE_TO_OPEN_DIALOG = "E_UNABLE_TO_OPEN_DIALOG"; + @VisibleForTesting + public static final String FRAGMENT_TAG = "ShareAndroid"; + + /* package */ static final String ACTION_SHARED = "sharedAction"; + /* package */ static final String ACTION_DISMISSED = "dismissedAction"; + + static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY"; + static final String ERROR_INVALID_CONTENT = "E_INVALID_CONTENT"; + static final String ERROR_UNABLE_TO_OPEN_DIALOG = "E_UNABLE_TO_OPEN_DIALOG"; + static final String ERROR_NO_PACKAGE_TO_SHARE = "E_NO_PACKAGE_TO_SHARE"; + + private ReactApplicationContext mContext; public ShareModule(ReactApplicationContext reactContext) { super(reactContext); + mContext = reactContext; } @Override @@ -39,6 +66,41 @@ public String getName() { return "ShareModule"; } + interface OnClickItemListener { + void onClick(DialogInterface dialog, String packageName); + } + + /* package */ class ShareDialogListener implements OnClickItemListener, OnDismissListener { + + private final Promise mPromise; + private boolean mPromiseResolved = false; + + public ShareDialogListener(Promise promise) { + mPromise = promise; + } + + @Override + public void onClick(DialogInterface dialog, String packageName) { + if (!mPromiseResolved && getReactApplicationContext().hasActiveCatalystInstance()) { + WritableMap result = new WritableNativeMap(); + result.putString("action", ACTION_SHARED); + result.putString("packageName", packageName); + mPromise.resolve(result); + mPromiseResolved = true; + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + if (!mPromiseResolved && getReactApplicationContext().hasActiveCatalystInstance()) { + WritableMap result = new WritableNativeMap(); + result.putString("action", ACTION_DISMISSED); + mPromise.resolve(result); + mPromiseResolved = true; + } + } + } + /** * Open a chooser dialog to send text content to other apps. * @@ -70,16 +132,52 @@ public void share(ReadableMap content, String dialogTitle, Promise promise) { intent.putExtra(Intent.EXTRA_TEXT, content.getString("url")); // this will overwrite message } - //TODO: use createChooser (Intent target, CharSequence dialogTitle, IntentSender sender) after API level 22 - Intent chooser = Intent.createChooser(intent, dialogTitle); + Activity activity = getCurrentActivity(); + if (activity == null) { + promise.reject( + ERROR_NO_ACTIVITY, + "Tried to open a Share dialog while not attached to an Activity"); + return; + } + + // We want to support both android.app.Activity and the pre-Honeycomb FragmentActivity + // (for apps that use it for legacy reasons). This unfortunately leads to some code duplication. + if (activity instanceof android.support.v4.app.FragmentActivity) { + android.support.v4.app.FragmentManager fragmentManager = + ((android.support.v4.app.FragmentActivity) activity).getSupportFragmentManager(); + android.support.v4.app.DialogFragment oldFragment = + (android.support.v4.app.DialogFragment)fragmentManager.findFragmentByTag(FRAGMENT_TAG); + if (oldFragment != null) { + oldFragment.dismiss(); + } + + SupportShareDialogFragment fragment = new SupportShareDialogFragment(intent); + + final Bundle args = new Bundle(); + args.putString(ShareDialogFragment.ARG_TITLE, dialogTitle); + fragment.setArguments(args); - Activity currentActivity = getCurrentActivity(); - if (currentActivity != null) { - currentActivity.startActivity(chooser); + ShareDialogListener listener = new ShareDialogListener(promise); + fragment.setListener(listener); + fragment.show(fragmentManager, FRAGMENT_TAG); } else { - getReactApplicationContext().startActivity(chooser); + FragmentManager fragmentManager = activity.getFragmentManager(); + DialogFragment oldFragment = (DialogFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG); + if (oldFragment != null) { + oldFragment.dismiss(); + } + + ShareDialogFragment fragment = new ShareDialogFragment(intent); + + final Bundle args = new Bundle(); + args.putString(ShareDialogFragment.ARG_TITLE, dialogTitle); + fragment.setArguments(args); + + ShareDialogListener listener = new ShareDialogListener(promise); + fragment.setListener(listener); + fragment.show(fragmentManager, FRAGMENT_TAG); } - promise.resolve(null); + } catch (Exception e) { FLog.e(ReactConstants.TAG, "Failed to open share dialog", e); promise.reject(ERROR_UNABLE_TO_OPEN_DIALOG, "Failed to open share dialog"); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/SupportShareDialogFragment.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/SupportShareDialogFragment.java new file mode 100644 index 00000000000000..c620e499b10f06 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/SupportShareDialogFragment.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of 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.share; + +import javax.annotation.Nullable; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.widget.ArrayAdapter; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("ValidFragment") +public class SupportShareDialogFragment extends DialogFragment { + + @Nullable ShareModule.ShareDialogListener mListener; + private Intent mIntent; + + public SupportShareDialogFragment(Intent intent) { + mIntent = intent; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle args = getArguments(); + + final List packages = new ArrayList(); + final List resInfos = getActivity().getPackageManager().queryIntentActivities(mIntent, 0); + for (ResolveInfo resInfo : resInfos) { + String packageName = resInfo.activityInfo.packageName; + packages.add(packageName); + } + + ArrayAdapter adapter = new ChooserArrayAdapter( + getActivity(), android.R.layout.select_dialog_item, android.R.id.text1, packages); + + final OnClickListener onClickItem = new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item ) { + String packageName = packages.get(item); + mIntent.setComponent(new ComponentName(packageName, resInfos.get(item).activityInfo.name)); + mIntent.setPackage(packageName); + getActivity().startActivity(mIntent); + if (mListener != null) { + mListener.onClick(dialog, packageName); + } + } + }; + + return ShareDialogFragment.createDialog(args, getActivity(), onClickItem, adapter); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (mListener != null) { + mListener.onDismiss(dialog); + } + } + + public void setListener(@Nullable ShareModule.ShareDialogListener listener) { + mListener = listener; + } +} From 29b5e3bacc620819790f90749979888be7abdf41 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Wed, 13 Jul 2016 13:54:56 +0900 Subject: [PATCH 28/37] android integration tests --- .../java/com/facebook/react/tests/BUCK | 1 + .../facebook/react/tests/ShareTestCase.java | 162 ++++++++++++++++++ .../src/androidTest/js/ShareTestModule.js | 47 +++++ ReactAndroid/src/androidTest/js/TestBundle.js | 5 + 4 files changed, 215 insertions(+) create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java create mode 100644 ReactAndroid/src/androidTest/js/ShareTestModule.js diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK index 8f16de6cb9958f..3fbf31cc13fb94 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK @@ -16,6 +16,7 @@ deps = [ react_native_target('java/com/facebook/react/common:common'), react_native_target('java/com/facebook/react/modules/core:core'), react_native_target('java/com/facebook/react/modules/datepicker:datepicker'), + react_native_target('java/com/facebook/react/modules/share:share'), react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), react_native_target('java/com/facebook/react/modules/timepicker:timepicker'), react_native_target('java/com/facebook/react/touch:touch'), diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java new file mode 100644 index 00000000000000..25f6a876cb56e9 --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.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. + */ + +package com.facebook.react.tests; + +import java.util.ArrayList; +import java.util.List; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.support.v4.app.DialogFragment; + +import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.testing.ReactInstanceSpecForTest; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.modules.share.ShareModule; +import com.facebook.react.testing.ReactAppInstrumentationTestCase; + +/** + * Test case for {@link ShareModule}. + */ +public class ShareTestCase extends ReactAppInstrumentationTestCase { + + private static interface ShareTestModule extends JavaScriptModule { + public void showShareDialog(WritableMap content, WritableMap options); + } + + private static class ShareRecordingModule extends BaseJavaModule { + + private int mShared = 0; + private int mDismissed = 0; + private int mErrors = 0; + + @Override + public String getName() { + return "ShareRecordingModule"; + } + + @ReactMethod + public void recordShared() { + mShared++; + } + + @ReactMethod + public void recordDismissed() { + mDismissed++; + } + + @ReactMethod + public void recordError() { + mErrors++; + } + + public int getShared() { + return mShared; + } + + public int getDismissed() { + return mDismissed; + } + + public int getErrors() { + return mErrors; + } + } + + final ShareRecordingModule mRecordingModule = new ShareRecordingModule(); + + @Override + protected ReactInstanceSpecForTest createReactInstanceSpecForTest() { + return super.createReactInstanceSpecForTest() + .addNativeModule(mRecordingModule) + .addJSModule(ShareTestModule.class); + } + + @Override + protected String getReactApplicationKeyUnderTest() { + return "ShareTestApp"; + } + + private ShareTestModule getTestModule() { + return getReactContext().getCatalystInstance().getJSModule(ShareTestModule.class); + } + + private DialogFragment showDialog(WritableMap content, WritableMap options) { + getTestModule().showShareDialog(content, options); + + waitForBridgeAndUIIdle(); + getInstrumentation().waitForIdleSync(); + + return (DialogFragment) getActivity().getSupportFragmentManager() + .findFragmentByTag(ShareModule.FRAGMENT_TAG); + } + + public void testShowBasicShareDialog() { + final WritableMap content = new WritableNativeMap(); + content.putString("message", "Hello, ReactNative!"); + final WritableMap options = new WritableNativeMap(); + + final DialogFragment fragment = showDialog(content, options); + + assertNotNull(fragment); + } + + public void testShareCallback() throws Throwable { + final WritableMap content = new WritableNativeMap(); + content.putString("message", "Hello, ReactNative!"); + final WritableMap options = new WritableNativeMap(); + + final DialogFragment fragment = showDialog(content, options); + + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + ((AlertDialog) fragment.getDialog()) + .getListView().performItemClick(null, 0, 0); + } + }); + + getInstrumentation().waitForIdleSync(); + waitForBridgeAndUIIdle(); + + assertEquals(0, mRecordingModule.getErrors()); + assertEquals(1, mRecordingModule.getShared()); + assertEquals(0, mRecordingModule.getDismissed()); + } + + public void testDismissCallback() throws Throwable { + final WritableMap content = new WritableNativeMap(); + content.putString("message", "Hello, ReactNative!"); + final WritableMap options = new WritableNativeMap(); + + final DialogFragment fragment = showDialog(content, options); + + assertEquals(0, mRecordingModule.getDismissed()); + + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + fragment.getDialog().dismiss(); + } + }); + + getInstrumentation().waitForIdleSync(); + waitForBridgeAndUIIdle(); + + assertEquals(0, mRecordingModule.getErrors()); + assertEquals(0, mRecordingModule.getShared()); + assertEquals(1, mRecordingModule.getDismissed()); + } + +} diff --git a/ReactAndroid/src/androidTest/js/ShareTestModule.js b/ReactAndroid/src/androidTest/js/ShareTestModule.js new file mode 100644 index 00000000000000..318ebd89e314dc --- /dev/null +++ b/ReactAndroid/src/androidTest/js/ShareTestModule.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in 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 ShareTestModule + */ + +'use strict'; + +var BatchedBridge = require('BatchedBridge'); +var React = require('React'); +var RecordingModule = require('NativeModules').ShareRecordingModule; +var Share = require('Share'); +var View = require('View'); + +var ShareTestApp = React.createClass({ + render: function() { + return (); + }, +}); + +var ShareTestModule = { + ShareTestApp: ShareTestApp, + showShareDialog: function(content, options) { + Share.share(content, options).then( + ({action, className}) => { + if (action === Share.sharedAction) { + RecordingModule.recordShared(); + } else if (action === Share.dismissedAction) { + RecordingModule.recordDismissed(); + } + }, + ({code, message}) => RecordingModule.recordError() + ); + }, +}; + +BatchedBridge.registerCallableModule( + 'ShareTestModule', + ShareTestModule +); + +module.exports = ShareTestModule; diff --git a/ReactAndroid/src/androidTest/js/TestBundle.js b/ReactAndroid/src/androidTest/js/TestBundle.js index 960c485a371f6c..f560068a3a41e8 100644 --- a/ReactAndroid/src/androidTest/js/TestBundle.js +++ b/ReactAndroid/src/androidTest/js/TestBundle.js @@ -25,6 +25,7 @@ require('DatePickerDialogTestModule'); require('MeasureLayoutTestModule'); require('PickerAndroidTestModule'); require('ScrollViewTestModule'); +require('ShareTestModule'); require('SwipeRefreshLayoutTestModule'); require('TextInputTestModule'); require('TimePickerDialogTestModule'); @@ -74,6 +75,10 @@ var apps = [ appKey: 'ScrollViewTestApp', component: () => require('ScrollViewTestModule').ScrollViewTestApp, }, +{ + appKey: 'ShareTestApp', + component: () => require('ShareTestModule').ShareTestApp, +}, { appKey: 'SubviewsClippingTestApp', component: () => require('SubviewsClippingTestModule').App, From db99ac49504805a40b7f39569377ae625adb6b09 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Wed, 13 Jul 2016 14:53:26 +0900 Subject: [PATCH 29/37] lint fix --- Examples/UIExplorer/ShareExample.js | 24 ++++++++++++------------ Libraries/Share/Share.js | 16 ++++++++-------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Examples/UIExplorer/ShareExample.js b/Examples/UIExplorer/ShareExample.js index 0d8a98293e4dc2..199e18ae1a8f74 100644 --- a/Examples/UIExplorer/ShareExample.js +++ b/Examples/UIExplorer/ShareExample.js @@ -21,7 +21,6 @@ var { StyleSheet, View, Text, - TextInput, TouchableHighlight, Share, } = ReactNative; @@ -39,6 +38,7 @@ exports.examples = [{ class ShareMessageExample extends React.Component { _shareMessage: Function; _shareText: Function; + _showResult: Function; state: any; constructor(props) { @@ -78,12 +78,12 @@ class ShareMessageExample extends React.Component { message: 'React Native | A framework for building native apps using React' }) .then(this._showResult) - .catch((error) => this.setState({result: 'error: ' + error.message})) + .catch((error) => this.setState({result: 'error: ' + error.message})); } _shareText() { Share.share({ - message: 'A framework for building native apps using React', + message: 'A framework for building native apps using React', url: 'http://facebook.github.io/react-native/', title: 'React Native' }, { @@ -94,20 +94,20 @@ class ShareMessageExample extends React.Component { tintColor: 'green' }) .then(this._showResult) - .catch((error) => this.setState({result: 'error: ' + error.message})) + .catch((error) => this.setState({result: 'error: ' + error.message})); } _showResult(result) { - if(result.action === Share.sharedAction) { - if(result.packageName) { - this.setState({result: 'shared with a packageName: ' + result.packageName}) - } else if(result.activityType) { - this.setState({result: 'shared with an activityType: ' + result.activityType}) + if (result.action === Share.sharedAction) { + if (result.packageName) { + this.setState({result: 'shared with a packageName: ' + result.packageName}); + } else if (result.activityType) { + this.setState({result: 'shared with an activityType: ' + result.activityType}); } else { - this.setState({result: 'shared'}) + this.setState({result: 'shared'}); } - } else if(result.action === Share.dismissedAction) { - this.setState({result: 'dismissed'}) + } else if (result.action === Share.dismissedAction) { + this.setState({result: 'dismissed'}); } } diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index fd79465c017aff..cb052701f2700b 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -26,15 +26,15 @@ class Share { /** * Open a dialog to share text content. - * + * * ### Content - * + * * - `message` - a message to share * - `url` - an URL to share. In Android, this will overwrite message * - `title` - title of the message - * + * * At least one of URL and message is required. - * + * * ### Options * * #### iOS @@ -43,7 +43,7 @@ class Share { * - `tintColor` * * #### Android - * + * * - `dialogTitle` */ static share(content: Content, options: Options = {}): Promise { @@ -76,11 +76,11 @@ class Share { resolve({ 'action': 'sharedAction', 'activityType': activityType - }) + }); } else { resolve({ 'action': 'dismissedAction' - }) + }); } } ); @@ -103,4 +103,4 @@ class Share { } -module.exports = Share; \ No newline at end of file +module.exports = Share; From d0f6fb8ec53f8620c99bc18ccb1ed3016267c14a Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Wed, 13 Jul 2016 16:46:59 +0900 Subject: [PATCH 30/37] Block launching activity for next test cases --- .../java/com/facebook/react/tests/ShareTestCase.java | 9 +++++++++ .../com/facebook/react/modules/share/ShareModule.java | 1 + 2 files changed, 10 insertions(+) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java index 25f6a876cb56e9..6dc714882bad1a 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java @@ -13,6 +13,8 @@ import android.app.AlertDialog; import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; import android.support.v4.app.DialogFragment; import com.facebook.react.bridge.BaseJavaModule; @@ -70,6 +72,7 @@ public int getDismissed() { public int getErrors() { return mErrors; } + } final ShareRecordingModule mRecordingModule = new ShareRecordingModule(); @@ -117,6 +120,12 @@ public void testShareCallback() throws Throwable { final DialogFragment fragment = showDialog(content, options); + /* Block launching activity for next test cases */ + IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SEND); + intentFilter.addCategory(Intent.CATEGORY_DEFAULT); + intentFilter.addDataType("text/plain"); + getInstrumentation().addMonitor(intentFilter, null, true); + runTestOnUiThread( new Runnable() { @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java index eaab9be5449588..a36ff5f5109344 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -119,6 +119,7 @@ public void share(ReadableMap content, String dialogTitle, Promise promise) { try { Intent intent = new Intent(Intent.ACTION_SEND); intent.setTypeAndNormalize("text/plain"); + intent.addCategory(Intent.CATEGORY_DEFAULT); if (content.hasKey("title")) { intent.putExtra(Intent.EXTRA_SUBJECT, content.getString("title")); From bc1445604ef1b17541a670cce996909cfb8a0220 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Thu, 14 Jul 2016 12:34:18 +0900 Subject: [PATCH 31/37] back to `Intent.createChooser` --- Examples/UIExplorer/ShareExample.js | 4 +- .../facebook/react/tests/ShareTestCase.java | 92 +++----------- .../src/androidTest/js/ShareTestModule.js | 8 +- .../modules/share/ChooserArrayAdapter.java | 60 --------- .../modules/share/ShareDialogFragment.java | 93 -------------- .../react/modules/share/ShareModule.java | 117 ++---------------- .../share/SupportShareDialogFragment.java | 81 ------------ 7 files changed, 29 insertions(+), 426 deletions(-) delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/share/ChooserArrayAdapter.java delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareDialogFragment.java delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/share/SupportShareDialogFragment.java diff --git a/Examples/UIExplorer/ShareExample.js b/Examples/UIExplorer/ShareExample.js index 199e18ae1a8f74..4a2f2b84d3e03e 100644 --- a/Examples/UIExplorer/ShareExample.js +++ b/Examples/UIExplorer/ShareExample.js @@ -99,9 +99,7 @@ class ShareMessageExample extends React.Component { _showResult(result) { if (result.action === Share.sharedAction) { - if (result.packageName) { - this.setState({result: 'shared with a packageName: ' + result.packageName}); - } else if (result.activityType) { + if (result.activityType) { this.setState({result: 'shared with an activityType: ' + result.activityType}); } else { this.setState({result: 'shared'}); diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java index 6dc714882bad1a..13ef775f3f4145 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ShareTestCase.java @@ -11,10 +11,13 @@ import java.util.ArrayList; import java.util.List; +import android.app.Activity; import android.app.AlertDialog; +import android.app.Instrumentation.ActivityMonitor; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentFilter.MalformedMimeTypeException; import android.support.v4.app.DialogFragment; import com.facebook.react.bridge.BaseJavaModule; @@ -37,8 +40,7 @@ private static interface ShareTestModule extends JavaScriptModule { private static class ShareRecordingModule extends BaseJavaModule { - private int mShared = 0; - private int mDismissed = 0; + private int mOpened = 0; private int mErrors = 0; @Override @@ -47,13 +49,8 @@ public String getName() { } @ReactMethod - public void recordShared() { - mShared++; - } - - @ReactMethod - public void recordDismissed() { - mDismissed++; + public void recordOpened() { + mOpened++; } @ReactMethod @@ -61,12 +58,8 @@ public void recordError() { mErrors++; } - public int getShared() { - return mShared; - } - - public int getDismissed() { - return mDismissed; + public int getOpened() { + return mOpened; } public int getErrors() { @@ -93,79 +86,24 @@ private ShareTestModule getTestModule() { return getReactContext().getCatalystInstance().getJSModule(ShareTestModule.class); } - private DialogFragment showDialog(WritableMap content, WritableMap options) { - getTestModule().showShareDialog(content, options); - - waitForBridgeAndUIIdle(); - getInstrumentation().waitForIdleSync(); - - return (DialogFragment) getActivity().getSupportFragmentManager() - .findFragmentByTag(ShareModule.FRAGMENT_TAG); - } - public void testShowBasicShareDialog() { final WritableMap content = new WritableNativeMap(); content.putString("message", "Hello, ReactNative!"); final WritableMap options = new WritableNativeMap(); - final DialogFragment fragment = showDialog(content, options); - - assertNotNull(fragment); - } - - public void testShareCallback() throws Throwable { - final WritableMap content = new WritableNativeMap(); - content.putString("message", "Hello, ReactNative!"); - final WritableMap options = new WritableNativeMap(); - - final DialogFragment fragment = showDialog(content, options); - - /* Block launching activity for next test cases */ - IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SEND); + IntentFilter intentFilter = new IntentFilter(Intent.ACTION_CHOOSER); intentFilter.addCategory(Intent.CATEGORY_DEFAULT); - intentFilter.addDataType("text/plain"); - getInstrumentation().addMonitor(intentFilter, null, true); - - runTestOnUiThread( - new Runnable() { - @Override - public void run() { - ((AlertDialog) fragment.getDialog()) - .getListView().performItemClick(null, 0, 0); - } - }); - - getInstrumentation().waitForIdleSync(); - waitForBridgeAndUIIdle(); - - assertEquals(0, mRecordingModule.getErrors()); - assertEquals(1, mRecordingModule.getShared()); - assertEquals(0, mRecordingModule.getDismissed()); - } - - public void testDismissCallback() throws Throwable { - final WritableMap content = new WritableNativeMap(); - content.putString("message", "Hello, ReactNative!"); - final WritableMap options = new WritableNativeMap(); - - final DialogFragment fragment = showDialog(content, options); - - assertEquals(0, mRecordingModule.getDismissed()); + ActivityMonitor monitor = getInstrumentation().addMonitor(intentFilter, null, true); - runTestOnUiThread( - new Runnable() { - @Override - public void run() { - fragment.getDialog().dismiss(); - } - }); + getTestModule().showShareDialog(content, options); - getInstrumentation().waitForIdleSync(); waitForBridgeAndUIIdle(); + getInstrumentation().waitForIdleSync(); + assertEquals(1, monitor.getHits()); + assertEquals(1, mRecordingModule.getOpened()); assertEquals(0, mRecordingModule.getErrors()); - assertEquals(0, mRecordingModule.getShared()); - assertEquals(1, mRecordingModule.getDismissed()); + } } diff --git a/ReactAndroid/src/androidTest/js/ShareTestModule.js b/ReactAndroid/src/androidTest/js/ShareTestModule.js index 318ebd89e314dc..dbba70103160d3 100644 --- a/ReactAndroid/src/androidTest/js/ShareTestModule.js +++ b/ReactAndroid/src/androidTest/js/ShareTestModule.js @@ -27,13 +27,7 @@ var ShareTestModule = { ShareTestApp: ShareTestApp, showShareDialog: function(content, options) { Share.share(content, options).then( - ({action, className}) => { - if (action === Share.sharedAction) { - RecordingModule.recordShared(); - } else if (action === Share.dismissedAction) { - RecordingModule.recordDismissed(); - } - }, + () => RecordingModule.recordOpened(), ({code, message}) => RecordingModule.recordError() ); }, diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ChooserArrayAdapter.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ChooserArrayAdapter.java deleted file mode 100644 index 2c6cc95ed65028..00000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ChooserArrayAdapter.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -package com.facebook.react.modules.share; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.graphics.drawable.Drawable; -import android.util.TypedValue; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.TextView; - -import java.util.List; - -public class ChooserArrayAdapter extends ArrayAdapter { - PackageManager mPm; - int mTextViewResourceId; - List mPackages; - - public ChooserArrayAdapter(Context context, int resource, int textViewResourceId, List packages) { - super(context, resource, textViewResourceId, packages); - mPm = context.getPackageManager(); - mTextViewResourceId = textViewResourceId; - mPackages = packages; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - String pkg = mPackages.get(position); - View view = super.getView(position, convertView, parent); - - try { - ApplicationInfo ai = mPm.getApplicationInfo(pkg, 0); - - CharSequence appName = mPm.getApplicationLabel(ai); - Drawable appIcon = mPm.getApplicationIcon(pkg); - - TextView textView = (TextView) view.findViewById(mTextViewResourceId); - textView.setText(appName); - textView.setCompoundDrawablesWithIntrinsicBounds(appIcon, null, null, null); - textView.setCompoundDrawablePadding((int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 12, getContext().getResources().getDisplayMetrics()) - ); - } catch (NameNotFoundException e) { - e.printStackTrace(); - } - - return view; - } - } \ No newline at end of file diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareDialogFragment.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareDialogFragment.java deleted file mode 100644 index 2ef860bf14cb42..00000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareDialogFragment.java +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -package com.facebook.react.modules.share; - -import javax.annotation.Nullable; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.ComponentName; -import android.content.Intent; -import android.content.pm.ResolveInfo; -import android.os.Bundle; -import android.widget.ArrayAdapter; - -import java.util.ArrayList; -import java.util.List; - -@SuppressWarnings("ValidFragment") -public class ShareDialogFragment extends DialogFragment { - - /* package */ static final String ARG_TITLE = "title"; - - @Nullable ShareModule.ShareDialogListener mListener; - private Intent mIntent; - - public ShareDialogFragment(Intent intent) { - mIntent = intent; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Bundle args = getArguments(); - - final List packages = new ArrayList(); - final List resInfos = getActivity().getPackageManager().queryIntentActivities(mIntent, 0); - for (ResolveInfo resInfo : resInfos) { - String packageName = resInfo.activityInfo.packageName; - packages.add(packageName); - } - - ArrayAdapter adapter = new ChooserArrayAdapter( - getActivity(), android.R.layout.select_dialog_item, android.R.id.text1, packages); - - final OnClickListener onClickItem = new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int item ) { - String packageName = packages.get(item); - mIntent.setComponent(new ComponentName(packageName, resInfos.get(item).activityInfo.name)); - mIntent.setPackage(packageName); - getActivity().startActivity(mIntent); - if (mListener != null) { - mListener.onClick(dialog, packageName); - } - } - }; - - return createDialog(args, getActivity(), onClickItem, adapter); - } - - /* package */ static Dialog createDialog( - Bundle args, Context activityContext, @Nullable OnClickListener onClickListener, ArrayAdapter adapter - ) { - - AlertDialog.Builder builder = new AlertDialog.Builder(activityContext) - .setTitle(args.getString(ARG_TITLE)) - .setAdapter(adapter, onClickListener); - return builder.create(); - } - - @Override - public void onDismiss(DialogInterface dialog) { - super.onDismiss(dialog); - if (mListener != null) { - mListener.onDismiss(dialog); - } - } - - public void setListener(@Nullable ShareModule.ShareDialogListener listener) { - mListener = listener; - } - - -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java index a36ff5f5109344..c20665f4ef2f4c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -9,22 +9,9 @@ package com.facebook.react.modules.share; -import java.util.List; -import java.util.ArrayList; - import android.app.Activity; -import android.app.AlertDialog; -import android.app.DialogFragment; -import android.app.FragmentManager; -import android.content.ComponentName; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.DialogInterface.OnDismissListener; import android.content.Intent; -import android.content.pm.ResolveInfo; import android.net.Uri; -import android.widget.ArrayAdapter; -import android.os.Bundle; import com.facebook.common.logging.FLog; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; @@ -36,29 +23,18 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.annotations.VisibleForTesting; /** * Intent module. Launch other activities or open URLs. */ public class ShareModule extends ReactContextBaseJavaModule { - @VisibleForTesting - public static final String FRAGMENT_TAG = "ShareAndroid"; - /* package */ static final String ACTION_SHARED = "sharedAction"; - /* package */ static final String ACTION_DISMISSED = "dismissedAction"; - - static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY"; - static final String ERROR_INVALID_CONTENT = "E_INVALID_CONTENT"; - static final String ERROR_UNABLE_TO_OPEN_DIALOG = "E_UNABLE_TO_OPEN_DIALOG"; - static final String ERROR_NO_PACKAGE_TO_SHARE = "E_NO_PACKAGE_TO_SHARE"; - - private ReactApplicationContext mContext; + /* package */ static final String ERROR_INVALID_CONTENT = "E_INVALID_CONTENT"; + /* package */ static final String ERROR_UNABLE_TO_OPEN_DIALOG = "E_UNABLE_TO_OPEN_DIALOG"; public ShareModule(ReactApplicationContext reactContext) { super(reactContext); - mContext = reactContext; } @Override @@ -66,41 +42,6 @@ public String getName() { return "ShareModule"; } - interface OnClickItemListener { - void onClick(DialogInterface dialog, String packageName); - } - - /* package */ class ShareDialogListener implements OnClickItemListener, OnDismissListener { - - private final Promise mPromise; - private boolean mPromiseResolved = false; - - public ShareDialogListener(Promise promise) { - mPromise = promise; - } - - @Override - public void onClick(DialogInterface dialog, String packageName) { - if (!mPromiseResolved && getReactApplicationContext().hasActiveCatalystInstance()) { - WritableMap result = new WritableNativeMap(); - result.putString("action", ACTION_SHARED); - result.putString("packageName", packageName); - mPromise.resolve(result); - mPromiseResolved = true; - } - } - - @Override - public void onDismiss(DialogInterface dialog) { - if (!mPromiseResolved && getReactApplicationContext().hasActiveCatalystInstance()) { - WritableMap result = new WritableNativeMap(); - result.putString("action", ACTION_DISMISSED); - mPromise.resolve(result); - mPromiseResolved = true; - } - } - } - /** * Open a chooser dialog to send text content to other apps. * @@ -119,7 +60,6 @@ public void share(ReadableMap content, String dialogTitle, Promise promise) { try { Intent intent = new Intent(Intent.ACTION_SEND); intent.setTypeAndNormalize("text/plain"); - intent.addCategory(Intent.CATEGORY_DEFAULT); if (content.hasKey("title")) { intent.putExtra(Intent.EXTRA_SUBJECT, content.getString("title")); @@ -133,52 +73,19 @@ public void share(ReadableMap content, String dialogTitle, Promise promise) { intent.putExtra(Intent.EXTRA_TEXT, content.getString("url")); // this will overwrite message } - Activity activity = getCurrentActivity(); - if (activity == null) { - promise.reject( - ERROR_NO_ACTIVITY, - "Tried to open a Share dialog while not attached to an Activity"); - return; - } - - // We want to support both android.app.Activity and the pre-Honeycomb FragmentActivity - // (for apps that use it for legacy reasons). This unfortunately leads to some code duplication. - if (activity instanceof android.support.v4.app.FragmentActivity) { - android.support.v4.app.FragmentManager fragmentManager = - ((android.support.v4.app.FragmentActivity) activity).getSupportFragmentManager(); - android.support.v4.app.DialogFragment oldFragment = - (android.support.v4.app.DialogFragment)fragmentManager.findFragmentByTag(FRAGMENT_TAG); - if (oldFragment != null) { - oldFragment.dismiss(); - } - - SupportShareDialogFragment fragment = new SupportShareDialogFragment(intent); - - final Bundle args = new Bundle(); - args.putString(ShareDialogFragment.ARG_TITLE, dialogTitle); - fragment.setArguments(args); + Intent chooser = Intent.createChooser(intent, dialogTitle); + chooser.addCategory(Intent.CATEGORY_DEFAULT); - ShareDialogListener listener = new ShareDialogListener(promise); - fragment.setListener(listener); - fragment.show(fragmentManager, FRAGMENT_TAG); + Activity currentActivity = getCurrentActivity(); + if (currentActivity != null) { + currentActivity.startActivity(chooser); } else { - FragmentManager fragmentManager = activity.getFragmentManager(); - DialogFragment oldFragment = (DialogFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG); - if (oldFragment != null) { - oldFragment.dismiss(); - } - - ShareDialogFragment fragment = new ShareDialogFragment(intent); - - final Bundle args = new Bundle(); - args.putString(ShareDialogFragment.ARG_TITLE, dialogTitle); - fragment.setArguments(args); - - ShareDialogListener listener = new ShareDialogListener(promise); - fragment.setListener(listener); - fragment.show(fragmentManager, FRAGMENT_TAG); + getReactApplicationContext().startActivity(chooser); } - + WritableMap result = new WritableNativeMap(); + result.putString("action", ACTION_SHARED); + promise.resolve(result); + } catch (Exception e) { FLog.e(ReactConstants.TAG, "Failed to open share dialog", e); promise.reject(ERROR_UNABLE_TO_OPEN_DIALOG, "Failed to open share dialog"); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/SupportShareDialogFragment.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/SupportShareDialogFragment.java deleted file mode 100644 index c620e499b10f06..00000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/SupportShareDialogFragment.java +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) 2016-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of 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.share; - -import javax.annotation.Nullable; - -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.ComponentName; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.support.v4.app.DialogFragment; -import android.widget.ArrayAdapter; - -import java.util.ArrayList; -import java.util.List; - -@SuppressWarnings("ValidFragment") -public class SupportShareDialogFragment extends DialogFragment { - - @Nullable ShareModule.ShareDialogListener mListener; - private Intent mIntent; - - public SupportShareDialogFragment(Intent intent) { - mIntent = intent; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Bundle args = getArguments(); - - final List packages = new ArrayList(); - final List resInfos = getActivity().getPackageManager().queryIntentActivities(mIntent, 0); - for (ResolveInfo resInfo : resInfos) { - String packageName = resInfo.activityInfo.packageName; - packages.add(packageName); - } - - ArrayAdapter adapter = new ChooserArrayAdapter( - getActivity(), android.R.layout.select_dialog_item, android.R.id.text1, packages); - - final OnClickListener onClickItem = new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int item ) { - String packageName = packages.get(item); - mIntent.setComponent(new ComponentName(packageName, resInfos.get(item).activityInfo.name)); - mIntent.setPackage(packageName); - getActivity().startActivity(mIntent); - if (mListener != null) { - mListener.onClick(dialog, packageName); - } - } - }; - - return ShareDialogFragment.createDialog(args, getActivity(), onClickItem, adapter); - } - - @Override - public void onDismiss(DialogInterface dialog) { - super.onDismiss(dialog); - if (mListener != null) { - mListener.onDismiss(dialog); - } - } - - public void setListener(@Nullable ShareModule.ShareDialogListener listener) { - mListener = listener; - } -} From a404f49daf7dbd8c243ead07f8ead8a4e22263f1 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Thu, 14 Jul 2016 13:07:09 +0900 Subject: [PATCH 32/37] dep for the `@Nullable` annotation --- ReactAndroid/src/test/java/com/facebook/react/modules/BUCK | 1 + 1 file changed, 1 insertion(+) diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK index 5dcbf954da7051..0b8feaba4e85ef 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK @@ -8,6 +8,7 @@ robolectric3_test( deps = [ react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), react_native_dep('third-party/java/fest:fest'), + react_native_dep('third-party/java/jsr-305:jsr-305'), react_native_dep('third-party/java/junit:junit'), react_native_dep('third-party/java/mockito:mockito'), react_native_dep('third-party/java/okhttp:okhttp3'), From 9c896e5f080f29618d180cf5b46100d5e2d7296a Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Thu, 14 Jul 2016 13:17:30 +0900 Subject: [PATCH 33/37] documentation update --- Libraries/Share/Share.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index cb052701f2700b..1ac5ffc626713b 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -26,6 +26,12 @@ class Share { /** * Open a dialog to share text content. + * + * In iOS, Returns a Promise which will be invoked an object containing `action`, `activityType`. + * If the user dismissed the dialog, the Promise will still be resolved with action being `Share.dismissedAction` + * and all the other keys being undefined. + * + * In Android, Returns a Promise which always be resolved with action being `Share.sharedAction`. * * ### Content * @@ -45,6 +51,7 @@ class Share { * #### Android * * - `dialogTitle` + * */ static share(content: Content, options: Options = {}): Promise { invariant( @@ -98,6 +105,7 @@ class Share { /** * The dialog has been dismissed. + * @platform ios */ static get dismissedAction() { return 'dismissedAction'; } From 5127f3033e2789aa8a085bc15430795a56f37fc0 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Fri, 15 Jul 2016 15:19:07 +0900 Subject: [PATCH 34/37] move example file (#2f73ca8) --- Examples/UIExplorer/{ => js}/ShareExample.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Examples/UIExplorer/{ => js}/ShareExample.js (100%) diff --git a/Examples/UIExplorer/ShareExample.js b/Examples/UIExplorer/js/ShareExample.js similarity index 100% rename from Examples/UIExplorer/ShareExample.js rename to Examples/UIExplorer/js/ShareExample.js From 2b52ff55e71dbbf2b7f16a3e0a7a661565f03182 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Fri, 22 Jul 2016 16:24:43 +0900 Subject: [PATCH 35/37] fix unit tests --- .../com/facebook/react/modules/share/BUCK | 1 - .../react/modules/share/ShareModule.java | 4 +- .../react/modules/share/ShareModuleTest.java | 129 ++++++++++-------- 3 files changed, 76 insertions(+), 58 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK index b5cdf103b35c83..4e95826b0dda6c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/BUCK @@ -7,7 +7,6 @@ android_library( react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/common:common'), react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), - react_native_dep('third-party/android/support/v4:lib-support-v4'), react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/jsr-305:jsr-305'), ], diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java index c20665f4ef2f4c..6d69b2033ed2e5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -14,6 +14,7 @@ import android.net.Uri; import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; @@ -21,7 +22,6 @@ import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; -import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.common.ReactConstants; /** @@ -82,7 +82,7 @@ public void share(ReadableMap content, String dialogTitle, Promise promise) { } else { getReactApplicationContext().startActivity(chooser); } - WritableMap result = new WritableNativeMap(); + WritableMap result = Arguments.createMap(); result.putString("action", ACTION_SHARED); promise.resolve(result); diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java index 027171848ce831..f9d1b31e7037db 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/share/ShareModuleTest.java @@ -12,8 +12,10 @@ import android.app.Activity; import android.content.Intent; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactTestHelper; import com.facebook.react.bridge.JavaOnlyMap; import javax.annotation.Nullable; @@ -21,17 +23,25 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.Rule; import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.internal.ShadowExtractor; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; -import org.robolectric.shadows.ShadowActivity; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowApplication; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +@PrepareForTest({Arguments.class}) @RunWith(RobolectricTestRunner.class) @PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) public class ShareModuleTest { @@ -39,6 +49,69 @@ public class ShareModuleTest { private Activity mActivity; private ShareModule mShareModule; + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Before + public void prepareModules() throws Exception { + PowerMockito.mockStatic(Arguments.class); + Mockito.when(Arguments.createMap()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return new JavaOnlyMap(); + } + }); + + mShareModule = new ShareModule(ReactTestHelper.createCatalystContextForTest()); + } + + @After + public void cleanUp() { + mActivity = null; + mShareModule = null; + } + + @Test + public void testShareDialog() { + final String title = "Title"; + final String message = "Message"; + final String dialogTitle = "Dialog Title"; + + JavaOnlyMap content = new JavaOnlyMap(); + content.putString("title", title); + content.putString("message", message); + + final SimplePromise promise = new SimplePromise(); + + mShareModule.share(content, dialogTitle, promise); + + final Intent chooserIntent = + ((ShadowApplication)ShadowExtractor.extract(RuntimeEnvironment.application)).getNextStartedActivity(); + assertNotNull("Dialog was not displayed", chooserIntent); + assertEquals(Intent.ACTION_CHOOSER, chooserIntent.getAction()); + assertEquals(dialogTitle, chooserIntent.getExtras().get(Intent.EXTRA_TITLE)); + + final Intent contentIntent = (Intent)chooserIntent.getExtras().get(Intent.EXTRA_INTENT); + assertNotNull("Intent was not built correctly", contentIntent); + assertEquals(Intent.ACTION_SEND, contentIntent.getAction()); + assertEquals(title, contentIntent.getExtras().get(Intent.EXTRA_SUBJECT)); + assertEquals(message, contentIntent.getExtras().get(Intent.EXTRA_TEXT)); + + assertEquals(1, promise.getResolved()); + } + + @Test + public void testInvalidContent() { + final String dialogTitle = "Dialog Title"; + + final SimplePromise promise = new SimplePromise(); + + mShareModule.share(null, dialogTitle, promise); + + assertEquals(1, promise.getRejected()); + assertEquals(ShareModule.ERROR_INVALID_CONTENT, promise.getErrorCode()); + } + final static class SimplePromise implements Promise { private static final String DEFAULT_ERROR = "EUNSPECIFIED"; @@ -103,58 +176,4 @@ public void reject(String code, String message, @Nullable Throwable e) { } } - @Before - public void setUp() throws Exception { - mActivity = Robolectric.setupActivity(Activity.class); - ReactApplicationContext context = PowerMockito.mock(ReactApplicationContext.class); - PowerMockito.when(context, "getCurrentActivity").thenReturn(mActivity); - mShareModule = new ShareModule(context); - } - - @After - public void tearDown() { - mActivity = null; - mShareModule = null; - } - - @Test - public void testShareDialog() { - final String title = "Title"; - final String message = "Message"; - final String dialogTitle = "Dialog Title"; - - JavaOnlyMap content = new JavaOnlyMap(); - content.putString("title", title); - content.putString("message", message); - - final SimplePromise promise = new SimplePromise(); - - mShareModule.share(content, dialogTitle, promise); - - final Intent chooserIntent = ((ShadowActivity)ShadowExtractor.extract(mActivity)).getNextStartedActivity(); - assertNotNull("Dialog was not displayed", chooserIntent); - assertEquals(Intent.ACTION_CHOOSER, chooserIntent.getAction()); - assertEquals(dialogTitle, chooserIntent.getExtras().get(Intent.EXTRA_TITLE)); - - final Intent contentIntent = (Intent)chooserIntent.getExtras().get(Intent.EXTRA_INTENT); - assertNotNull("Intent was not built correctly", contentIntent); - assertEquals(Intent.ACTION_SEND, contentIntent.getAction()); - assertEquals(title, contentIntent.getExtras().get(Intent.EXTRA_SUBJECT)); - assertEquals(message, contentIntent.getExtras().get(Intent.EXTRA_TEXT)); - - assertEquals(1, promise.getResolved()); - } - - @Test - public void testInvalidContent() { - final String dialogTitle = "Dialog Title"; - - final SimplePromise promise = new SimplePromise(); - - mShareModule.share(null, dialogTitle, promise); - - assertEquals(1, promise.getRejected()); - assertEquals(ShareModule.ERROR_INVALID_CONTENT, promise.getErrorCode()); - } - } From dc1c57cb790e4cd8ddbb4dc6192415c74f252e04 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Mon, 25 Jul 2016 18:18:50 +0900 Subject: [PATCH 36/37] do not log the error if we're already rejecting --- Libraries/Share/Share.js | 11 +++++------ .../com/facebook/react/modules/share/ShareModule.java | 10 ++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index 1ac5ffc626713b..ef3804b417bc0f 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -26,12 +26,12 @@ class Share { /** * Open a dialog to share text content. - * + * * In iOS, Returns a Promise which will be invoked an object containing `action`, `activityType`. - * If the user dismissed the dialog, the Promise will still be resolved with action being `Share.dismissedAction` - * and all the other keys being undefined. - * - * In Android, Returns a Promise which always be resolved with action being `Share.sharedAction`. + * If the user dismissed the dialog, the Promise will still be resolved with action being `Share.dismissedAction` + * and all the other keys being undefined. + * + * In Android, Returns a Promise which always be resolved with action being `Share.sharedAction`. * * ### Content * @@ -93,7 +93,6 @@ class Share { ); }); } else { - console.warn('Share.share is not supported on this platform'); return Promise.reject(new Error('Unsupported platform')); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java index 6d69b2033ed2e5..f916cdb5660137 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -13,7 +13,6 @@ import android.content.Intent; import android.net.Uri; -import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.Promise; @@ -46,7 +45,7 @@ public String getName() { * Open a chooser dialog to send text content to other apps. * * Refer http://developer.android.com/intl/ko/training/sharing/send.html - * + * * @param content the data to send * @param dialogTitle the title of the chooser dialog */ @@ -60,14 +59,14 @@ public void share(ReadableMap content, String dialogTitle, Promise promise) { try { Intent intent = new Intent(Intent.ACTION_SEND); intent.setTypeAndNormalize("text/plain"); - + if (content.hasKey("title")) { intent.putExtra(Intent.EXTRA_SUBJECT, content.getString("title")); } if (content.hasKey("message")) { intent.putExtra(Intent.EXTRA_TEXT, content.getString("message")); - } + } if (content.hasKey("url")) { intent.putExtra(Intent.EXTRA_TEXT, content.getString("url")); // this will overwrite message @@ -85,9 +84,8 @@ public void share(ReadableMap content, String dialogTitle, Promise promise) { WritableMap result = Arguments.createMap(); result.putString("action", ACTION_SHARED); promise.resolve(result); - + } catch (Exception e) { - FLog.e(ReactConstants.TAG, "Failed to open share dialog", e); promise.reject(ERROR_UNABLE_TO_OPEN_DIALOG, "Failed to open share dialog"); } From 94023f99ae302eeadb099f1aa218bad19668f583 Mon Sep 17 00:00:00 2001 From: SangYeob Bono Yu Date: Mon, 25 Jul 2016 18:23:37 +0900 Subject: [PATCH 37/37] `url` in `content` prop is iOS only now --- Libraries/Share/Share.js | 5 ++++- .../java/com/facebook/react/modules/share/ShareModule.java | 4 ---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Libraries/Share/Share.js b/Libraries/Share/Share.js index ef3804b417bc0f..22839a07967c41 100644 --- a/Libraries/Share/Share.js +++ b/Libraries/Share/Share.js @@ -36,9 +36,12 @@ class Share { * ### Content * * - `message` - a message to share - * - `url` - an URL to share. In Android, this will overwrite message * - `title` - title of the message * + * #### iOS + * + * - `url` - an URL to share + * * At least one of URL and message is required. * * ### Options diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java index f916cdb5660137..eee2a42ca8f5c7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/share/ShareModule.java @@ -68,10 +68,6 @@ public void share(ReadableMap content, String dialogTitle, Promise promise) { intent.putExtra(Intent.EXTRA_TEXT, content.getString("message")); } - if (content.hasKey("url")) { - intent.putExtra(Intent.EXTRA_TEXT, content.getString("url")); // this will overwrite message - } - Intent chooser = Intent.createChooser(intent, dialogTitle); chooser.addCategory(Intent.CATEGORY_DEFAULT);