diff --git a/README.md b/README.md index b7c6b9ef..837f1403 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,14 @@ This is a helper module which brings react native as an engine to drive share ex

+# Features +- You can share within your app: + - a list of images, + - text + - url + - messages (from whatsapp for instance, we get either the text or the image) +- Return an array like `[{type, value}]` + # Installation Installation should be very easy by just installing it from npm. @@ -150,15 +158,21 @@ RCT_EXPORT_MODULE(); # Set the NSExtensionActivationRule key in your Info.plist -For the time being, this package only handles sharing of urls specifically from browsers. In order to tell the system to show your extension only when sharing a url, you must set the `NSExtensionActivationRule` key (under `NSExtensionAttributes`) in the share extension's Info.plist file as follows (this is also needed to pass Apple's reveiw): +For the time being, this package handles sharing of urls, text or images. In order to tell the system to show your extension only when type is supported, you must set the `NSExtensionActivationRule` key (under `NSExtensionAttributes`) in the share extension's Info.plist file as follows (this is also needed to pass Apple's review): ``` NSExtensionAttributes NSExtensionActivationRule - NSExtensionActivationSupportsWebURLWithMaxCount - 1 + NSExtensionActivationSupportsImageWithMaxCount + 2 + NSExtensionActivationSupportsMovieWithMaxCount + 0 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 ``` @@ -287,7 +301,7 @@ public class MainApplication extends Application implements ReactApplication { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override - protected boolean getUseDeveloperSupport() { + public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } @@ -321,11 +335,16 @@ public class MainApplication extends Application implements ReactApplication { android:theme="@style/Theme.Share.Transparent" > + // for sharing links include - // for sharing photos include - + // for sharing photos include + + // for sharing videos include + + // for sharing audio include + ``` diff --git a/android/build.gradle b/android/build.gradle index 0ec7bf23..fe8a69e2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,3 +1,7 @@ +def safeExtGet(prop, fallback) { + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback +} + buildscript { repositories { jcenter() @@ -11,12 +15,12 @@ buildscript { apply plugin: 'com.android.library' android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" + compileSdkVersion safeExtGet('compileSdkVersion', 23) + buildToolsVersion safeExtGet('buildToolsVersion', "23.0.1") defaultConfig { - minSdkVersion 16 - targetSdkVersion 22 + minSdkVersion safeExtGet('minSdkVersion', 16) + targetSdkVersion safeExtGet('targetSdkVersion', 22) versionCode 1 versionName "1.0" } diff --git a/android/src/main/java/com/alinz/parkerdan/shareextension/RealPathUtil.java b/android/src/main/java/com/alinz/parkerdan/shareextension/RealPathUtil.java index 5f6015fe..0957f839 100644 --- a/android/src/main/java/com/alinz/parkerdan/shareextension/RealPathUtil.java +++ b/android/src/main/java/com/alinz/parkerdan/shareextension/RealPathUtil.java @@ -20,7 +20,6 @@ public class RealPathUtil { public static String getRealPathFromURI(final Context context, final Uri uri) { final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - // DocumentProvider if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider @@ -105,6 +104,8 @@ public static String getDataColumn(Context context, Uri uri, String selection, final int index = cursor.getColumnIndexOrThrow(column); return cursor.getString(index); } + } catch (Exception e) { + return null; } finally { if (cursor != null) cursor.close(); @@ -138,18 +139,20 @@ public static boolean isMediaDocument(Uri uri) { } public static String getImagePath(Context context, Uri uri){ - if ("content".equalsIgnoreCase(uri.getScheme())) { - - if (isGoogleOldPhotosUri(uri)) { - // return http path, then download file. - return uri.getLastPathSegment(); - } else if (isGoogleNewPhotosUri(uri) || isMMSFile(uri)) { - // copy from uri. context.getContentResolver().openInputStream(uri); - return copyFile(context, uri); - } - } - - return getDataColumn(context, uri, null, null); + if (isGoogleOldPhotosUri(uri)) { + // return http path, then download file. + return uri.getLastPathSegment(); + } else if (isGoogleNewPhotosUri(uri) || isMMSFile(uri) || isWhatsappFile(uri)) { + // copy from uri. context.getContentResolver().openInputStream(uri); + return copyFile(context, uri); + } + final String dataColumn; + dataColumn = getDataColumn(context, uri, null, null); + if (dataColumn != null){ + return dataColumn; + } else { + return copyFile(context, uri); + } } /** @@ -165,7 +168,12 @@ public static boolean isGoogleNewPhotosUri(Uri uri) { } public static boolean isMMSFile(Uri uri) { - return "com.android.mms.file".equals(uri.getAuthority()); + // uri.getAuthority can be equal to "com.android.mms.file" and "mms" + return uri.getAuthority().contains("mms"); +} + + public static boolean isWhatsappFile(Uri uri) { + return "com.whatsapp.provider.media".equals(uri.getAuthority()); } private static String copyFile(Context context, Uri uri) { diff --git a/android/src/main/java/com/alinz/parkerdan/shareextension/ShareModule.java b/android/src/main/java/com/alinz/parkerdan/shareextension/ShareModule.java index e1aea2ce..a27483a0 100644 --- a/android/src/main/java/com/alinz/parkerdan/shareextension/ShareModule.java +++ b/android/src/main/java/com/alinz/parkerdan/shareextension/ShareModule.java @@ -5,72 +5,85 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.Arguments; import android.app.Activity; import android.content.Intent; import android.net.Uri; - import android.graphics.Bitmap; -import java.io.InputStream; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; +import java.util.ArrayList; public class ShareModule extends ReactContextBaseJavaModule { - public ShareModule(ReactApplicationContext reactContext) { - super(reactContext); - } - - @Override - public String getName() { - return "ReactNativeShareExtension"; - } - - @ReactMethod - public void close() { - getCurrentActivity().finish(); - } - - @ReactMethod - public void data(Promise promise) { - promise.resolve(processIntent()); - } - - public WritableMap processIntent() { - WritableMap map = Arguments.createMap(); - - String value = ""; - String type = ""; - String action = ""; - - Activity currentActivity = getCurrentActivity(); - - if (currentActivity != null) { - Intent intent = currentActivity.getIntent(); - action = intent.getAction(); - type = intent.getType(); - if (type == null) { - type = ""; - } - if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) { - value = intent.getStringExtra(Intent.EXTRA_TEXT); + public ShareModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "ReactNativeShareExtension"; + } + + @ReactMethod + public void close() { + getCurrentActivity().finish(); + } + + @ReactMethod + public void data(Promise promise) { + promise.resolve(processIntent()); + } + + public WritableArray processIntent() { + WritableArray dataArrayMap = Arguments.createArray(); + Set mediaTypesSupported = new HashSet(); + mediaTypesSupported.add("video"); + mediaTypesSupported.add("audio"); + mediaTypesSupported.add("image"); + + String type = ""; + String action = ""; + String typePart = ""; + + Activity currentActivity = getCurrentActivity(); + + if (currentActivity != null) { + Intent intent = currentActivity.getIntent(); + action = intent.getAction(); + type = intent.getType(); + if (type == null) { + type = ""; + } else { + typePart = type.substring(0, type.indexOf('/')); + } + if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) { + WritableMap dataMap = Arguments.createMap(); + dataMap.putString("type", type); + dataMap.putString("value", intent.getStringExtra(Intent.EXTRA_TEXT)); + dataArrayMap.pushMap(dataMap); + } else if (Intent.ACTION_SEND.equals(action) && ( + mediaTypesSupported.contains(typePart))) { + WritableMap dataMap = Arguments.createMap(); + dataMap.putString("type", type); + Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); + dataMap.putString("value", "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri)); + dataArrayMap.pushMap(dataMap); + } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && mediaTypesSupported.contains(typePart)) { + ArrayList uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + for (Uri uri : uris) { + WritableMap dataMap = Arguments.createMap(); + dataMap.putString("type", type); + dataMap.putString("value", "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri)); + dataArrayMap.pushMap(dataMap); + } + } } - else if (Intent.ACTION_SEND.equals(action) && ("image/*".equals(type) || "image/jpeg".equals(type) || "image/png".equals(type) || "image/jpg".equals(type) ) ) { - Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); - value = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri); - - } else { - value = ""; - } - } else { - value = ""; - type = ""; - } - - map.putString("type", type); - map.putString("value",value); - - return map; - } + return dataArrayMap; + } } diff --git a/android/src/main/java/com/alinz/parkerdan/shareextension/SharePackage.java b/android/src/main/java/com/alinz/parkerdan/shareextension/SharePackage.java index 3e168353..b23a6dfa 100644 --- a/android/src/main/java/com/alinz/parkerdan/shareextension/SharePackage.java +++ b/android/src/main/java/com/alinz/parkerdan/shareextension/SharePackage.java @@ -16,10 +16,6 @@ public List createNativeModules(ReactApplicationContext reactConte return Arrays.asList(new ShareModule(reactContext)); } - public List> createJSModules() { - return Collections.emptyList(); - } - @Override public List createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); diff --git a/android/src/main/java/com/github/alinz/reactNativeShareExtension/ShareExPackage.java b/android/src/main/java/com/github/alinz/reactNativeShareExtension/ShareExPackage.java index f21656c3..485c87c5 100644 --- a/android/src/main/java/com/github/alinz/reactNativeShareExtension/ShareExPackage.java +++ b/android/src/main/java/com/github/alinz/reactNativeShareExtension/ShareExPackage.java @@ -17,10 +17,6 @@ public List createNativeModules(ReactApplicationContext reactConte return Arrays.asList(new ShareExModule(reactContext)); } - public List> createJSModules() { - return Collections.emptyList(); - } - @Override public List createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); diff --git a/examples/Sample1/android/app/src/main/AndroidManifest.xml b/examples/Sample1/android/app/src/main/AndroidManifest.xml index b20054a8..2eaa119e 100644 --- a/examples/Sample1/android/app/src/main/AndroidManifest.xml +++ b/examples/Sample1/android/app/src/main/AndroidManifest.xml @@ -34,8 +34,16 @@ android:theme="@style/Theme.Share.Transparent" > + - + // for sharing links include + + // for sharing photos include + + // for sharing videos include + + // for sharing audio include + diff --git a/examples/Sample1/android/app/src/main/java/com/sample1/MainApplication.java b/examples/Sample1/android/app/src/main/java/com/sample1/MainApplication.java index 19ffaa34..2df8d0e6 100644 --- a/examples/Sample1/android/app/src/main/java/com/sample1/MainApplication.java +++ b/examples/Sample1/android/app/src/main/java/com/sample1/MainApplication.java @@ -19,7 +19,7 @@ public class MainApplication extends Application implements ReactApplication { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override - protected boolean getUseDeveloperSupport() { + public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } diff --git a/examples/Sample1/share.ios.js b/examples/Sample1/share.ios.js deleted file mode 100644 index 5a2acf14..00000000 --- a/examples/Sample1/share.ios.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Sample React Native Share Extension - * @flow - */ - -import React, { Component } from 'react' -import Modal from 'react-native-modalbox' -import ShareExtension from 'react-native-share-extension' - -import { - Text, - View, - TouchableOpacity -} from 'react-native' - -export default class Share extends Component { - constructor(props, context) { - super(props, context) - this.state = { - isOpen: true, - type: '', - value: '' - } - } - - async componentDidMount() { - try { - const { type, value } = await ShareExtension.data() - this.setState({ - type, - value - }) - } catch(e) { - console.log('errrr', e) - } - } - - onClose = () => ShareExtension.close() - - closing = () => this.setState({ isOpen: false }); - - render() { - return ( - - - - - Close - type: { this.state.type } - value: { this.state.value } - - - - - ); - } -} diff --git a/examples/Sample1/share.android.js b/examples/Sample1/share.js similarity index 74% rename from examples/Sample1/share.android.js rename to examples/Sample1/share.js index 54803f0b..bd233bfe 100644 --- a/examples/Sample1/share.android.js +++ b/examples/Sample1/share.js @@ -5,12 +5,13 @@ import React, { Component } from 'react' import Modal from 'react-native-modalbox' -import ShareExtension from 'react-native-share-extension' +import ShareExtension from './react-native-share-extension' import { Text, TextInput, View, + Image, TouchableOpacity } from 'react-native' @@ -19,17 +20,15 @@ export default class Share extends Component { super(props, context) this.state = { isOpen: true, - type: '', - value: '' + data: [], } } async componentDidMount() { try { - const { type, value } = await ShareExtension.data() + const data = await ShareExtension.data(); this.setState({ - type, - value + data }) } catch(e) { console.log('errrr', e) @@ -45,8 +44,17 @@ export default class Share extends Component { isOpen: false }) } + renderData(data, index) { + return ( + + type: { data.type } + value: { data.value } + + ); + } render() { + const dataComponent = this.state.data.map(this.renderData); return ( @@ -54,8 +62,7 @@ export default class Share extends Component { Close - type: { this.state.type } - value: { this.state.value } + {dataComponent} diff --git a/ios/ReactNativeShareExtension.m b/ios/ReactNativeShareExtension.m index f6a18ce5..5310edb0 100644 --- a/ios/ReactNativeShareExtension.m +++ b/ios/ReactNativeShareExtension.m @@ -55,99 +55,105 @@ - (void)viewDidLoad { resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - [self extractDataFromContext: extensionContext withCallback:^(NSString* val, NSString* contentType, NSException* err) { + [self extractDataFromContext: extensionContext withCallback:^(NSArray* items, NSException* err) { if(err) { reject(@"error", err.description, nil); } else { - resolve(@{ - @"type": contentType, - @"value": val - }); + resolve(items); } }]; } -- (void)extractDataFromContext:(NSExtensionContext *)context withCallback:(void(^)(NSString *value, NSString* contentType, NSException *exception))callback { - +- (void)extractDataFromContext:(NSExtensionContext *)context withCallback:(void(^)(NSArray *items, NSException *exception))callback { @try { + __block NSMutableArray *itemArray = [NSMutableArray new]; NSExtensionItem *item = [context.inputItems firstObject]; + NSArray *attachments = item.attachments; __block NSItemProvider *urlProvider = nil; __block NSItemProvider *imageProvider = nil; __block NSItemProvider *textProvider = nil; + __block NSUInteger index = 0; [attachments enumerateObjectsUsingBlock:^(NSItemProvider *provider, NSUInteger idx, BOOL *stop) { - if([provider hasItemConformingToTypeIdentifier:URL_IDENTIFIER]) { + if ([provider hasItemConformingToTypeIdentifier:IMAGE_IDENTIFIER]){ + imageProvider = provider; + [imageProvider loadItemForTypeIdentifier:IMAGE_IDENTIFIER options:nil completionHandler:^(id item, NSError *error) { + /** + * Save the image to NSTemporaryDirectory(), which cleans itself tri-daily. + * This is necessary as the iOS 11 screenshot editor gives us a UIImage, while + * sharing from Photos and similar apps gives us a URL + * Therefore the solution is to save a UIImage, either way, and return the local path to that temp UIImage + * This path will be sent to React Native and can be processed and accessed RN side. + **/ + + UIImage *sharedImage; + NSString *filename; + + if ([(NSObject *)item isKindOfClass:[UIImage class]]){ + sharedImage = (UIImage *)item; + NSString *name = @"RNSE_TEMP_IMG_"; + NSString *nbFiles = [NSString stringWithFormat:@"%@", @(index)]; + NSString *fullname = [name stringByAppendingString:(nbFiles)]; + filename = [fullname stringByAppendingPathExtension:@"png"]; + }else if ([(NSObject *)item isKindOfClass:[NSURL class]]){ + NSURL* url = (NSURL *)item; + filename = [[url lastPathComponent] lowercaseString]; + NSData *data = [NSData dataWithContentsOfURL:url]; + sharedImage = [UIImage imageWithData:data]; + } + NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; + + [UIImageJPEGRepresentation(sharedImage, 1.0) writeToFile:filePath atomically:YES]; + index += 1; + + [itemArray addObject: @{ + @"type": [filePath pathExtension], + @"value": filePath + }]; + if (callback && (index == [attachments count])) { + callback(itemArray, nil); + } + + }]; + } else if([provider hasItemConformingToTypeIdentifier:URL_IDENTIFIER]) { urlProvider = provider; - *stop = YES; + index += 1; + [urlProvider loadItemForTypeIdentifier:URL_IDENTIFIER options:nil completionHandler:^(id item, NSError *error) { + NSURL *url = (NSURL *)item; + [itemArray addObject: @{ + @"type": @"text/plain", + @"value": [url absoluteString] + }]; + if (callback && (index == [attachments count])) { + callback(itemArray, nil); + } + }]; } else if ([provider hasItemConformingToTypeIdentifier:TEXT_IDENTIFIER]){ textProvider = provider; - *stop = YES; - } else if ([provider hasItemConformingToTypeIdentifier:IMAGE_IDENTIFIER]){ - imageProvider = provider; - *stop = YES; + [textProvider loadItemForTypeIdentifier:TEXT_IDENTIFIER options:nil completionHandler:^(id item, NSError *error) { + NSString *text = (NSString *)item; + index += 1; + [itemArray addObject: @{ + @"type": @"text/plain", + @"value": text + }]; + if (callback && (index == [attachments count])) { + callback(itemArray, nil); + } + }]; + } else { + index += 1; } }]; - - if(urlProvider) { - [urlProvider loadItemForTypeIdentifier:URL_IDENTIFIER options:nil completionHandler:^(id item, NSError *error) { - NSURL *url = (NSURL *)item; - - if(callback) { - callback([url absoluteString], @"text/plain", nil); - } - }]; - } else if (imageProvider) { - [imageProvider loadItemForTypeIdentifier:IMAGE_IDENTIFIER options:nil completionHandler:^(id item, NSError *error) { - - /** - * Save the image to NSTemporaryDirectory(), which cleans itself tri-daily. - * This is necessary as the iOS 11 screenshot editor gives us a UIImage, while - * sharing from Photos and similar apps gives us a URL - * Therefore the solution is to save a UIImage, either way, and return the local path to that temp UIImage - * This path will be sent to React Native and can be processed and accessed RN side. - **/ - - UIImage *sharedImage; - NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"RNSE_TEMP_IMG"]; - NSString *fullPath = [filePath stringByAppendingPathExtension:@"png"]; - - if ([(NSObject *)item isKindOfClass:[UIImage class]]){ - sharedImage = (UIImage *)item; - }else if ([(NSObject *)item isKindOfClass:[NSURL class]]){ - NSURL* url = (NSURL *)item; - NSData *data = [NSData dataWithContentsOfURL:url]; - sharedImage = [UIImage imageWithData:data]; - } - - [UIImagePNGRepresentation(sharedImage) writeToFile:fullPath atomically:YES]; - - if(callback) { - callback(fullPath, [fullPath pathExtension], nil); - } - }]; - } else if (textProvider) { - [textProvider loadItemForTypeIdentifier:TEXT_IDENTIFIER options:nil completionHandler:^(id item, NSError *error) { - NSString *text = (NSString *)item; - - if(callback) { - callback(text, @"text/plain", nil); - } - }]; - } else { - if(callback) { - callback(nil, nil, [NSException exceptionWithName:@"Error" reason:@"couldn't find provider" userInfo:nil]); - } - } + // } } @catch (NSException *exception) { if(callback) { - callback(nil, nil, exception); + callback(nil, exception); } } } - - @end