Sharebutton vollständig implementiert.

This commit is contained in:
2016-02-03 16:15:27 +01:00
parent 4fdd18bb11
commit 4439b7320c
53 changed files with 3831 additions and 129 deletions

View File

@@ -0,0 +1,602 @@
package nl.xservices.plugins;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.*;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.Html;
import android.util.Base64;
import android.view.Gravity;
import android.widget.Toast;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SocialSharing extends CordovaPlugin {
private static final String ACTION_AVAILABLE_EVENT = "available";
private static final String ACTION_SHARE_EVENT = "share";
private static final String ACTION_CAN_SHARE_VIA = "canShareVia";
private static final String ACTION_CAN_SHARE_VIA_EMAIL = "canShareViaEmail";
private static final String ACTION_SHARE_VIA = "shareVia";
private static final String ACTION_SHARE_VIA_TWITTER_EVENT = "shareViaTwitter";
private static final String ACTION_SHARE_VIA_FACEBOOK_EVENT = "shareViaFacebook";
private static final String ACTION_SHARE_VIA_FACEBOOK_WITH_PASTEMESSAGEHINT = "shareViaFacebookWithPasteMessageHint";
private static final String ACTION_SHARE_VIA_WHATSAPP_EVENT = "shareViaWhatsApp";
private static final String ACTION_SHARE_VIA_INSTAGRAM_EVENT = "shareViaInstagram";
private static final String ACTION_SHARE_VIA_SMS_EVENT = "shareViaSMS";
private static final String ACTION_SHARE_VIA_EMAIL_EVENT = "shareViaEmail";
private static final int ACTIVITY_CODE_SEND = 1;
private static final int ACTIVITY_CODE_SENDVIAEMAIL = 2;
private static final int ACTIVITY_CODE_SENDVIAWHATSAPP = 3;
private CallbackContext _callbackContext;
private String pasteMessage;
private abstract class SocialSharingRunnable implements Runnable {
public CallbackContext callbackContext;
SocialSharingRunnable(CallbackContext cb) {
this.callbackContext = cb;
}
}
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
this._callbackContext = callbackContext; // only used for onActivityResult
this.pasteMessage = null;
if (ACTION_AVAILABLE_EVENT.equals(action)) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
return true;
} else if (ACTION_SHARE_EVENT.equals(action)) {
return doSendIntent(callbackContext, args.getString(0), args.getString(1), args.getJSONArray(2), args.getString(3), null, false);
} else if (ACTION_SHARE_VIA_TWITTER_EVENT.equals(action)) {
return doSendIntent(callbackContext, args.getString(0), args.getString(1), args.getJSONArray(2), args.getString(3), "twitter", false);
} else if (ACTION_SHARE_VIA_FACEBOOK_EVENT.equals(action)) {
return doSendIntent(callbackContext, args.getString(0), args.getString(1), args.getJSONArray(2), args.getString(3), "com.facebook.katana", false);
} else if (ACTION_SHARE_VIA_FACEBOOK_WITH_PASTEMESSAGEHINT.equals(action)) {
this.pasteMessage = args.getString(4);
return doSendIntent(callbackContext, args.getString(0), args.getString(1), args.getJSONArray(2), args.getString(3), "com.facebook.katana", false);
} else if (ACTION_SHARE_VIA_WHATSAPP_EVENT.equals(action)) {
if (notEmpty(args.getString(4))) {
return shareViaWhatsAppDirectly(callbackContext, args.getString(0), args.getString(1), args.getJSONArray(2), args.getString(3), args.getString(4));
} else {
return doSendIntent(callbackContext, args.getString(0), args.getString(1), args.getJSONArray(2), args.getString(3), "whatsapp", false);
}
} else if (ACTION_SHARE_VIA_INSTAGRAM_EVENT.equals(action)) {
if (notEmpty(args.getString(0))) {
copyHintToClipboard(args.getString(0), "Instagram paste message");
}
return doSendIntent(callbackContext, args.getString(0), args.getString(1), args.getJSONArray(2), args.getString(3), "instagram", false);
} else if (ACTION_CAN_SHARE_VIA.equals(action)) {
return doSendIntent(callbackContext, args.getString(0), args.getString(1), args.getJSONArray(2), args.getString(3), args.getString(4), true);
} else if (ACTION_CAN_SHARE_VIA_EMAIL.equals(action)) {
if (isEmailAvailable()) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
return true;
} else {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, "not available"));
return false;
}
} else if (ACTION_SHARE_VIA.equals(action)) {
return doSendIntent(callbackContext, args.getString(0), args.getString(1), args.getJSONArray(2), args.getString(3), args.getString(4), false);
} else if (ACTION_SHARE_VIA_SMS_EVENT.equals(action)) {
return invokeSMSIntent(callbackContext, args.getJSONObject(0), args.getString(1));
} else if (ACTION_SHARE_VIA_EMAIL_EVENT.equals(action)) {
return invokeEmailIntent(callbackContext, args.getString(0), args.getString(1), args.getJSONArray(2), args.isNull(3) ? null : args.getJSONArray(3), args.isNull(4) ? null : args.getJSONArray(4), args.isNull(5) ? null : args.getJSONArray(5));
} else {
callbackContext.error("socialSharing." + action + " is not a supported function. Did you mean '" + ACTION_SHARE_EVENT + "'?");
return false;
}
}
private boolean isEmailAvailable() {
final Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", "someone@domain.com", null));
return cordova.getActivity().getPackageManager().queryIntentActivities(intent, 0).size() > 0;
}
private boolean invokeEmailIntent(final CallbackContext callbackContext, final String message, final String subject, final JSONArray to, final JSONArray cc, final JSONArray bcc, final JSONArray files) throws JSONException {
final SocialSharing plugin = this;
cordova.getThreadPool().execute(new SocialSharingRunnable(callbackContext) {
public void run() {
final Intent draft = new Intent(Intent.ACTION_SEND_MULTIPLE);
if (notEmpty(message)) {
Pattern htmlPattern = Pattern.compile(".*\\<[^>]+>.*", Pattern.DOTALL);
if (htmlPattern.matcher(message).matches()) {
draft.putExtra(android.content.Intent.EXTRA_TEXT, Html.fromHtml(message));
draft.setType("text/html");
} else {
draft.putExtra(android.content.Intent.EXTRA_TEXT, message);
draft.setType("text/plain");
}
}
if (notEmpty(subject)) {
draft.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
}
try {
if (to != null && to.length() > 0) {
draft.putExtra(android.content.Intent.EXTRA_EMAIL, toStringArray(to));
}
if (cc != null && cc.length() > 0) {
draft.putExtra(android.content.Intent.EXTRA_CC, toStringArray(cc));
}
if (bcc != null && bcc.length() > 0) {
draft.putExtra(android.content.Intent.EXTRA_BCC, toStringArray(bcc));
}
if (files.length() > 0) {
final String dir = getDownloadDir();
if (dir != null) {
ArrayList<Uri> fileUris = new ArrayList<Uri>();
for (int i = 0; i < files.length(); i++) {
final Uri fileUri = getFileUriAndSetType(draft, dir, files.getString(i), subject, i);
if (fileUri != null) {
fileUris.add(fileUri);
}
}
if (!fileUris.isEmpty()) {
draft.putExtra(Intent.EXTRA_STREAM, fileUris);
}
}
}
} catch (Exception e) {
callbackContext.error(e.getMessage());
}
draft.setType("application/octet-stream");
cordova.startActivityForResult(plugin, Intent.createChooser(draft, "Choose Email App"), ACTIVITY_CODE_SENDVIAEMAIL);
}
});
return true;
}
private String getDownloadDir() throws IOException {
// better check, otherwise it may crash the app
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
// we need to use external storage since we need to share to another app
final String dir = webView.getContext().getExternalFilesDir(null) + "/socialsharing-downloads";
createOrCleanDir(dir);
return dir;
} else {
return null;
}
}
private boolean doSendIntent(final CallbackContext callbackContext, final String msg, final String subject, final JSONArray files, final String url, final String appPackageName, final boolean peek) {
final CordovaInterface mycordova = cordova;
final CordovaPlugin plugin = this;
cordova.getThreadPool().execute(new SocialSharingRunnable(callbackContext) {
public void run() {
String message = msg;
final boolean hasMultipleAttachments = files.length() > 1;
final Intent sendIntent = new Intent(hasMultipleAttachments ? Intent.ACTION_SEND_MULTIPLE : Intent.ACTION_SEND);
sendIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
try {
if (files.length() > 0 && !"".equals(files.getString(0))) {
final String dir = getDownloadDir();
if (dir != null) {
ArrayList<Uri> fileUris = new ArrayList<Uri>();
Uri fileUri = null;
for (int i = 0; i < files.length(); i++) {
fileUri = getFileUriAndSetType(sendIntent, dir, files.getString(i), subject, i);
if (fileUri != null) {
fileUris.add(fileUri);
}
}
if (!fileUris.isEmpty()) {
if (hasMultipleAttachments) {
sendIntent.putExtra(Intent.EXTRA_STREAM, fileUris);
} else {
sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
}
}
} else {
sendIntent.setType("text/plain");
}
} else {
sendIntent.setType("text/plain");
}
} catch (Exception e) {
callbackContext.error(e.getMessage());
}
if (notEmpty(subject)) {
sendIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
}
// add the URL to the message, as there seems to be no separate field
if (notEmpty(url)) {
if (notEmpty(message)) {
message += " " + url;
} else {
message = url;
}
}
if (notEmpty(message)) {
sendIntent.putExtra(android.content.Intent.EXTRA_TEXT, message);
// sometimes required when the user picks share via sms
if (Build.VERSION.SDK_INT < 21) { // LOLLIPOP
sendIntent.putExtra("sms_body", message);
}
}
if (appPackageName != null) {
String packageName = appPackageName;
String passedActivityName = null;
if (packageName.contains("/")) {
String[] items = appPackageName.split("/");
packageName = items[0];
passedActivityName = items[1];
}
final ActivityInfo activity = getActivity(callbackContext, sendIntent, packageName);
if (activity != null) {
if (peek) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
} else {
sendIntent.addCategory(Intent.CATEGORY_LAUNCHER);
sendIntent.setComponent(new ComponentName(activity.applicationInfo.packageName,
passedActivityName != null ? passedActivityName : activity.name));
mycordova.startActivityForResult(plugin, sendIntent, 0);
if (pasteMessage != null) {
// add a little delay because target app (facebook only atm) needs to be started first
new Timer().schedule(new TimerTask() {
public void run() {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
copyHintToClipboard(msg, pasteMessage);
showPasteMessage(pasteMessage);
}
});
}
}, 2000);
}
}
}
} else {
if (peek) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
} else {
mycordova.startActivityForResult(plugin, Intent.createChooser(sendIntent, null), ACTIVITY_CODE_SEND);
}
}
}
});
return true;
}
@SuppressLint("NewApi")
private void copyHintToClipboard(String msg, String label) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return;
}
final ClipboardManager clipboard = (android.content.ClipboardManager) cordova.getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
final ClipData clip = android.content.ClipData.newPlainText(label, msg);
clipboard.setPrimaryClip(clip);
}
@SuppressLint("NewApi")
private void showPasteMessage(String label) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return;
}
final Toast toast = Toast.makeText(webView.getContext(), label, Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL, 0, 0);
toast.show();
}
private Uri getFileUriAndSetType(Intent sendIntent, String dir, String image, String subject, int nthFile) throws IOException {
// we're assuming an image, but this can be any filetype you like
String localImage = image;
sendIntent.setType("image/*");
if (image.startsWith("http") || image.startsWith("www/")) {
String filename = getFileName(image);
localImage = "file://" + dir + "/" + filename;
if (image.startsWith("http")) {
// filename optimisation taken from https://github.com/EddyVerbruggen/SocialSharing-PhoneGap-Plugin/pull/56
URLConnection connection = new URL(image).openConnection();
String disposition = connection.getHeaderField("Content-Disposition");
if (disposition != null) {
final Pattern dispositionPattern = Pattern.compile("filename=([^;]+)");
Matcher matcher = dispositionPattern.matcher(disposition);
if (matcher.find()) {
filename = matcher.group(1).replaceAll("[^a-zA-Z0-9._-]", "");
if (filename.length() == 0) {
// in this case we can't determine a filetype so some targets (gmail) may not render it correctly
filename = "file";
}
localImage = "file://" + dir + "/" + filename;
}
}
saveFile(getBytes(connection.getInputStream()), dir, filename);
} else {
saveFile(getBytes(webView.getContext().getAssets().open(image)), dir, filename);
}
} else if (image.startsWith("data:")) {
// safeguard for https://code.google.com/p/android/issues/detail?id=7901#c43
if (!image.contains(";base64,")) {
sendIntent.setType("text/plain");
return null;
}
// image looks like this: data:image/png;base64,R0lGODlhDAA...
final String encodedImg = image.substring(image.indexOf(";base64,") + 8);
// correct the intent type if anything else was passed, like a pdf: data:application/pdf;base64,..
if (!image.contains("data:image/")) {
sendIntent.setType(image.substring(image.indexOf("data:") + 5, image.indexOf(";base64")));
}
// the filename needs a valid extension, so it renders correctly in target apps
final String imgExtension = image.substring(image.indexOf("/") + 1, image.indexOf(";base64"));
String fileName;
// if a subject was passed, use it as the filename
// filenames must be unique when passing in multiple files [#158]
if (notEmpty(subject)) {
fileName = sanitizeFilename(subject) + (nthFile == 0 ? "" : "_" + nthFile) + "." + imgExtension;
} else {
fileName = "file" + (nthFile == 0 ? "" : "_" + nthFile) + "." + imgExtension;
}
saveFile(Base64.decode(encodedImg, Base64.DEFAULT), dir, fileName);
localImage = "file://" + dir + "/" + fileName;
} else if (image.startsWith("df:")) {
// safeguard for https://code.google.com/p/android/issues/detail?id=7901#c43
if (!image.contains(";base64,")) {
sendIntent.setType("text/plain");
return null;
}
// format looks like this : df:filename.txt;data:image/png;base64,R0lGODlhDAA...
final String fileName = image.substring(image.indexOf("df:") + 3, image.indexOf(";data:"));
final String fileType = image.substring(image.indexOf(";data:") + 6, image.indexOf(";base64,"));
final String encodedImg = image.substring(image.indexOf(";base64,") + 8);
sendIntent.setType(fileType);
saveFile(Base64.decode(encodedImg, Base64.DEFAULT), dir, sanitizeFilename(fileName));
localImage = "file://" + dir + "/" + fileName;
} else if (!image.startsWith("file://")) {
throw new IllegalArgumentException("URL_NOT_SUPPORTED");
}
return Uri.parse(localImage);
}
private boolean shareViaWhatsAppDirectly(final CallbackContext callbackContext, String message, final String subject, final JSONArray files, final String url, final String number) {
// add the URL to the message, as there seems to be no separate field
if (notEmpty(url)) {
if (notEmpty(message)) {
message += " " + url;
} else {
message = url;
}
}
final String shareMessage = message;
final SocialSharing plugin = this;
cordova.getThreadPool().execute(new SocialSharingRunnable(callbackContext) {
public void run() {
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("smsto:" + number));
intent.putExtra("sms_body", shareMessage);
intent.putExtra("sms_subject", subject);
intent.setPackage("com.whatsapp");
try {
if (files.length() > 0 && !"".equals(files.getString(0))) {
final boolean hasMultipleAttachments = files.length() > 1;
final String dir = getDownloadDir();
if (dir != null) {
ArrayList<Uri> fileUris = new ArrayList<Uri>();
Uri fileUri = null;
for (int i = 0; i < files.length(); i++) {
fileUri = getFileUriAndSetType(intent, dir, files.getString(i), subject, i);
if (fileUri != null) {
fileUris.add(fileUri);
}
}
if (!fileUris.isEmpty()) {
if (hasMultipleAttachments) {
intent.putExtra(Intent.EXTRA_STREAM, fileUris);
} else {
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
}
}
}
}
} catch (Exception e) {
callbackContext.error(e.getMessage());
}
try {
cordova.startActivityForResult(plugin, intent, ACTIVITY_CODE_SENDVIAWHATSAPP);
} catch (Exception e) {
callbackContext.error(e.getMessage());
}
}
});
return true;
}
private boolean invokeSMSIntent(final CallbackContext callbackContext, JSONObject options, String p_phonenumbers) {
final String message = options.optString("message");
// TODO test this on a real SMS enabled device before releasing it
// final String subject = options.optString("subject");
// final String image = options.optString("image");
final String subject = null; //options.optString("subject");
final String image = null; // options.optString("image");
final String phonenumbers = getPhoneNumbersWithManufacturerSpecificSeparators(p_phonenumbers);
final SocialSharing plugin = this;
cordova.getThreadPool().execute(new SocialSharingRunnable(callbackContext) {
public void run() {
Intent intent;
if (Build.VERSION.SDK_INT >= 19) { // Build.VERSION_CODES.KITKAT) {
// passing in no phonenumbers for kitkat may result in an error,
// but it may also work for some devices, so documentation will need to cover this case
intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("smsto:" + (notEmpty(phonenumbers) ? phonenumbers : "")));
} else {
intent = new Intent(Intent.ACTION_VIEW);
intent.setType("vnd.android-dir/mms-sms");
if (phonenumbers != null) {
intent.putExtra("address", phonenumbers);
}
}
intent.putExtra("sms_body", message);
intent.putExtra("sms_subject", subject);
try {
if (image != null && !"".equals(image)) {
final Uri fileUri = getFileUriAndSetType(intent, getDownloadDir(), image, subject, 0);
if (fileUri != null) {
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
}
}
cordova.startActivityForResult(plugin, intent, 0);
} catch (Exception e) {
callbackContext.error(e.getMessage());
}
}
});
return true;
}
private static String getPhoneNumbersWithManufacturerSpecificSeparators(String phonenumbers) {
if (notEmpty(phonenumbers)) {
char separator;
if (android.os.Build.MANUFACTURER.equalsIgnoreCase("samsung")) {
separator = ',';
} else {
separator = ';';
}
return phonenumbers.replace(';', separator).replace(',', separator);
}
return null;
}
private ActivityInfo getActivity(final CallbackContext callbackContext, final Intent shareIntent, final String appPackageName) {
final PackageManager pm = webView.getContext().getPackageManager();
List<ResolveInfo> activityList = pm.queryIntentActivities(shareIntent, 0);
for (final ResolveInfo app : activityList) {
if ((app.activityInfo.packageName).contains(appPackageName)) {
return app.activityInfo;
}
}
// no matching app found
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, getShareActivities(activityList)));
return null;
}
private JSONArray getShareActivities(List<ResolveInfo> activityList) {
List<String> packages = new ArrayList<String>();
for (final ResolveInfo app : activityList) {
packages.add(app.activityInfo.packageName);
}
return new JSONArray(packages);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
if (_callbackContext != null) {
if (ACTIVITY_CODE_SENDVIAEMAIL == requestCode) {
_callbackContext.success();
} else {
_callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, resultCode == Activity.RESULT_OK));
}
}
}
private void createOrCleanDir(final String downloadDir) throws IOException {
final File dir = new File(downloadDir);
if (!dir.exists()) {
if (!dir.mkdirs()) {
throw new IOException("CREATE_DIRS_FAILED");
}
} else {
cleanupOldFiles(dir);
}
}
private static String getFileName(String url) {
if (url.endsWith("/")) {
url = url.substring(0, url.length()-1);
}
final String pattern = ".*/([^?#]+)?";
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(url);
if (m.find()) {
return m.group(1);
} else {
return "file";
}
}
private byte[] getBytes(InputStream is) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[16384];
while ((nRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
return buffer.toByteArray();
}
private void saveFile(byte[] bytes, String dirName, String fileName) throws IOException {
final File dir = new File(dirName);
final FileOutputStream fos = new FileOutputStream(new File(dir, fileName));
fos.write(bytes);
fos.flush();
fos.close();
}
/**
* As file.deleteOnExit does not work on Android, we need to delete files manually.
* Deleting them in onActivityResult is not a good idea, because for example a base64 encoded file
* will not be available for upload to Facebook (it's deleted before it's uploaded).
* So the best approach is deleting old files when saving (sharing) a new one.
*/
private void cleanupOldFiles(File dir) {
for (File f : dir.listFiles()) {
//noinspection ResultOfMethodCallIgnored
f.delete();
}
}
private static boolean notEmpty(String what) {
return what != null &&
!"".equals(what) &&
!"null".equalsIgnoreCase(what);
}
private static String[] toStringArray(JSONArray jsonArray) throws JSONException {
String[] result = new String[jsonArray.length()];
for (int i = 0; i < jsonArray.length(); i++) {
result[i] = jsonArray.getString(i);
}
return result;
}
public static String sanitizeFilename(String name) {
return name.replaceAll("[:\\\\/*?|<> ]", "_");
}
}

View File

@@ -0,0 +1,27 @@
#import <Cordova/CDV.h>
#import <MessageUI/MFMailComposeViewController.h>
@interface SocialSharing : CDVPlugin <UIPopoverControllerDelegate, MFMailComposeViewControllerDelegate, UIDocumentInteractionControllerDelegate>
@property (nonatomic, strong) MFMailComposeViewController *globalMailComposer;
@property (nonatomic, strong) UIDocumentInteractionController * documentInteractionController;
@property (retain) NSString * tempStoredFile;
@property (retain) CDVInvokedUrlCommand * command;
- (void)available:(CDVInvokedUrlCommand*)command;
- (void)setIPadPopupCoordinates:(CDVInvokedUrlCommand*)command;
- (void)share:(CDVInvokedUrlCommand*)command;
- (void)canShareVia:(CDVInvokedUrlCommand*)command;
- (void)canShareViaEmail:(CDVInvokedUrlCommand*)command;
- (void)shareVia:(CDVInvokedUrlCommand*)command;
- (void)shareViaTwitter:(CDVInvokedUrlCommand*)command;
- (void)shareViaFacebook:(CDVInvokedUrlCommand*)command;
- (void)shareViaFacebookWithPasteMessageHint:(CDVInvokedUrlCommand*)command;
- (void)shareViaWhatsApp:(CDVInvokedUrlCommand*)command;
- (void)shareViaSMS:(CDVInvokedUrlCommand*)command;
- (void)shareViaEmail:(CDVInvokedUrlCommand*)command;
- (void)shareViaInstagram:(CDVInvokedUrlCommand*)command;
- (void)saveToPhotoAlbum:(CDVInvokedUrlCommand*)command;
@end

View File

@@ -0,0 +1,720 @@
#import "SocialSharing.h"
#import <Cordova/CDV.h>
#import <Social/Social.h>
#import <Foundation/NSException.h>
#import <MessageUI/MFMessageComposeViewController.h>
#import <MessageUI/MFMailComposeViewController.h>
#import <MobileCoreServices/MobileCoreServices.h>
@implementation SocialSharing {
UIPopoverController *_popover;
NSString *_popupCoordinates;
}
- (void)pluginInitialize {
if ([self isEmailAvailable]) {
[self cycleTheGlobalMailComposer];
}
}
- (void)available:(CDVInvokedUrlCommand*)command {
BOOL avail = NO;
if (NSClassFromString(@"UIActivityViewController")) {
avail = YES;
}
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:avail];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
- (NSString*)getIPadPopupCoordinates {
if (_popupCoordinates != nil) {
return _popupCoordinates;
}
if ([self.webView respondsToSelector:@selector(stringByEvaluatingJavaScriptFromString:)]) {
return [(UIWebView*)self.webView stringByEvaluatingJavaScriptFromString:@"window.plugins.socialsharing.iPadPopupCoordinates();"];
} else {
// prolly a wkwebview, ignoring for now
return nil;
}
}
- (void)setIPadPopupCoordinates:(CDVInvokedUrlCommand*)command {
_popupCoordinates = [command.arguments objectAtIndex:0];
}
- (CGRect)getPopupRectFromIPadPopupCoordinates:(NSArray*)comps {
CGRect rect = CGRectZero;
if ([comps count] == 4) {
rect = CGRectMake([[comps objectAtIndex:0] integerValue], [[comps objectAtIndex:1] integerValue], [[comps objectAtIndex:2] integerValue], [[comps objectAtIndex:3] integerValue]);
}
return rect;
}
- (void)share:(CDVInvokedUrlCommand*)command {
[self.commandDelegate runInBackground:^{ //avoid main thread block especially if sharing big files from url
if (!NSClassFromString(@"UIActivityViewController")) {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"not available"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
return;
}
NSString *message = [command.arguments objectAtIndex:0];
NSString *subject = [command.arguments objectAtIndex:1];
NSArray *filenames = [command.arguments objectAtIndex:2];
NSString *urlString = [command.arguments objectAtIndex:3];
NSMutableArray *activityItems = [[NSMutableArray alloc] init];
[activityItems addObject:message];
NSMutableArray *files = [[NSMutableArray alloc] init];
if (filenames != (id)[NSNull null] && filenames.count > 0) {
for (NSString* filename in filenames) {
NSObject *file = [self getImage:filename];
if (file == nil) {
file = [self getFile:filename];
}
if (file != nil) {
[files addObject:file];
}
}
[activityItems addObjectsFromArray:files];
}
if (urlString != (id)[NSNull null]) {
[activityItems addObject:[NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]]];
}
UIActivity *activity = [[UIActivity alloc] init];
NSArray *applicationActivities = [[NSArray alloc] initWithObjects:activity, nil];
UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:applicationActivities];
if (subject != (id)[NSNull null]) {
[activityVC setValue:subject forKey:@"subject"];
}
// TODO deprecated in iOS 8.0, change this some day
[activityVC setCompletionHandler:^(NSString *activityType, BOOL completed) {
[self cleanupStoredFiles];
NSLog(@"SocialSharing app selected: %@", activityType);
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:completed];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}];
NSArray * socialSharingExcludeActivities = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"SocialSharingExcludeActivities"];
if (socialSharingExcludeActivities!=nil && [socialSharingExcludeActivities count] > 0) {
activityVC.excludedActivityTypes = socialSharingExcludeActivities;
}
dispatch_async(dispatch_get_main_queue(), ^(void){
// iPad on iOS >= 8 needs a different approach
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
NSString* iPadCoords = [self getIPadPopupCoordinates];
if (iPadCoords != nil && ![iPadCoords isEqual:@"-1,-1,-1,-1"]) {
NSArray *comps = [iPadCoords componentsSeparatedByString:@","];
CGRect rect = [self getPopupRectFromIPadPopupCoordinates:comps];
if ([activityVC respondsToSelector:@selector(popoverPresentationController)]) {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 // iOS 8.0 supported
activityVC.popoverPresentationController.sourceView = self.webView;
activityVC.popoverPresentationController.sourceRect = rect;
#endif
} else {
_popover = [[UIPopoverController alloc] initWithContentViewController:activityVC];
_popover.delegate = self;
[_popover presentPopoverFromRect:rect inView:self.webView permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
} else if ([activityVC respondsToSelector:@selector(popoverPresentationController)]) {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 // iOS 8.0 supported
activityVC.popoverPresentationController.sourceView = self.webView;
// position the popup at the bottom, just like iOS < 8 did (and iPhone still does on iOS 8)
NSArray *comps = [NSArray arrayWithObjects:
[NSNumber numberWithInt:(self.viewController.view.frame.size.width/2)-200],
[NSNumber numberWithInt:self.viewController.view.frame.size.height],
[NSNumber numberWithInt:400],
[NSNumber numberWithInt:400],
nil];
CGRect rect = [self getPopupRectFromIPadPopupCoordinates:comps];
activityVC.popoverPresentationController.sourceRect = rect;
#endif
}
}
[[self getTopMostViewController] presentViewController:activityVC animated:YES completion:nil];
});
}];
}
- (void)shareViaTwitter:(CDVInvokedUrlCommand*)command {
[self shareViaInternal:command type:SLServiceTypeTwitter];
}
- (void)shareViaFacebook:(CDVInvokedUrlCommand*)command {
[self shareViaInternal:command type:SLServiceTypeFacebook];
}
- (void)shareViaFacebookWithPasteMessageHint:(CDVInvokedUrlCommand*)command {
// If Fb app is installed a message is not prefilled.
// When shared through the default iOS widget (iOS Settings > Facebook) the message is prefilled already.
NSString *message = [command.arguments objectAtIndex:0];
if (message != (id)[NSNull null]) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1000 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
BOOL fbAppInstalled = [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"fb://"]]; // requires whitelisting on iOS9
if (fbAppInstalled) {
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
[pasteboard setValue:message forPasteboardType:@"public.text"];
NSString *hint = [command.arguments objectAtIndex:4];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:hint delegate:nil cancelButtonTitle:nil otherButtonTitles:nil];
[alert show];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2800 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
[alert dismissWithClickedButtonIndex:-1 animated:YES];
});
}
});
}
[self shareViaInternal:command type:SLServiceTypeFacebook];
}
- (void)shareVia:(CDVInvokedUrlCommand*)command {
[self shareViaInternal:command type:[command.arguments objectAtIndex:4]];
}
- (void)canShareVia:(CDVInvokedUrlCommand*)command {
NSString *via = [command.arguments objectAtIndex:4];
CDVPluginResult * pluginResult;
if ([@"sms" caseInsensitiveCompare:via] == NSOrderedSame && [self canShareViaSMS]) {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
} else if ([@"email" caseInsensitiveCompare:via] == NSOrderedSame && [self isEmailAvailable]) {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
} else if ([@"whatsapp" caseInsensitiveCompare:via] == NSOrderedSame && [self canShareViaWhatsApp]) {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
} else if ([@"instagram" caseInsensitiveCompare:via] == NSOrderedSame && [self canShareViaInstagram]) {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
} else if ([self isAvailableForSharing:command type:via]) {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
} else {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"not available"];
}
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
- (void)canShareViaEmail:(CDVInvokedUrlCommand*)command {
if ([self isEmailAvailable]) {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
} else {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"not available"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
}
- (bool)isEmailAvailable {
Class messageClass = (NSClassFromString(@"MFMailComposeViewController"));
return messageClass != nil && [messageClass canSendMail];
}
- (bool)isAvailableForSharing:(CDVInvokedUrlCommand*)command
type:(NSString *) type {
// isAvailableForServiceType returns true if you pass it a type that is not
// in the defined constants, this is probably a bug on apples part
if(!([type isEqualToString:SLServiceTypeFacebook]
|| [type isEqualToString:SLServiceTypeTwitter]
|| [type isEqualToString:SLServiceTypeTencentWeibo]
|| [type isEqualToString:SLServiceTypeSinaWeibo])) {
return false;
}
// wrapped in try-catch, because isAvailableForServiceType may crash if an invalid type is passed
@try {
return [SLComposeViewController isAvailableForServiceType:type];
}
@catch (NSException* exception) {
return false;
}
}
- (void)shareViaInternal:(CDVInvokedUrlCommand*)command
type:(NSString *) type {
NSString *message = [command.arguments objectAtIndex:0];
// subject is not supported by the SLComposeViewController
NSArray *filenames = [command.arguments objectAtIndex:2];
NSString *urlString = [command.arguments objectAtIndex:3];
// boldly invoke the target app, because the phone will display a nice message asking to configure the app
SLComposeViewController *composeViewController = [SLComposeViewController composeViewControllerForServiceType:type];
if (message != (id)[NSNull null]) {
[composeViewController setInitialText:message];
}
for (NSString* filename in filenames) {
UIImage* image = [self getImage:filename];
if (image != nil) {
[composeViewController addImage:image];
}
}
if (urlString != (id)[NSNull null]) {
[composeViewController addURL:[NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]]];
}
[composeViewController setCompletionHandler:^(SLComposeViewControllerResult result) {
if (SLComposeViewControllerResultCancelled == result) {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"cancelled"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
} else if ([self isAvailableForSharing:command type:type]) {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:SLComposeViewControllerResultDone == result];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
} else {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"not available"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
// required for iOS6 (issues #162 and #167)
[self.viewController dismissViewControllerAnimated:YES completion:nil];
}];
[[self getTopMostViewController] presentViewController:composeViewController animated:YES completion:nil];
}
- (void)shareViaEmail:(CDVInvokedUrlCommand*)command {
if ([self isEmailAvailable]) {
if (TARGET_IPHONE_SIMULATOR && IsAtLeastiOSVersion(@"8.0")) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"SocialSharing plugin"
message:@"Sharing via email is not supported on the iOS 8 simulator."
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
return;
}
self.globalMailComposer.mailComposeDelegate = self;
if ([command.arguments objectAtIndex:0] != (id)[NSNull null]) {
NSString *message = [command.arguments objectAtIndex:0];
BOOL isHTML = [message rangeOfString:@"<[^>]+>" options:NSRegularExpressionSearch].location != NSNotFound;
[self.globalMailComposer setMessageBody:message isHTML:isHTML];
}
if ([command.arguments objectAtIndex:1] != (id)[NSNull null]) {
[self.globalMailComposer setSubject: [command.arguments objectAtIndex:1]];
}
if ([command.arguments objectAtIndex:2] != (id)[NSNull null]) {
[self.globalMailComposer setToRecipients:[command.arguments objectAtIndex:2]];
}
if ([command.arguments objectAtIndex:3] != (id)[NSNull null]) {
[self.globalMailComposer setCcRecipients:[command.arguments objectAtIndex:3]];
}
if ([command.arguments objectAtIndex:4] != (id)[NSNull null]) {
[self.globalMailComposer setBccRecipients:[command.arguments objectAtIndex:4]];
}
if ([command.arguments objectAtIndex:5] != (id)[NSNull null]) {
NSArray* attachments = [command.arguments objectAtIndex:5];
NSFileManager* fileManager = [NSFileManager defaultManager];
for (NSString* path in attachments) {
NSURL *file = [self getFile:path];
NSData* data = [fileManager contentsAtPath:file.path];
NSString* fileName;
NSString* mimeType;
NSString* basename = [self getBasenameFromAttachmentPath:path];
if ([basename hasPrefix:@"data:"]) {
mimeType = (NSString*)[[[basename substringFromIndex:5] componentsSeparatedByString: @";"] objectAtIndex:0];
fileName = @"attachment.";
fileName = [fileName stringByAppendingString:(NSString*)[[mimeType componentsSeparatedByString: @"/"] lastObject]];
NSString *base64content = (NSString*)[[basename componentsSeparatedByString: @","] lastObject];
data = [SocialSharing dataFromBase64String:base64content];
} else {
fileName = [basename pathComponents].lastObject;
mimeType = [self getMimeTypeFromFileExtension:[basename pathExtension]];
}
[self.globalMailComposer addAttachmentData:data mimeType:mimeType fileName:fileName];
}
}
// remember the command, because we need it in the didFinishWithResult method
_command = command;
[self.commandDelegate runInBackground:^{
[[self getTopMostViewController] presentViewController:self.globalMailComposer animated:YES completion:nil];
}];
} else {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"not available"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
}
- (UIViewController*) getTopMostViewController {
UIViewController *presentingViewController = [[[UIApplication sharedApplication] delegate] window].rootViewController;
while (presentingViewController.presentedViewController != nil) {
presentingViewController = presentingViewController.presentedViewController;
}
return presentingViewController;
}
- (NSString*) getBasenameFromAttachmentPath:(NSString*)path {
if ([path hasPrefix:@"base64:"]) {
NSString* pathWithoutPrefix = [path stringByReplacingOccurrencesOfString:@"base64:" withString:@""];
return [pathWithoutPrefix substringToIndex:[pathWithoutPrefix rangeOfString:@"//"].location];
}
return path;
}
- (NSString*) getMimeTypeFromFileExtension:(NSString*)extension {
if (!extension) {
return nil;
}
// Get the UTI from the file's extension
CFStringRef ext = (CFStringRef)CFBridgingRetain(extension);
CFStringRef type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext, NULL);
// Converting UTI to a mime type
NSString *result = (NSString*)CFBridgingRelease(UTTypeCopyPreferredTagWithClass(type, kUTTagClassMIMEType));
CFRelease(ext);
CFRelease(type);
return result;
}
/**
* Delegate will be called after the mail composer did finish an action
* to dismiss the view.
*/
- (void) mailComposeController:(MFMailComposeViewController*)controller
didFinishWithResult:(MFMailComposeResult)result
error:(NSError*)error {
bool ok = result == MFMailComposeResultSent;
[self.globalMailComposer dismissViewControllerAnimated:YES completion:^{[self cycleTheGlobalMailComposer];}];
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:ok];
[self.commandDelegate sendPluginResult:pluginResult callbackId:_command.callbackId];
}
-(void)cycleTheGlobalMailComposer {
// we are cycling the damned GlobalMailComposer: http://stackoverflow.com/questions/25604552/i-have-real-misunderstanding-with-mfmailcomposeviewcontroller-in-swift-ios8-in/25604976#25604976
self.globalMailComposer = nil;
self.globalMailComposer = [[MFMailComposeViewController alloc] init];
}
- (bool)canShareViaSMS {
Class messageClass = (NSClassFromString(@"MFMessageComposeViewController"));
return messageClass != nil && [messageClass canSendText];
}
- (void)shareViaSMS:(CDVInvokedUrlCommand*)command {
if ([self canShareViaSMS]) {
NSDictionary* options = [command.arguments objectAtIndex:0];
NSString *phonenumbers = [command.arguments objectAtIndex:1];
NSString *message = [options objectForKey:@"message"];
NSString *subject = [options objectForKey:@"subject"];
NSString *image = [options objectForKey:@"image"];
MFMessageComposeViewController *picker = [[MFMessageComposeViewController alloc] init];
picker.messageComposeDelegate = (id) self;
if (message != (id)[NSNull null]) {
picker.body = message;
}
if (subject != (id)[NSNull null]) {
[picker setSubject:subject];
}
if (image != nil && image != (id)[NSNull null]) {
BOOL canSendAttachments = [[MFMessageComposeViewController class] respondsToSelector:@selector(canSendAttachments)];
if (canSendAttachments) {
NSURL *file = [self getFile:image];
if (file != nil) {
[picker addAttachmentURL:file withAlternateFilename:nil];
}
}
}
if (phonenumbers != (id)[NSNull null]) {
[picker setRecipients:[phonenumbers componentsSeparatedByString:@","]];
}
// remember the command, because we need it in the didFinishWithResult method
_command = command;
[self.commandDelegate runInBackground:^{
picker.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[[self getTopMostViewController] presentViewController:picker animated:YES completion:nil];
}];
} else {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"not available"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
}
// Dismisses the SMS composition interface when users taps Cancel or Send
- (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result {
bool ok = result == MessageComposeResultSent;
[[self getTopMostViewController] dismissViewControllerAnimated:YES completion:nil];
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:ok];
[self.commandDelegate sendPluginResult:pluginResult callbackId:_command.callbackId];
}
- (bool)canShareViaInstagram {
return [[UIApplication sharedApplication] canOpenURL: [NSURL URLWithString:@"instagram://app"]]; // requires whitelisting on iOS9
}
- (bool)canShareViaWhatsApp {
return [[UIApplication sharedApplication] canOpenURL: [NSURL URLWithString:@"whatsapp://app"]]; // requires whitelisting on iOS9
}
// this is only an internal test method for now, can be used to open a share sheet with 'Open in xx' links for tumblr, drive, dropbox, ..
- (void)openImage:(NSString *)imageName {
UIImage* image =[self getImage:imageName];
if (image != nil) {
NSString * savePath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/myTempImage.jpg"];
[UIImageJPEGRepresentation(image, 1.0) writeToFile:savePath atomically:YES];
_documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:savePath]];
_documentInteractionController.UTI = @""; // TODO find the scheme for google drive and create a shareViaGoogleDrive function
[_documentInteractionController presentOpenInMenuFromRect:CGRectZero inView:self.viewController.view animated: YES];
}
}
- (void)shareViaInstagram:(CDVInvokedUrlCommand*)command {
// on iOS9 canShareVia('instagram'..) will only work if instagram:// is whitelisted.
// If it's not, this method will ask permission to the user on iOS9 for opening the app,
// which is of course better than Instagram sharing not working at all because you forgot to whitelist it.
// Tradeoff: on iOS9 this method will always return true, so make sure to whitelist it and call canShareVia('instagram'..)
if (!IsAtLeastiOSVersion(@"9.0")) {
if (![self canShareViaInstagram]) {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"not available"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
return;
}
}
NSString *message = [command.arguments objectAtIndex:0];
// subject is not supported by the SLComposeViewController
NSArray *filenames = [command.arguments objectAtIndex:2];
// only use the first image (for now.. maybe we can share in a loop?)
UIImage* image = nil;
for (NSString* filename in filenames) {
image = [self getImage:filename];
break;
}
// NSData *imageObj = [NSData dataFromBase64String:objectAtIndex0];
NSString *tmpDir = NSTemporaryDirectory();
NSString *path = [tmpDir stringByAppendingPathComponent:@"instagram.igo"];
[UIImageJPEGRepresentation(image, 1.0) writeToFile:path atomically:YES];
_documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:path]];
_documentInteractionController.delegate = self;
_documentInteractionController.UTI = @"com.instagram.exclusivegram";
if (message != (id)[NSNull null]) {
// no longer working, so ..
_documentInteractionController.annotation = @{@"InstagramCaption" : message};
// .. we put the message on the clipboard (you app can prompt the user to paste it)
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
[pasteboard setValue:message forPasteboardType:@"public.text"];
}
// remember the command for the delegate method
_command = command;
[_documentInteractionController presentOpenInMenuFromRect:CGRectZero inView:self.webView animated:YES];
}
- (void)shareViaWhatsApp:(CDVInvokedUrlCommand*)command {
// on iOS9 canShareVia('whatsapp'..) will only work if whatsapp:// is whitelisted.
// If it's not, this method will ask permission to the user on iOS9 for opening the app,
// which is of course better than WhatsApp sharing not working at all because you forgot to whitelist it.
// Tradeoff: on iOS9 this method will always return true, so make sure to whitelist it and call canShareVia('whatsapp'..)
if (!IsAtLeastiOSVersion(@"9.0")) {
if (![self canShareViaWhatsApp]) {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"not available"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
return;
}
}
NSString *message = [command.arguments objectAtIndex:0];
// subject is not supported by the SLComposeViewController
NSArray *filenames = [command.arguments objectAtIndex:2];
NSString *urlString = [command.arguments objectAtIndex:3];
NSString *abid = [command.arguments objectAtIndex:4];
// only use the first image (for now.. maybe we can share in a loop?)
UIImage* image = nil;
for (NSString* filename in filenames) {
image = [self getImage:filename];
break;
}
// with WhatsApp, we can share an image OR text+url.. image wins if set
if (image != nil) {
NSString * savePath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/whatsAppTmp.wai"];
[UIImageJPEGRepresentation(image, 1.0) writeToFile:savePath atomically:YES];
_documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:savePath]];
_documentInteractionController.UTI = @"net.whatsapp.image";
_documentInteractionController.delegate = self;
_command = command;
[_documentInteractionController presentOpenInMenuFromRect:CGRectZero inView:self.viewController.view animated: YES];
} else {
// append an url to a message, if both are passed
NSString * shareString = @"";
if (message != (id)[NSNull null]) {
shareString = message;
}
if (urlString != (id)[NSNull null]) {
if ([shareString isEqual: @""]) {
shareString = urlString;
} else {
shareString = [NSString stringWithFormat:@"%@ %@", shareString, [urlString stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]];
}
}
NSString * encodedShareString = [shareString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// also encode the '=' character
encodedShareString = [encodedShareString stringByReplacingOccurrencesOfString:@"=" withString:@"%3D"];
encodedShareString = [encodedShareString stringByReplacingOccurrencesOfString:@"&" withString:@"%26"];
NSString * abidString = @"";
if (abid != (id)[NSNull null]) {
abidString = [NSString stringWithFormat:@"abid=%@&", abid];
}
NSString * encodedShareStringForWhatsApp = [NSString stringWithFormat:@"whatsapp://send?%@text=%@", abidString, encodedShareString];
NSURL *whatsappURL = [NSURL URLWithString:encodedShareStringForWhatsApp];
[[UIApplication sharedApplication] openURL: whatsappURL];
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
}
- (void)saveToPhotoAlbum:(CDVInvokedUrlCommand*)command {
self.command = command;
NSArray *filenames = [command.arguments objectAtIndex:0];
[self.commandDelegate runInBackground:^{
bool shared = false;
for (NSString* filename in filenames) {
UIImage* image = [self getImage:filename];
if (image != nil) {
shared = true;
UIImageWriteToSavedPhotosAlbum(image, self, @selector(thisImage:wasSavedToPhotoAlbumWithError:contextInfo:), nil);
}
}
if (!shared) {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no valid image was passed"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.command.callbackId];
}
}];
}
// called from saveToPhotoAlbum, note that we only send feedback for the first image that's being saved (not keeping the callback)
// but since the UIImageWriteToSavedPhotosAlbum function is only called with valid images that should not be a problem
- (void)thisImage:(UIImage *)image wasSavedToPhotoAlbumWithError:(NSError *)error contextInfo:(void*)ctxInfo {
if (error) {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:error.localizedDescription];
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.command.callbackId];
} else {
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.command.callbackId];
}
}
-(UIImage*)getImage: (NSString *)imageName {
UIImage *image = nil;
if (imageName != (id)[NSNull null]) {
if ([imageName hasPrefix:@"http"]) {
image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageName]]];
} else if ([imageName hasPrefix:@"www/"]) {
image = [UIImage imageNamed:imageName];
} else if ([imageName hasPrefix:@"file://"]) {
image = [UIImage imageWithData:[NSData dataWithContentsOfFile:[[NSURL URLWithString:imageName] path]]];
} else if ([imageName hasPrefix:@"data:"]) {
// using a base64 encoded string
NSURL *imageURL = [NSURL URLWithString:imageName];
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
image = [UIImage imageWithData:imageData];
} else if ([imageName hasPrefix:@"assets-library://"]) {
// use assets-library
NSURL *imageURL = [NSURL URLWithString:imageName];
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
image = [UIImage imageWithData:imageData];
} else {
// assume anywhere else, on the local filesystem
image = [UIImage imageWithData:[NSData dataWithContentsOfFile:imageName]];
}
}
return image;
}
-(NSURL*)getFile: (NSString *)fileName {
NSURL *file = nil;
if (fileName != (id)[NSNull null]) {
if ([fileName hasPrefix:@"http"]) {
NSURL *url = [NSURL URLWithString:fileName];
NSData *fileData = [NSData dataWithContentsOfURL:url];
file = [NSURL fileURLWithPath:[self storeInFile:(NSString*)[[fileName componentsSeparatedByString: @"/"] lastObject] fileData:fileData]];
} else if ([fileName hasPrefix:@"www/"]) {
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
NSString *fullPath = [NSString stringWithFormat:@"%@/%@", bundlePath, fileName];
file = [NSURL fileURLWithPath:fullPath];
} else if ([fileName hasPrefix:@"file://"]) {
// stripping the first 6 chars, because the path should start with / instead of file://
file = [NSURL fileURLWithPath:[fileName substringFromIndex:6]];
} else if ([fileName hasPrefix:@"data:"]) {
// using a base64 encoded string
// extract some info from the 'fileName', which is for example: data:text/calendar;base64,<encoded stuff here>
NSString *fileType = (NSString*)[[[fileName substringFromIndex:5] componentsSeparatedByString: @";"] objectAtIndex:0];
fileType = (NSString*)[[fileType componentsSeparatedByString: @"/"] lastObject];
NSString *base64content = (NSString*)[[fileName componentsSeparatedByString: @","] lastObject];
NSData *fileData = [SocialSharing dataFromBase64String:base64content];
file = [NSURL fileURLWithPath:[self storeInFile:[NSString stringWithFormat:@"%@.%@", @"file", fileType] fileData:fileData]];
} else {
// assume anywhere else, on the local filesystem
file = [NSURL fileURLWithPath:fileName];
}
}
return file;
}
-(NSString*) storeInFile: (NSString*) fileName
fileData: (NSData*) fileData {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
[fileData writeToFile:filePath atomically:YES];
_tempStoredFile = filePath;
return filePath;
}
- (void) cleanupStoredFiles {
if (_tempStoredFile != nil) {
NSError *error;
[[NSFileManager defaultManager]removeItemAtPath:_tempStoredFile error:&error];
}
}
+ (NSData*) dataFromBase64String:(NSString*)aString {
return [[NSData alloc] initWithBase64EncodedString:aString options:0];
}
#pragma mark - UIPopoverControllerDelegate methods
- (void)popoverController:(UIPopoverController *)popoverController willRepositionPopoverToRect:(inout CGRect *)rect inView:(inout UIView **)view {
NSArray *comps = [[self getIPadPopupCoordinates] componentsSeparatedByString:@","];
CGRect newRect = [self getPopupRectFromIPadPopupCoordinates:comps];
rect->origin = newRect.origin;
}
- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController {
_popover = nil;
}
#pragma mark - UIDocumentInteractionControllerDelegate methods
- (void) documentInteractionController: (UIDocumentInteractionController *) controller willBeginSendingToApplication: (NSString *) application {
// note that the application actually contains the app bundle id which was picked (for whatsapp and instagram only)
NSLog(@"SocialSharing app selected: %@", application);
}
- (void) documentInteractionControllerDidDismissOpenInMenu: (UIDocumentInteractionController *) controller {
if (self.command != nil) {
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId: self.command.callbackId];
}
}
@end

View File

@@ -0,0 +1,168 @@
var cordova = require('cordova');
module.exports = {
share: function (win, fail, args) {
//Text Message
var message = args[0];
//Title
var subject = args[1];
//File(s) Path
var fileOrFileArray = args[2];
//Web link
var url = args[3];
var folder = Windows.Storage.ApplicationData.current.temporaryFolder;
var getExtension = function (strBase64) {
return strBase64.substring(strBase64.indexOf("/") + 1, strBase64.indexOf(";base64"));
};
var replaceAll = function (str, find, replace) {
return str.replace(new RegExp(find, 'g'), replace);
};
var sanitizeFilename = function (name) {
return replaceAll(name, "[:\\\\/*?|<> ]", "_");
};
var getFileName = function (position, fileExtension) {
var fileName = (subject ? sanitizeFilename(subject) : "file") + (position == 0 ? "" : "_" + position) + "." + fileExtension;
return fileName;
};
var createTemporalFile = function (fileName, buffer) {
var filePath = "";
return folder.createFileAsync(fileName, Windows.Storage.CreationCollisionOption.replaceExisting).then(function (file) {
filePath = file.path;
return Windows.Storage.FileIO.writeBufferAsync(file, buffer);
}).then(function(){
return Windows.Storage.StorageFile.getFileFromPathAsync(filePath);
});
};
var doShare = function (e) {
e.request.data.properties.title = subject?subject: "Sharing";
if (message) e.request.data.setText(message);
if (url) e.request.data.setWebLink(new Windows.Foundation.Uri(url));
if (fileOrFileArray.length > 0) {
var deferral = e.request.getDeferral();
var storageItems = [];
var filesCount = fileOrFileArray.length;
var completeFile = function () {
if (!--filesCount) {
storageItems.length && e.request.data.setStorageItems(storageItems);
deferral.complete();
}
};
for (var i = 0; i < fileOrFileArray.length; i++) {
var file = fileOrFileArray[i];
if (file.indexOf("data:") >= 0) {
var fileName = getFileName(i, getExtension(file));
var buffer = Windows.Security.Cryptography.CryptographicBuffer.decodeFromBase64String(file.split(',')[1]);
if (buffer) {
createTemporalFile(fileName, buffer).done(
function (file) {
storageItems.push(file);
completeFile();
},
function () {
completeFile();
}
);
}
else {
completeFile();
}
}
else {
Windows.Storage.StorageFile.getFileFromPathAsync(file).done(
function (file) {
storageItems.push(file);
completeFile();
},
function () {
completeFile();
}
);
}
}
}
};
var dataTransferManager = Windows.ApplicationModel.DataTransfer.DataTransferManager.getForCurrentView();
dataTransferManager.addEventListener("datarequested", doShare);
try {
Windows.ApplicationModel.DataTransfer.DataTransferManager.showShareUI();
win(true);
} catch (err) {
fail(err);
}
},
canShareViaEmail: function (win, fail, args) {
win(true);
},
shareViaEmail: function (win, fail, args) {
//Text Message
var message = args[0];
//Title
var subject = args[1];
//File(s) Path
var fileOrFileArray = args[5];
var doShare = function (e) {
e.request.data.properties.title = subject ? subject : "Sharing";
if (message) {
var htmlFormat = Windows.ApplicationModel.DataTransfer.HtmlFormatHelper.createHtmlFormat(message);
e.request.data.setHtmlFormat(htmlFormat);
}
if (fileOrFileArray.length > 0) {
var deferral = e.request.getDeferral();
var storageItems = [];
var filesCount = fileOrFileArray.length;
for (var i = 0; i < fileOrFileArray.length; i++) {
Windows.Storage.StorageFile.getFileFromPathAsync(fileOrFileArray[i]).done(
function (index, file) {
var path = fileOrFileArray[index];
var streamRef = Windows.Storage.Streams.RandomAccessStreamReference.createFromFile(file);
e.request.data.resourceMap[path] = streamRef;
storageItems.push(file);
if (!--filesCount) {
e.request.data.setStorageItems(storageItems);
deferral.complete();
}
}.bind(this, i),
function () {
if (!--filesCount) {
e.request.data.setStorageItems(storageItems);
deferral.complete();
}
}
);
}
}
};
var dataTransferManager = Windows.ApplicationModel.DataTransfer.DataTransferManager.getForCurrentView();
dataTransferManager.addEventListener("datarequested", doShare);
try {
Windows.ApplicationModel.DataTransfer.DataTransferManager.showShareUI();
win(true);
} catch (err) {
fail(err);
}
}
};
require("cordova/exec/proxy").add("SocialSharing", module.exports);

View File

@@ -0,0 +1,103 @@
using Microsoft.Phone.Tasks;
using WPCordovaClassLib.Cordova;
using WPCordovaClassLib.Cordova.Commands;
using WPCordovaClassLib.Cordova.JSON;
using Newtonsoft.Json;
namespace Cordova.Extension.Commands
{
public class SocialSharing : BaseCommand
{
public void available(string jsonArgs)
{
DispatchCommandResult(new PluginResult(PluginResult.Status.OK));
}
public void share(string jsonArgs)
{
var options = JsonHelper.Deserialize<string[]>(jsonArgs);
var message = options[0];
var title = options[1];
var files = JsonHelper.Deserialize<string[]>(options[2]);
var link = options[3];
if (!"null".Equals(link))
{
ShareLinkTask shareLinkTask = new ShareLinkTask();
shareLinkTask.Title = title;
shareLinkTask.LinkUri = new System.Uri(link, System.UriKind.Absolute);
shareLinkTask.Message = message;
shareLinkTask.Show();
}
else if (files != null && files.Length > 0)
{
ShareLinkTask shareLinkTask = new ShareLinkTask();
shareLinkTask.Title = title;
shareLinkTask.LinkUri = new System.Uri(files[0], System.UriKind.Absolute);
shareLinkTask.Message = message;
shareLinkTask.Show();
}
else
{
var shareStatusTask = new ShareStatusTask { Status = message };
shareStatusTask.Show();
}
// unfortunately, there is no way to tell if something was shared, so just invoke the successCallback
DispatchCommandResult(new PluginResult(PluginResult.Status.OK));
}
public void canShareViaEmail(string jsonArgs)
{
DispatchCommandResult(new PluginResult(PluginResult.Status.OK));
}
// HTML and attachments are currently not supported on WP8
public void shareViaEmail(string jsonArgs)
{
var options = JsonHelper.Deserialize<string[]>(jsonArgs);
EmailComposeTask draft = new EmailComposeTask();
draft.Body = options[0];
draft.Subject = options[1];
if (!"null".Equals(options[2]))
{
draft.To = string.Join(",", options[2]);
}
if (!"null".Equals(options[3]))
{
draft.Cc = string.Join(",", options[3]);
}
if (!"null".Equals(options[4]))
{
draft.Bcc = string.Join(",", options[4]);
}
draft.Show();
DispatchCommandResult(new PluginResult(PluginResult.Status.OK, true));
}
public void shareViaSMS(string jsonArgs)
{
var options = JsonHelper.Deserialize<string[]>(jsonArgs);
SmsComposeTask smsComposeTask = new SmsComposeTask();
smsComposeTask.To = options[1];
SMSMessageClass m = JsonConvert.DeserializeObject<SMSMessageClass>(options[0]);
smsComposeTask.Body = m.message;
smsComposeTask.Show();
DispatchCommandResult(new PluginResult(PluginResult.Status.OK, true));
}
}
public class SMSMessageClass
{
public string message { get; set; }
}
}