diff --git a/ExampleApp/android/.project b/ExampleApp/android/.project new file mode 100644 index 0000000..0e0a1ba --- /dev/null +++ b/ExampleApp/android/.project @@ -0,0 +1,17 @@ + + + android_ + Project android_ created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/ExampleApp/android/.settings/org.eclipse.buildship.core.prefs b/ExampleApp/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..e889521 --- /dev/null +++ b/ExampleApp/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 diff --git a/android/.project b/android/.project new file mode 100644 index 0000000..3964dd3 --- /dev/null +++ b/android/.project @@ -0,0 +1,17 @@ + + + android + Project android created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..e889521 --- /dev/null +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 diff --git a/android/src/main/java/com/reactlibrary/mailcompose/RNMailComposeModule.java b/android/src/main/java/com/reactlibrary/mailcompose/RNMailComposeModule.java index f0f2bd0..acffcc9 100644 --- a/android/src/main/java/com/reactlibrary/mailcompose/RNMailComposeModule.java +++ b/android/src/main/java/com/reactlibrary/mailcompose/RNMailComposeModule.java @@ -1,23 +1,42 @@ package com.reactlibrary.mailcompose; import android.app.Activity; +import android.app.PendingIntent; import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.content.IntentSender; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Bundle; +import android.os.Parcelable; import android.text.Html; import android.text.Spanned; import android.util.Base64; +import android.support.v4.content.FileProvider; +import android.util.Log; +import android.widget.Toast; +import android.os.Build; import com.facebook.react.bridge.ActivityEventListener; -import com.facebook.react.bridge.BaseActivityEventListener; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableNativeArray; import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; import java.io.ByteArrayOutputStream; import java.io.File; @@ -30,35 +49,22 @@ import java.net.URL; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; +import android.support.annotation.NonNull; -public class RNMailComposeModule extends ReactContextBaseJavaModule { - private static final int ACTIVITY_SEND = 129382; - +public class RNMailComposeModule extends ReactContextBaseJavaModule implements ActivityEventListener { + private final ReactApplicationContext reactContext; + private static final int ACTIVITY_SEND = 12938; + public static String lastSelection = null; private Promise mPromise; - private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() { - - @Override - public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) { - if (requestCode == ACTIVITY_SEND) { - if (mPromise != null) { - if (resultCode == Activity.RESULT_CANCELED) { - mPromise.reject("cancelled", "Operation has been cancelled"); - } else { - mPromise.resolve("sent"); - } - mPromise = null; - } - } - } - }; - public RNMailComposeModule(final ReactApplicationContext reactContext) { super(reactContext); - reactContext.addActivityEventListener(mActivityEventListener); + reactContext.addActivityEventListener(this); + this.reactContext = reactContext; } @Override @@ -97,7 +103,7 @@ private void putExtra(Intent intent, String key, ArrayList value) { } } - private void addAttachments(Intent intent, ReadableArray attachments) { + private void addAttachments(Intent intent, ReadableArray attachments, String fileProviderUri) { if (attachments == null) return; ArrayList uris = new ArrayList<>(); @@ -105,7 +111,14 @@ private void addAttachments(Intent intent, ReadableArray attachments) { if (attachments.getType(i) == ReadableType.Map) { ReadableMap attachment = attachments.getMap(i); if (attachment != null) { - byte[] blob = getBlob(attachment, "data"); + Uri contentUri = null; + byte[] blob = null; + if (attachment.hasKey("url") && attachment.getType("url") == ReadableType.String && fileProviderUri != null) { + contentUri = FileProvider.getUriForFile(this.reactContext, fileProviderUri, new File(attachment.getString("url"))); + } else { + blob = getBlob(attachment, "data"); + } + String text = getString(attachment, "text"); // String mimeType = getString(attachment, "mimeType"); String filename = getString(attachment, "filename"); @@ -114,12 +127,16 @@ private void addAttachments(Intent intent, ReadableArray attachments) { } String ext = getString(attachment, "ext"); - File tempFile = createTempFile(filename, ext); + File tempFile = null; if (blob != null) { + createTempFile(filename, ext); tempFile = writeBlob(tempFile, blob); } else if (text != null) { + createTempFile(filename, ext); tempFile = writeText(tempFile, text); + } else if (contentUri != null) { + uris.add(contentUri); } if (tempFile != null) { @@ -283,15 +300,132 @@ private File writeBlob(File file, byte[] blob) { return null; } + public static class ChooserBroadcast extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String result = String.valueOf(intent.getExtras().get(Intent.EXTRA_CHOSEN_COMPONENT)); + String chosenComponent = result.substring(result.indexOf("{") + 1, result.indexOf("/")); + + RNMailComposeModule.lastSelection = chosenComponent; + } + } + + @ReactMethod + public void hasMailApp(String appName, Promise promise) { + + // Get App infos + Intent emailAppIntent = getEmailAppIntent(); + List emailAppInfos = getCurrentActivity().getPackageManager().queryIntentActivities(emailAppIntent, PackageManager.MATCH_ALL); + + for (int i = 0; i < emailAppInfos.size(); i++) { + String packageName = emailAppInfos.get(i).activityInfo.packageName; + if (packageName.equals(appName)) { + promise.resolve(true); + return; + } + } + promise.resolve(false); + } + + @ReactMethod + public void getMailAppData(Promise promise) { + + WritableArray emailAppArray = new WritableNativeArray(); + + // Get App infos + Intent emailAppIntent = getEmailAppIntent(); + PackageManager packageManager = getCurrentActivity().getPackageManager(); + List emailAppInfos = packageManager.queryIntentActivities(emailAppIntent, PackageManager.MATCH_ALL); + + ArrayList addedPackages = new ArrayList<>(); + for (int i = 0; i < emailAppInfos.size(); i++) { + ActivityInfo activityInfo = emailAppInfos.get(i).activityInfo; + // Prevent Duplicated + if (addedPackages.indexOf(activityInfo.packageName) == -1) { + addedPackages.add(activityInfo.packageName); + + // Build Map with app data + WritableMap emailAppData = new WritableNativeMap(); + emailAppData.putString("name", activityInfo.packageName); + emailAppData.putString("raw", packageManager.getApplicationLabel(activityInfo.applicationInfo).toString()); + + Drawable icon = packageManager.getApplicationIcon(activityInfo.applicationInfo); + emailAppData.putString("icon", getBase64(icon != null ? icon : packageManager.getDefaultActivityIcon())); + + // Add to array + emailAppArray.pushMap(emailAppData); + } + } + promise.resolve(emailAppArray); + } + + @NonNull + static private Bitmap getBitmapFromDrawable(@NonNull Drawable drawable) { + final Bitmap bmp = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bmp); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bmp; + } + + private String getBase64(Drawable icon) { + Bitmap bitmap = getBitmapFromDrawable(icon); + return encodeToBase64(bitmap, Bitmap.CompressFormat.PNG, 100); + } + + private String encodeToBase64(Bitmap image, Bitmap.CompressFormat compressFormat, int quality) { + ByteArrayOutputStream byteArrayOS = new ByteArrayOutputStream(); + image.compress(compressFormat, quality, byteArrayOS); + return Base64.encodeToString(byteArrayOS.toByteArray(), Base64.DEFAULT); + } + + @ReactMethod + public void getLastSelection(Promise promise) { + String selected = lastSelection; + lastSelection = null; + promise.resolve(selected); + } + @ReactMethod - public void send(ReadableMap data, Promise promise) throws IOException { + public void send(ReadableMap data, Promise promise) { if (mPromise != null) { mPromise.reject("timeout", "Operation has timed out"); mPromise = null; } - Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); + try { + // Check if Mail App exists + ArrayList mailIntents = getEmailAppLauncherIntentsWithData(data); + if (mailIntents == null || mailIntents.size() == 0) { + Toast.makeText(getCurrentActivity(), "No matching app found", Toast.LENGTH_LONG).show(); + return; + } + + // Create chooser + Intent chooserIntent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + chooserIntent = Intent.createChooser(new Intent(), "Select email app:", getIntentSender()); + } else { + chooserIntent = Intent.createChooser(new Intent(), "Select email app:"); + } + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, mailIntents.toArray( new Parcelable[mailIntents.size()] )); + getCurrentActivity().startActivityForResult(chooserIntent, ACTIVITY_SEND); + mPromise = promise; + } catch (NullPointerException e) { + promise.reject("failed", "StartActivityForResult failed"); + } catch (ActivityNotFoundException e) { + promise.reject("failed", "Activity Not Found"); + } catch (RuntimeException e) { + promise.reject("failed", "External App Probably Cannot Handle Parcelable"); + } catch (Exception e) { + promise.reject("failed", "Unknown Error"); + } + } + // Create intent for data share + private Intent getDataIntent (ReadableMap data) { + Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); String text = getString(data, "body"); String html = getString(data, "html"); if (!isEmpty(html)) { @@ -299,30 +433,80 @@ public void send(ReadableMap data, Promise promise) throws IOException { putExtra(intent, Intent.EXTRA_TEXT, Html.fromHtml(html)); putExtra(intent, Intent.EXTRA_HTML_TEXT, Html.fromHtml(html)); } else { - intent.setType("text/plain"); + // intent.setType("text/plain"); + intent.setType("message/rfc822"); + if (!isEmpty(text)) { putExtra(intent, Intent.EXTRA_TEXT, text); } } - putExtra(intent, Intent.EXTRA_SUBJECT, getString(data, "subject")); putExtra(intent, Intent.EXTRA_EMAIL, getStringArray(data, "toRecipients")); putExtra(intent, Intent.EXTRA_CC, getStringArray(data, "ccRecipients")); putExtra(intent, Intent.EXTRA_BCC, getStringArray(data, "bccRecipients")); - addAttachments(intent, getArray(data, "attachments")); - + addAttachments(intent, getArray(data, "attachments"), getString(data, "fileProviderUri")); intent.putExtra("exit_on_sent", true); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - try { - getCurrentActivity().startActivityForResult(Intent.createChooser(intent, "Send Mail"), ACTIVITY_SEND); - mPromise = promise; - } catch (ActivityNotFoundException e) { - promise.reject("failed", "Activity Not Found"); - } catch (Exception e) { - promise.reject("failed", "Unknown Error"); + return intent; + } + + // Intent that only email apps can handle + private Intent getEmailAppIntent() { + Intent emailAppIntent = new Intent(Intent.ACTION_SENDTO); + emailAppIntent.setData(Uri.parse("mailto:")); + emailAppIntent.putExtra(Intent.EXTRA_EMAIL, ""); + emailAppIntent.putExtra(Intent.EXTRA_SUBJECT, ""); + + return emailAppIntent; + } + + // Get E-Mail App intents only for share picker (filtered for duplicates) + private ArrayList getEmailAppLauncherIntentsWithData (ReadableMap data) { + ArrayList emailAppLauncherIntentsWithData = new ArrayList<>(); + Intent dataIntent = getDataIntent(data); + Intent emailAppIntent = getEmailAppIntent(); + + String selectedApp = getString(data, "selectedApp"); + + if (selectedApp != null) { + // Set selected mail app + emailAppLauncherIntentsWithData.add(((Intent) dataIntent.clone()).setPackage(selectedApp)); + } else { + // Get All installed apps that can handle email intent + PackageManager packageManager = getCurrentActivity().getPackageManager(); + List emailApps = packageManager.queryIntentActivities(emailAppIntent, PackageManager.GET_META_DATA); + ArrayList addedPackages = new ArrayList<>(); + for (int i = 0; i < emailApps.size(); i++) { + String packageName = emailApps.get(i).activityInfo.packageName; + if (addedPackages.indexOf(packageName) == -1) { + addedPackages.add(packageName); + emailAppLauncherIntentsWithData.add(((Intent) dataIntent.clone()).setPackage(packageName)); + } + } } + return emailAppLauncherIntentsWithData; } -} + private IntentSender getIntentSender () { + Intent receiverIntent = new Intent(reactContext, ChooserBroadcast.class); + PendingIntent pendingIntent = PendingIntent.getBroadcast(reactContext, ACTIVITY_SEND, receiverIntent, PendingIntent.FLAG_UPDATE_CURRENT); + return pendingIntent.getIntentSender(); + } + + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + if (requestCode == ACTIVITY_SEND) { + + if (mPromise != null) { + mPromise.resolve("unknown"); + mPromise = null; + } + } + } + + @Override + public void onNewIntent(Intent intent) { + } +} diff --git a/ios/RNMailCompose/RNMailCompose.swift b/ios/RNMailCompose/RNMailCompose.swift index 5bc690a..2aa36c6 100644 --- a/ios/RNMailCompose/RNMailCompose.swift +++ b/ios/RNMailCompose/RNMailCompose.swift @@ -48,45 +48,54 @@ class RNMailCompose: NSObject, MFMailComposeViewControllerDelegate { return } - let vc = MFMailComposeViewController() - - if let value = data["subject"] as? String { - vc.setSubject(value) - } - if let value = data["toRecipients"] as? [String] { - vc.setToRecipients(value) - } - if let value = data["ccRecipients"] as? [String] { - vc.setCcRecipients(value) - } - if let value = data["bccRecipients"] as? [String] { - vc.setBccRecipients(value) - } - if let value = data["body"] as? String { - vc.setMessageBody(value, isHTML: false) - } - if let value = data["html"] as? String { - vc.setMessageBody(value, isHTML: true) - } - - if let value = data["attachments"] as? [[String: String]] { - for dict in value { - if let data = textToData(utf8: dict["text"], base64: dict["data"]), let mimeType = dict["mimeType"], let filename = toFilename(filename: dict["filename"], ext: dict["ext"]) { - vc.addAttachmentData(data, mimeType: mimeType, fileName: filename) + DispatchQueue.main.async { + let vc = MFMailComposeViewController() + + if let value = data["subject"] as? String { + vc.setSubject(value) + } + if let value = data["toRecipients"] as? [String] { + vc.setToRecipients(value) + } + if let value = data["ccRecipients"] as? [String] { + vc.setCcRecipients(value) + } + if let value = data["bccRecipients"] as? [String] { + vc.setBccRecipients(value) + } + if let value = data["body"] as? String { + vc.setMessageBody(value, isHTML: false) + } + if let value = data["html"] as? String { + vc.setMessageBody(value, isHTML: true) + } + + if let value = data["attachments"] as? [[String: String]] { + for dict in value { + if let data = self.textToData(utf8: dict["text"], base64: dict["data"]), let mimeType = dict["mimeType"], let filename = self.toFilename(filename: dict["filename"], ext: dict["ext"]) { + vc.addAttachmentData(data, mimeType: mimeType, fileName: filename) + } + if let url = dict["url"], let mimeType = dict["mimeType"], let filename = self.toFilename(filename: dict["filename"], ext: dict["ext"]) { + do { + try vc.addAttachmentData(Data(contentsOf: URL(fileURLWithPath: url)), mimeType: mimeType, fileName: filename) + } catch let error { + reject("fileNotFound", "File not found", error) + } + } } } + + vc.mailComposeDelegate = self + + self.resolve = resolve + self.reject = reject + + var rootVC = UIApplication.shared.keyWindow?.rootViewController; + while (rootVC?.presentedViewController != nil) { + rootVC = rootVC?.presentedViewController; + } + rootVC?.present(vc, animated: true, completion: nil) } - - vc.mailComposeDelegate = self - - self.resolve = resolve - self.reject = reject - - var rootVC = UIApplication.shared.keyWindow?.rootViewController; - while (rootVC?.presentedViewController != nil) { - rootVC = rootVC?.presentedViewController; - } - rootVC?.present(vc, animated: true, completion: nil) } func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { @@ -109,4 +118,9 @@ class RNMailCompose: NSObject, MFMailComposeViewControllerDelegate { controller.dismiss(animated: true, completion: nil) } + + @objc + static func requiresMainQueueSetup() -> Bool { + return true + } } diff --git a/ios/RNMailCompose/RNMailComposeBridge.m b/ios/RNMailCompose/RNMailComposeBridge.m index f174ff1..2c5be95 100644 --- a/ios/RNMailCompose/RNMailComposeBridge.m +++ b/ios/RNMailCompose/RNMailComposeBridge.m @@ -21,4 +21,9 @@ @interface RCT_EXTERN_MODULE(RNMailCompose, NSObject) resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject); ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + @end diff --git a/js/RNMailCompose.android.js b/js/RNMailCompose.android.js index 1476c94..de6f4eb 100644 --- a/js/RNMailCompose.android.js +++ b/js/RNMailCompose.android.js @@ -6,8 +6,16 @@ const {RNMailCompose} = NativeModules; export default { name: RNMailCompose.name, - - send(data) { - return RNMailCompose.send(data); + hasMailApp(appName) { + return RNMailCompose.hasMailApp(appName) + }, + getMailAppData() { + return RNMailCompose.getMailAppData() }, + getLastSelection() { + return RNMailCompose.getLastSelection() + }, + send(data) { + return RNMailCompose.send(data) + } }; diff --git a/js/RNMailCompose.ios.js b/js/RNMailCompose.ios.js index 774bb3d..5e34557 100644 --- a/js/RNMailCompose.ios.js +++ b/js/RNMailCompose.ios.js @@ -6,12 +6,10 @@ const {RNMailCompose} = NativeModules; export default { name: RNMailCompose.name, - canSendMail() { return RNMailCompose.canSendMail(); }, - send(data) { return RNMailCompose.send(data); - }, + } }; diff --git a/react-native-mail-compose.podspec b/react-native-mail-compose.podspec index 4d5be68..799ff7b 100644 --- a/react-native-mail-compose.podspec +++ b/react-native-mail-compose.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |s| - s.name = "react-native-mail-compose" - s.version = "0.0.3" - s.summary = "React Native library for composing email. Wraps MFMailComposeViewController for iOS and Intent for Android." + s.name = 'react-native-mail-compose' + s.version = '0.0.3' + s.summary = 'React Native library for composing email. Wraps MFMailComposeViewController for iOS and Intent for Android.' s.requires_arc = true s.license = 'MIT' s.homepage = 'https://github.com/joonhocho/react-native-mail-compose' - s.author = "Joon Ho Cho" - s.source = { :git => "https://github.com/joonhocho/react-native-mail-compose.git" } + s.author = 'Joon Ho Cho' + s.source = { :git => 'https://github.com/joonhocho/react-native-mail-compose.git' } s.source_files = 'ios/**/*.{h,m,swift}' - s.platform = :ios, "8.0" - s.dependency 'React/Core' + s.platform = :ios, '8.0' + s.dependency 'React' end