diff --git a/android/build.gradle b/android/build.gradle index 1a1c7b3..ef270df 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group 'com.homex.open_mail_app' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.8.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.4' + classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,8 +25,8 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 31 - + compileSdk 33 + namespace = "com.homex.open_mail_app" sourceSets { main.java.srcDirs += 'src/main/kotlin' } @@ -40,5 +40,5 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.google.code.gson:gson:2.8.5' + implementation 'com.google.code.gson:gson:2.10.1' } diff --git a/android/gradle.properties b/android/gradle.properties index 38c8d45..94adc3a 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M -android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 297f2fe..59bc51a 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/lib/open_mail_app.dart b/lib/open_mail_app.dart index 3908cb7..5bae36a 100644 --- a/lib/open_mail_app.dart +++ b/lib/open_mail_app.dart @@ -6,31 +6,209 @@ import 'package:platform/platform.dart'; import 'package:url_launcher/url_launcher.dart'; /// Launch Schemes for supported apps: +const String _LAUNCH_SCHEME_AIRMAIL = 'airmail://'; const String _LAUNCH_SCHEME_APPLE_MAIL = 'message://'; -const String _LAUNCH_SCHEME_GMAIL = 'googlegmail://'; const String _LAUNCH_SCHEME_DISPATCH = 'x-dispatch://'; -const String _LAUNCH_SCHEME_SPARK = 'readdle-spark://'; -const String _LAUNCH_SCHEME_AIRMAIL = 'airmail://'; -const String _LAUNCH_SCHEME_OUTLOOK = 'ms-outlook://'; -const String _LAUNCH_SCHEME_YAHOO = 'ymail://'; const String _LAUNCH_SCHEME_FASTMAIL = 'fastmail://'; -const String _LAUNCH_SCHEME_SUPERHUMAN = 'superhuman://'; +const String _LAUNCH_SCHEME_GMAIL = 'googlegmail://'; +const String _LAUNCH_SCHEME_OUTLOOK = 'ms-outlook://'; const String _LAUNCH_SCHEME_PROTONMAIL = 'protonmail://'; +const String _LAUNCH_SCHEME_SPARK = 'readdle-spark://'; +const String _LAUNCH_SCHEME_SUPERHUMAN = 'superhuman://'; +const String _LAUNCH_SCHEME_YAHOO = 'ymail://'; + +class ComposeData { + String base; + String to; + String cc; + String bcc; + String subject; + String body; + bool composeStarted = false; + + ComposeData({ + this.base = 'mailto:', + this.to = 'to', + this.cc = 'cc', + this.bcc = 'bcc', + this.subject = 'subject', + this.body = 'body', + }); + + String get qsPairSeparator { + String separator = !composeStarted ? '?' : '&'; + composeStarted = true; + return separator; + } + + String getComposeLaunchSchemeForIos(EmailContent content) { + String scheme = base; + + if (content.to.isNotEmpty) { + scheme += '$qsPairSeparator$to=${content.to.join(',')}'; + } + + if (content.cc.isNotEmpty) { + scheme += '$qsPairSeparator$cc=${content.cc.join(',')}'; + } + + if (content.bcc.isNotEmpty) { + scheme += '$qsPairSeparator$bcc=${content.bcc.join(',')}'; + } + + if (content.subject.isNotEmpty) { + scheme += '$qsPairSeparator$subject=${content.subject}'; + } + + if (content.body.isNotEmpty) { + scheme += '$qsPairSeparator$body=${content.body}'; + } + + // Reset to make sure you can fetch this multiple times on the same instance. + composeStarted = false; + + return scheme; + } + + @override + String toString() { + return this.getComposeLaunchSchemeForIos(EmailContent()); + } +} + +/// Used to populate the precomposed emails +/// +/// [to] List of [String] Addressees, +/// [cc] Carbon Copy [String] list +/// [bcc] Blind carbon copy [String] list +/// [subject] [String], getter returns [Uri.encodeComponent] from the set [String] +/// [body] [String], getter returns [Uri.encodeComponent] from the set [String] +class EmailContent { + final List to; + final List cc; + final List bcc; + final String _subject; + + final String _body; + EmailContent({ + List? to, + List? cc, + List? bcc, + String? subject, + String? body, + }) : this.to = to ?? const [], + this.cc = cc ?? const [], + this.bcc = bcc ?? const [], + this._subject = subject ?? '', + this._body = body ?? ''; + + String get body => OpenMailApp._isIOS ? Uri.encodeComponent(_body) : _body; + + String get subject => + OpenMailApp._isIOS ? Uri.encodeComponent(_subject) : _subject; + + String toJson() { + final Map emailContent = { + 'to': this.to, + 'cc': this.cc, + 'bcc': this.bcc, + 'subject': this.subject, + 'body': this.body, + }; + + return json.encode(emailContent); + } +} + +class MailApp { + final String name; + final String iosLaunchScheme; + final ComposeData? composeData; + + const MailApp({ + required this.name, + required this.iosLaunchScheme, + this.composeData, + }); + + factory MailApp.fromJson(Map json) => MailApp( + name: json["name"], + iosLaunchScheme: json["iosLaunchScheme"] ?? '', + composeData: json["composeData"] ?? ComposeData(), + ); + + String? composeLaunchScheme(EmailContent content) { + if (OpenMailApp._isAndroid) { + return content.toJson(); + } else if (OpenMailApp._isIOS) { + return this.composeData!.getComposeLaunchSchemeForIos(content); + } else { + throw Exception('Platform not supported'); + } + } + + Map toJson() => { + "name": name, + "iosLaunchScheme": iosLaunchScheme, + "composeData": composeData, + }; +} + +/// A simple dialog for allowing the user to pick and open an email app +/// Use with [OpenMailApp.getMailApps] or [OpenMailApp.openMailApp] to get a +/// list of mail apps installed on the device. +class MailAppPickerDialog extends StatelessWidget { + /// The title of the dialog + final String title; + + /// The mail apps for the dialog to provide as options + final List mailApps; + final EmailContent? emailContent; + + const MailAppPickerDialog({ + Key? key, + this.title = 'Choose Mail App', + required this.mailApps, + this.emailContent, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SimpleDialog( + title: Text(title), + children: [ + for (var app in mailApps) + SimpleDialogOption( + child: Text(app.name), + onPressed: () { + final content = this.emailContent; + if (content != null) { + OpenMailApp.composeNewEmailInSpecificMailApp( + mailApp: app, + emailContent: content, + ); + } else { + OpenMailApp.openSpecificMailApp(app); + } + + Navigator.pop(context); + }, + ), + ], + ); + } +} /// Provides ability to query device for installed email apps and open those /// apps class OpenMailApp { - OpenMailApp._(); - @visibleForTesting static Platform platform = LocalPlatform(); - static bool get _isAndroid => platform.isAndroid; - - static bool get _isIOS => platform.isIOS; - static const MethodChannel _channel = const MethodChannel('open_mail_app'); + static List _filterList = ['paypal']; + static List _supportedMailApps = [ MailApp( name: 'Apple Mail', @@ -104,43 +282,9 @@ class OpenMailApp { ), ]; - /// Attempts to open an email app installed on the device. - /// - /// Android: Will open mail app or show native picker if multiple. - /// - /// iOS: Will open mail app if single installed mail app is found. If multiple - /// are found will return a [OpenMailAppResult] that contains list of - /// [MailApp]s. This can be used along with [MailAppPickerDialog] to allow - /// the user to pick the mail app they want to open. - /// - /// Also see [openSpecificMailApp] and [getMailApps] for other use cases. - /// - /// Android: [nativePickerTitle] will set the title of the native picker. - static Future openMailApp({ - String nativePickerTitle = '', - }) async { - if (_isAndroid) { - final result = await _channel.invokeMethod( - 'openMailApp', - {'nativePickerTitle': nativePickerTitle}, - ) ?? - false; - return OpenMailAppResult(didOpen: result); - } else if (_isIOS) { - final apps = await _getIosMailApps(); - if (apps.length == 1) { - final result = await launch( - apps.first.iosLaunchScheme, - forceSafariVC: false, - ); - return OpenMailAppResult(didOpen: result); - } else { - return OpenMailAppResult(didOpen: false, options: apps); - } - } else { - throw Exception('Platform not supported'); - } - } + static bool get _isAndroid => platform.isAndroid; + static bool get _isIOS => platform.isIOS; + OpenMailApp._(); /// Allows you to open a mail application installed on the user's device /// and start composing a new email with the contents in [emailContent]. @@ -170,9 +314,8 @@ class OpenMailApp { String? launchScheme = installedApps.first.composeLaunchScheme(emailContent); if (launchScheme != null) { - result = await launch( - launchScheme, - forceSafariVC: false, + result = await launchUrl( + Uri.parse(launchScheme), ); } return OpenMailAppResult(didOpen: result); @@ -209,9 +352,8 @@ class OpenMailApp { } else if (_isIOS) { String? launchScheme = mailApp.composeLaunchScheme(emailContent); if (launchScheme != null) { - return await launch( - launchScheme, - forceSafariVC: false, + return await launchUrl( + Uri.parse(launchScheme), ); } @@ -221,6 +363,56 @@ class OpenMailApp { } } + /// Returns a list of installed email apps on the device + /// + /// iOS: [MailApp.iosLaunchScheme] will be populated + static Future> getMailApps() async { + if (_isAndroid) { + return await _getAndroidMailApps(); + } else if (_isIOS) { + return await _getIosMailApps(); + } else { + throw Exception('Platform not supported'); + } + } + + /// Attempts to open an email app installed on the device. + /// + /// Android: Will open mail app or show native picker if multiple. + /// + /// iOS: Will open mail app if single installed mail app is found. If multiple + /// are found will return a [OpenMailAppResult] that contains list of + /// [MailApp]s. This can be used along with [MailAppPickerDialog] to allow + /// the user to pick the mail app they want to open. + /// + /// Also see [openSpecificMailApp] and [getMailApps] for other use cases. + /// + /// Android: [nativePickerTitle] will set the title of the native picker. + static Future openMailApp({ + String nativePickerTitle = '', + }) async { + if (_isAndroid) { + final result = await _channel.invokeMethod( + 'openMailApp', + {'nativePickerTitle': nativePickerTitle}, + ) ?? + false; + return OpenMailAppResult(didOpen: result); + } else if (_isIOS) { + final apps = await _getIosMailApps(); + if (apps.length == 1) { + final result = await launchUrl( + Uri.parse(apps.first.iosLaunchScheme), + ); + return OpenMailAppResult(didOpen: result); + } else { + return OpenMailAppResult(didOpen: false, options: apps); + } + } else { + throw Exception('Platform not supported'); + } + } + /// Attempts to open a specific email app installed on the device. /// Get a [MailApp] from calling [getMailApps] static Future openSpecificMailApp(MailApp mailApp) async { @@ -232,26 +424,23 @@ class OpenMailApp { false; return result; } else if (_isIOS) { - return await launch( - mailApp.iosLaunchScheme, - forceSafariVC: false, + return await launchUrl( + Uri.parse(mailApp.iosLaunchScheme), ); } else { throw Exception('Platform not supported'); } } - /// Returns a list of installed email apps on the device + /// Clears existing filter list and sets the filter list to the passed values. + /// Filter list is case insensitive. Listed apps will be excluded from the results + /// of `getMailApps` by name. /// - /// iOS: [MailApp.iosLaunchScheme] will be populated - static Future> getMailApps() async { - if (_isAndroid) { - return await _getAndroidMailApps(); - } else if (_isIOS) { - return await _getIosMailApps(); - } else { - throw Exception('Platform not supported'); - } + /// Default filter list includes PayPal, since it implements the mailto: intent-filter + /// on Android, but the intention of this plugin is to provide + /// a utility for finding and opening apps dedicated to sending/receiving email. + static void setFilterList(List filterList) { + _filterList = filterList.map((e) => e.toLowerCase()).toList(); } static Future> _getAndroidMailApps() async { @@ -271,162 +460,13 @@ class OpenMailApp { static Future> _getIosMailApps() async { var installedApps = []; for (var app in _supportedMailApps) { - if (await canLaunch(app.iosLaunchScheme) && + if (await canLaunchUrl(Uri.parse(app.iosLaunchScheme)) && !_filterList.contains(app.name.toLowerCase())) { installedApps.add(app); } } return installedApps; } - - /// Clears existing filter list and sets the filter list to the passed values. - /// Filter list is case insensitive. Listed apps will be excluded from the results - /// of `getMailApps` by name. - /// - /// Default filter list includes PayPal, since it implements the mailto: intent-filter - /// on Android, but the intention of this plugin is to provide - /// a utility for finding and opening apps dedicated to sending/receiving email. - static void setFilterList(List filterList) { - _filterList = filterList.map((e) => e.toLowerCase()).toList(); - } -} - -/// A simple dialog for allowing the user to pick and open an email app -/// Use with [OpenMailApp.getMailApps] or [OpenMailApp.openMailApp] to get a -/// list of mail apps installed on the device. -class MailAppPickerDialog extends StatelessWidget { - /// The title of the dialog - final String title; - - /// The mail apps for the dialog to provide as options - final List mailApps; - final EmailContent? emailContent; - - const MailAppPickerDialog({ - Key? key, - this.title = 'Choose Mail App', - required this.mailApps, - this.emailContent, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return SimpleDialog( - title: Text(title), - children: [ - for (var app in mailApps) - SimpleDialogOption( - child: Text(app.name), - onPressed: () { - final content = this.emailContent; - if (content != null) { - OpenMailApp.composeNewEmailInSpecificMailApp( - mailApp: app, - emailContent: content, - ); - } else { - OpenMailApp.openSpecificMailApp(app); - } - - Navigator.pop(context); - }, - ), - ], - ); - } -} - -class ComposeData { - String base; - String to; - String cc; - String bcc; - String subject; - String body; - bool composeStarted = false; - - String get qsPairSeparator { - String separator = !composeStarted ? '?' : '&'; - composeStarted = true; - return separator; - } - - ComposeData({ - this.base = 'mailto:', - this.to = 'to', - this.cc = 'cc', - this.bcc = 'bcc', - this.subject = 'subject', - this.body = 'body', - }); - - String getComposeLaunchSchemeForIos(EmailContent content) { - String scheme = base; - - if (content.to.isNotEmpty) { - scheme += '$qsPairSeparator$to=${content.to.join(',')}'; - } - - if (content.cc.isNotEmpty) { - scheme += '$qsPairSeparator$cc=${content.cc.join(',')}'; - } - - if (content.bcc.isNotEmpty) { - scheme += '$qsPairSeparator$bcc=${content.bcc.join(',')}'; - } - - if (content.subject.isNotEmpty) { - scheme += '$qsPairSeparator$subject=${content.subject}'; - } - - if (content.body.isNotEmpty) { - scheme += '$qsPairSeparator$body=${content.body}'; - } - - // Reset to make sure you can fetch this multiple times on the same instance. - composeStarted = false; - - return scheme; - } - - @override - String toString() { - return this.getComposeLaunchSchemeForIos(EmailContent()); - } -} - -class MailApp { - final String name; - final String iosLaunchScheme; - final ComposeData? composeData; - - const MailApp({ - required this.name, - required this.iosLaunchScheme, - this.composeData, - }); - - factory MailApp.fromJson(Map json) => MailApp( - name: json["name"], - iosLaunchScheme: json["iosLaunchScheme"] ?? '', - composeData: json["composeData"] ?? ComposeData(), - ); - - Map toJson() => { - "name": name, - "iosLaunchScheme": iosLaunchScheme, - "composeData": composeData, - }; - - String? composeLaunchScheme(EmailContent content) { - if (OpenMailApp._isAndroid) { - return content.toJson(); - } else if (OpenMailApp._isIOS) { - return this.composeData!.getComposeLaunchSchemeForIos(content); - } else { - throw Exception('Platform not supported'); - } - } } /// Result of calling [OpenMailApp.openMailApp] @@ -436,54 +476,10 @@ class OpenMailAppResult { final bool didOpen; final List options; - bool get canOpen => options.isNotEmpty; - OpenMailAppResult({ required this.didOpen, this.options = const [], }); -} - -/// Used to populate the precomposed emails -/// -/// [to] List of [String] Addressees, -/// [cc] Carbon Copy [String] list -/// [bcc] Blind carbon copy [String] list -/// [subject] [String], getter returns [Uri.encodeComponent] from the set [String] -/// [body] [String], getter returns [Uri.encodeComponent] from the set [String] -class EmailContent { - final List to; - final List cc; - final List bcc; - final String _subject; - - String get subject => - OpenMailApp._isIOS ? Uri.encodeComponent(_subject) : _subject; - final String _body; - - String get body => OpenMailApp._isIOS ? Uri.encodeComponent(_body) : _body; - EmailContent({ - List? to, - List? cc, - List? bcc, - String? subject, - String? body, - }) : this.to = to ?? const [], - this.cc = cc ?? const [], - this.bcc = bcc ?? const [], - this._subject = subject ?? '', - this._body = body ?? ''; - - String toJson() { - final Map emailContent = { - 'to': this.to, - 'cc': this.cc, - 'bcc': this.bcc, - 'subject': this.subject, - 'body': this.body, - }; - - return json.encode(emailContent); - } + bool get canOpen => options.isNotEmpty; } diff --git a/pubspec.yaml b/pubspec.yaml index 2e0a265..b815591 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,12 +4,11 @@ version: 0.4.4 homepage: https://github.com/HomeXLabs/open-mail-app-flutter environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.0" + sdk: ">=2.19.4 <3.0.0" dependencies: platform: ^3.1.0 - url_launcher: ^6.0.2 + url_launcher: ^6.1.11 flutter: sdk: flutter