package nl.xservices.plugins;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Intent;
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.text.Html;
import android.util.Base64;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.apache.http.util.ByteArrayBuffer;
import org.json.JSONArray;
import org.json.JSONException;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
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_WHATSAPP_EVENT = "shareViaWhatsApp";
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;
@Override
public boolean execute(String action, JSONArray args, CallbackContext pCallbackContext) throws JSONException {
this.callbackContext = pCallbackContext;
if (ACTION_AVAILABLE_EVENT.equals(action)) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
return true;
} else if (ACTION_SHARE_EVENT.equals(action)) {
return doSendIntent(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(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(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(args.getString(0), args.getString(1), args.getJSONArray(2), args.getString(3), "whatsapp", false);
} else if (ACTION_CAN_SHARE_VIA.equals(action)) {
return doSendIntent(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(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(args.getString(0), args.getString(1));
} else if (ACTION_SHARE_VIA_EMAIL_EVENT.equals(action)) {
return invokeEmailIntent(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() > 1;
}
private boolean invokeEmailIntent(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 Runnable() {
public void run() {
final Intent draft = new Intent(Intent.ACTION_SEND_MULTIPLE);
if (notEmpty(message)) {
if (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) {
ArrayList<Uri> fileUris = new ArrayList<Uri>();
final String dir = getDownloadDir();
for (int i = 0; i < files.length(); i++) {
final Uri fileUri = getFileUriAndSetType(draft, dir, files.getString(i), subject);
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 {
final String dir = webView.getContext().getExternalFilesDir(null) + "/socialsharing-downloads";
createOrCleanDir(dir);
return dir;
}
private boolean doSendIntent(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 Runnable() {
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);
if (files.length() > 0) {
ArrayList<Uri> fileUris = new ArrayList<Uri>();
try {
final String dir = getDownloadDir();
Uri fileUri = null;
for (int i = 0; i < files.length(); i++) {
fileUri = getFileUriAndSetType(sendIntent, dir, files.getString(i), subject);
if (fileUri != null) {
fileUris.add(fileUri);
}
}
if (!fileUris.isEmpty()) {
if (hasMultipleAttachments) {
sendIntent.putExtra(Intent.EXTRA_STREAM, fileUris);
} else {
sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
}
}
} catch (Exception e) {
callbackContext.error(e.getMessage());
}
} else {
sendIntent.setType("text/plain");
}
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);
}
if (appPackageName != null) {
final ActivityInfo activity = getActivity(sendIntent, appPackageName);
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, activity.name));
mycordova.startActivityForResult(plugin, sendIntent, 0);
}
}
} else {
if (peek) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
} else {
mycordova.startActivityForResult(plugin, Intent.createChooser(sendIntent, null), 1);
}
}
}
});
return true;
}
private Uri getFileUriAndSetType(Intent sendIntent, String dir, String image, String subject) 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 = "file." + imgExtension;
// if a subject was passed, use it as the filename
if (notEmpty(subject)) {
fileName = sanitizeFilename(subject) + "." + imgExtension;
}
saveFile(Base64.decode(encodedImg, Base64.DEFAULT), dir, fileName);
localImage = "file://" + dir + "/" + fileName;
} else if (!image.startsWith("file://")) {
throw new IllegalArgumentException("URL_NOT_SUPPORTED");
}
return Uri.parse(localImage);
}
private boolean invokeSMSIntent(String message, String p_phonenumbers) {
Intent intent;
final String phonenumbers = getPhoneNumbersWithManufacturerSpecificSeparators(p_phonenumbers);
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);
try {
this.cordova.startActivityForResult(this, intent, 0);
return true;
} catch (ActivityNotFoundException ignore) {
return false;
}
}
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 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);
}
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
if (ACTIVITY_CODE_SENDVIAEMAIL == requestCode) {
super.onActivityResult(requestCode, resultCode, intent);
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 String getFileName(String url) {
final int lastIndexOfSlash = url.lastIndexOf('/');
if (lastIndexOfSlash == -1) {
return url;
} else {
return url.substring(lastIndexOfSlash + 1);
}
}
private byte[] getBytes(InputStream is) throws IOException {
BufferedInputStream bis = new BufferedInputStream(is);
ByteArrayBuffer baf = new ByteArrayBuffer(5000);
int current;
while ((current = bis.read()) != -1) {
baf.append((byte) current);
}
return baf.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("[:\\\\/*?|<> ]", "_");
}
}