diff options
| author | Rene Ae <aehtyb@gmail.com> | 2015-12-01 00:51:02 -0600 |
|---|---|---|
| committer | Rene Ae <aehtyb@gmail.com> | 2015-12-01 00:51:02 -0600 |
| commit | 6415f506034262dd7151be2e35e1e1c1e97f4dfa (patch) | |
| tree | c6e564e374967725a40fd87f7c5f3a1ba8019089 /StoneIsland/plugins/cordova-plugin-x-socialsharing/src | |
| parent | 5e07e273e18a609978253c45f3c5f702b0de4991 (diff) | |
| parent | 9497b50fa02f3cfa9cb263ce3a96fa725282d60d (diff) | |
Merge branch 'master' of https://github.com/okfocus/stone-island
Diffstat (limited to 'StoneIsland/plugins/cordova-plugin-x-socialsharing/src')
5 files changed, 1492 insertions, 0 deletions
diff --git a/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/android/nl/xservices/plugins/SocialSharing.java b/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/android/nl/xservices/plugins/SocialSharing.java new file mode 100644 index 00000000..f1168f89 --- /dev/null +++ b/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/android/nl/xservices/plugins/SocialSharing.java @@ -0,0 +1,533 @@ +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_SENDVIAEMAIL = 2; + + 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)) { + 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), 1); + } + } + } + }); + 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._-]", ""); + 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 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) { + final String pattern = ".*/([^?#]+)?"; + Pattern r = Pattern.compile(pattern); + Matcher m = r.matcher(url); + if (m.find()) { + return m.group(1); + } else { + return null; + } + } + + 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("[:\\\\/*?|<> ]", "_"); + } +} diff --git a/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/ios/SocialSharing.h b/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/ios/SocialSharing.h new file mode 100755 index 00000000..b51474d3 --- /dev/null +++ b/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/ios/SocialSharing.h @@ -0,0 +1,27 @@ +#import <Cordova/CDV.h> +#import <MessageUI/MFMailComposeViewController.h> + +@interface SocialSharing : CDVPlugin <UIPopoverControllerDelegate, MFMailComposeViewControllerDelegate, UIDocumentInteractionControllerDelegate> + +@property (nonatomic, strong) MFMailComposeViewController *globalMailComposer; +@property (retain) 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 diff --git a/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/ios/SocialSharing.m b/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/ios/SocialSharing.m new file mode 100755 index 00000000..cd0913a4 --- /dev/null +++ b/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/ios/SocialSharing.m @@ -0,0 +1,715 @@ +#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]]; + } + + 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]]; + } + + [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]; + + // 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]; + } + } + NSString * encodedShareString = [shareString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + // also encode the '=' character + encodedShareString = [encodedShareString stringByReplacingOccurrencesOfString:@"=" withString:@"%3D"]; + encodedShareString = [encodedShareString stringByReplacingOccurrencesOfString:@"&" withString:@"%26"]; + NSString * encodedShareStringForWhatsApp = [NSString stringWithFormat:@"whatsapp://send?text=%@", 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 diff --git a/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/windows/SocialSharingProxy.js b/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/windows/SocialSharingProxy.js new file mode 100644 index 00000000..99e0af15 --- /dev/null +++ b/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/windows/SocialSharingProxy.js @@ -0,0 +1,114 @@ +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 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; + for (var i = 0; i < fileOrFileArray.length; i++) { + Windows.Storage.StorageFile.getFileFromPathAsync(fileOrFileArray[i]).done( + function (file) { + storageItems.push(file); + if (!--filesCount) { + e.request.data.setStorageItems(storageItems); + deferral.complete(); + } + }, + 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); + } + }, + + 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);
\ No newline at end of file diff --git a/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/wp8/SocialSharing.cs b/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/wp8/SocialSharing.cs new file mode 100644 index 00000000..1a165127 --- /dev/null +++ b/StoneIsland/plugins/cordova-plugin-x-socialsharing/src/wp8/SocialSharing.cs @@ -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; } + } + +}
\ No newline at end of file |
