diff --git a/packages/share/README.md b/packages/share/README.md
index 4536c5818b7d..0a58de0f7f61 100644
--- a/packages/share/README.md
+++ b/packages/share/README.md
@@ -22,3 +22,8 @@ Then invoke the static `share` method anywhere in your Dart code
``` dart
Share.share('check out my website https://example.com');
```
+
+To share a file invoke the static `shareFile` method anywhere in your Dart code
+``` dart
+Share.shareFile(File('${directory.path}/image.jpg'));
+```
diff --git a/packages/share/android/build.gradle b/packages/share/android/build.gradle
index 3b265e6c5ca7..f2b61ca35e13 100644
--- a/packages/share/android/build.gradle
+++ b/packages/share/android/build.gradle
@@ -45,3 +45,8 @@ android {
disable 'InvalidPackage'
}
}
+
+dependencies {
+ // use support-compat starting from 28.0.0
+ implementation 'com.android.support:support-core-utils:27.1.1'
+}
\ No newline at end of file
diff --git a/packages/share/android/src/main/AndroidManifest.xml b/packages/share/android/src/main/AndroidManifest.xml
index 407eae4d8128..e687dab0032d 100644
--- a/packages/share/android/src/main/AndroidManifest.xml
+++ b/packages/share/android/src/main/AndroidManifest.xml
@@ -1,3 +1,15 @@
+ package="io.flutter.plugins.share">
+
+
+
+
+
+
diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java b/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java
index 25d4e842ae1d..76e6b7a95028 100644
--- a/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java
+++ b/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java
@@ -5,9 +5,14 @@
package io.flutter.plugins.share;
import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
+import androidx.annotation.NonNull;
+import androidx.core.content.FileProvider;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.PluginRegistry.Registrar;
+import java.io.*;
import java.util.Map;
/** Plugin method host for presenting a share sheet via Intent */
@@ -29,15 +34,36 @@ private SharePlugin(Registrar registrar) {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
- if (call.method.equals("share")) {
- if (!(call.arguments instanceof Map)) {
- throw new IllegalArgumentException("Map argument expected");
- }
- // Android does not support showing the share sheet at a particular point on screen.
- share((String) call.argument("text"));
- result.success(null);
- } else {
- result.notImplemented();
+ switch (call.method) {
+ case "share":
+ expectMapArguments(call);
+ // Android does not support showing the share sheet at a particular point on screen.
+ share((String) call.argument("text"));
+ result.success(null);
+ break;
+ case "shareFile":
+ expectMapArguments(call);
+ // Android does not support showing the share sheet at a particular point on screen.
+ try {
+ shareFile(
+ (String) call.argument("path"),
+ (String) call.argument("mimeType"),
+ (String) call.argument("subject"),
+ (String) call.argument("text"));
+ result.success(null);
+ } catch (IOException e) {
+ result.error(e.getMessage(), null, null);
+ }
+ break;
+ default:
+ result.notImplemented();
+ break;
+ }
+ }
+
+ private void expectMapArguments(MethodCall call) throws IllegalArgumentException {
+ if (!(call.arguments instanceof Map)) {
+ throw new IllegalArgumentException("Map argument expected");
}
}
@@ -58,4 +84,95 @@ private void share(String text) {
mRegistrar.context().startActivity(chooserIntent);
}
}
+
+ private void shareFile(String path, String mimeType, String subject, String text)
+ throws IOException {
+ if (path == null || path.isEmpty()) {
+ throw new IllegalArgumentException("Non-empty path expected");
+ }
+
+ File file = new File(path);
+ clearExternalShareFolder();
+ if (!fileIsOnExternal(file)) {
+ file = copyToExternalShareFolder(file);
+ }
+
+ Uri fileUri =
+ FileProvider.getUriForFile(
+ mRegistrar.context(),
+ mRegistrar.context().getPackageName() + ".flutter.share_provider",
+ file);
+
+ Intent shareIntent = new Intent();
+ shareIntent.setAction(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
+ if (subject != null) shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
+ if (text != null) shareIntent.putExtra(Intent.EXTRA_TEXT, text);
+ shareIntent.setType(mimeType != null ? mimeType : "*/*");
+ shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */);
+ if (mRegistrar.activity() != null) {
+ mRegistrar.activity().startActivity(chooserIntent);
+ } else {
+ chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mRegistrar.context().startActivity(chooserIntent);
+ }
+ }
+
+ private boolean fileIsOnExternal(File file) {
+ try {
+ String filePath = file.getCanonicalPath();
+ File externalDir = Environment.getExternalStorageDirectory();
+ return externalDir != null && filePath.startsWith(externalDir.getCanonicalPath());
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ private void clearExternalShareFolder() {
+ File folder = getExternalShareFolder();
+ if (folder.exists()) {
+ for (File file : folder.listFiles()) {
+ file.delete();
+ }
+ folder.delete();
+ }
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ private File copyToExternalShareFolder(File file) throws IOException {
+ File folder = getExternalShareFolder();
+ if (!folder.exists()) {
+ folder.mkdirs();
+ }
+
+ File newFile = new File(folder, file.getName());
+ copy(file, newFile);
+ return newFile;
+ }
+
+ @NonNull
+ private File getExternalShareFolder() {
+ return new File(mRegistrar.context().getExternalCacheDir(), "share");
+ }
+
+ private static void copy(File src, File dst) throws IOException {
+ InputStream in = new FileInputStream(src);
+ try {
+ OutputStream out = new FileOutputStream(dst);
+ try {
+ // Transfer bytes from in to out
+ byte[] buf = new byte[1024];
+ int len;
+ while ((len = in.read(buf)) > 0) {
+ out.write(buf, 0, len);
+ }
+ } finally {
+ out.close();
+ }
+ } finally {
+ in.close();
+ }
+ }
}
diff --git a/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml b/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml
new file mode 100644
index 000000000000..ce94d87583e4
--- /dev/null
+++ b/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/share/ios/Classes/SharePlugin.m b/packages/share/ios/Classes/SharePlugin.m
index 7706e58e7152..2411650ae56e 100644
--- a/packages/share/ios/Classes/SharePlugin.m
+++ b/packages/share/ios/Classes/SharePlugin.m
@@ -14,8 +14,19 @@ + (void)registerWithRegistrar:(NSObject *)registrar {
binaryMessenger:registrar.messenger];
[shareChannel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) {
+ NSDictionary *arguments = [call arguments];
+ NSNumber *originX = arguments[@"originX"];
+ NSNumber *originY = arguments[@"originY"];
+ NSNumber *originWidth = arguments[@"originWidth"];
+ NSNumber *originHeight = arguments[@"originHeight"];
+
+ CGRect originRect;
+ if (originX != nil && originY != nil && originWidth != nil && originHeight != nil) {
+ originRect = CGRectMake([originX doubleValue], [originY doubleValue],
+ [originWidth doubleValue], [originHeight doubleValue]);
+ }
+
if ([@"share" isEqualToString:call.method]) {
- NSDictionary *arguments = [call arguments];
NSString *shareText = arguments[@"text"];
if (shareText.length == 0) {
@@ -25,18 +36,27 @@ + (void)registerWithRegistrar:(NSObject *)registrar {
return;
}
- NSNumber *originX = arguments[@"originX"];
- NSNumber *originY = arguments[@"originY"];
- NSNumber *originWidth = arguments[@"originWidth"];
- NSNumber *originHeight = arguments[@"originHeight"];
+ [self share:@[ shareText ]
+ withController:[UIApplication sharedApplication].keyWindow.rootViewController
+ atSource:originRect];
+ result(nil);
+ } else if ([@"shareFile" isEqualToString:call.method]) {
+ NSString *path = arguments[@"path"];
+ NSString *mimeType = arguments[@"mimeType"];
+ NSString *subject = arguments[@"subject"];
+ NSString *text = arguments[@"text"];
- CGRect originRect;
- if (originX != nil && originY != nil && originWidth != nil && originHeight != nil) {
- originRect = CGRectMake([originX doubleValue], [originY doubleValue],
- [originWidth doubleValue], [originHeight doubleValue]);
+ if (path.length == 0) {
+ result([FlutterError errorWithCode:@"error"
+ message:@"Non-empty path expected"
+ details:nil]);
+ return;
}
- [self share:shareText
+ [self shareFile:path
+ withMimeType:mimeType
+ withSubject:subject
+ withText:text
withController:[UIApplication sharedApplication].keyWindow.rootViewController
atSource:originRect];
result(nil);
@@ -46,12 +66,11 @@ + (void)registerWithRegistrar:(NSObject *)registrar {
}];
}
-+ (void)share:(id)sharedItems
++ (void)share:(NSArray *)shareItems
withController:(UIViewController *)controller
atSource:(CGRect)origin {
UIActivityViewController *activityViewController =
- [[UIActivityViewController alloc] initWithActivityItems:@[ sharedItems ]
- applicationActivities:nil];
+ [[UIActivityViewController alloc] initWithActivityItems:shareItems applicationActivities:nil];
activityViewController.popoverPresentationController.sourceView = controller.view;
if (!CGRectIsEmpty(origin)) {
activityViewController.popoverPresentationController.sourceRect = origin;
@@ -59,4 +78,30 @@ + (void)share:(id)sharedItems
[controller presentViewController:activityViewController animated:YES completion:nil];
}
++ (void)shareFile:(id)path
+ withMimeType:(id)mimeType
+ withSubject:(NSString *)subject
+ withText:(NSString *)text
+ withController:(UIViewController *)controller
+ atSource:(CGRect)origin {
+ NSMutableArray *items = [[NSMutableArray alloc] init];
+
+ if (subject != nil && subject.length != 0) {
+ [items addObject:subject];
+ }
+ if (text != nil && text.length != 0) {
+ [items addObject:text];
+ }
+
+ if ([mimeType hasPrefix:@"image/"]) {
+ UIImage *image = [UIImage imageWithContentsOfFile:path];
+ [items addObject:image];
+ } else {
+ NSURL *url = [NSURL fileURLWithPath:path];
+ [items addObject:url];
+ }
+
+ [self share:items withController:controller atSource:origin];
+}
+
@end
diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart
index a86a8261b5f5..7b26542d3e35 100644
--- a/packages/share/lib/share.dart
+++ b/packages/share/lib/share.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:io';
import 'dart:ui';
import 'package:flutter/services.dart';
@@ -46,4 +47,93 @@ class Share {
// ignore: strong_mode_implicit_dynamic_method
return channel.invokeMethod('share', params);
}
+
+ /// Summons the platform's share sheet to share a file.
+ ///
+ /// Wraps the platform's native share dialog. Can share a file.
+ /// It uses the ACTION_SEND Intent on Android and UIActivityViewController
+ /// on iOS.
+ ///
+ /// The optional `sharePositionOrigin` parameter can be used to specify a global
+ /// origin rect for the share sheet to popover from on iPads. It has no effect
+ /// on non-iPads.
+ ///
+ /// May throw [PlatformException] or [FormatException]
+ /// from [MethodChannel].
+ static Future shareFile(File file,
+ {String mimeType,
+ String subject,
+ String text,
+ Rect sharePositionOrigin}) {
+ assert(file != null);
+ assert(file.existsSync());
+ final Map params = {
+ 'path': file.path,
+ 'mimeType': mimeType ?? _mimeTypeForFile(file),
+ };
+
+ if (subject != null) params['subject'] = subject;
+ if (text != null) params['text'] = text;
+
+ if (sharePositionOrigin != null) {
+ params['originX'] = sharePositionOrigin.left;
+ params['originY'] = sharePositionOrigin.top;
+ params['originWidth'] = sharePositionOrigin.width;
+ params['originHeight'] = sharePositionOrigin.height;
+ }
+
+ return channel.invokeMethod('shareFile', params);
+ }
+
+ static String _mimeTypeForFile(File file) {
+ assert(file != null);
+ final String path = file.path;
+
+ final int extensionIndex = path.lastIndexOf("\.");
+ if (extensionIndex == -1 || extensionIndex == 0) {
+ return null;
+ }
+
+ final String extension = path.substring(extensionIndex + 1);
+ switch (extension) {
+ // image
+ case 'jpeg':
+ case 'jpg':
+ return 'image/jpeg';
+ case 'gif':
+ return 'image/gif';
+ case 'png':
+ return 'image/png';
+ case 'svg':
+ return 'image/svg+xml';
+ case 'tif':
+ case 'tiff':
+ return 'image/tiff';
+ // audio
+ case 'aac':
+ return 'audio/aac';
+ case 'oga':
+ return 'audio/ogg';
+ // video
+ case 'avi':
+ return 'video/x-msvideo';
+ case 'mpeg':
+ return 'video/mpeg';
+ case 'ogv':
+ return 'video/ogg';
+ // other
+ case 'csv':
+ return 'text/csv';
+ case 'htm':
+ case 'html':
+ return 'text/html';
+ case 'json':
+ return 'application/json';
+ case 'pdf':
+ return 'application/pdf';
+ case 'txt':
+ return 'text/plain';
+ }
+ return null;
+ }
}
diff --git a/packages/share/test/share_test.dart b/packages/share/test/share_test.dart
index 66f7af7b5ad9..747943cfd45f 100644
--- a/packages/share/test/share_test.dart
+++ b/packages/share/test/share_test.dart
@@ -2,14 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:io';
import 'dart:ui';
+import 'package:flutter/services.dart';
import 'package:mockito/mockito.dart';
import 'package:share/share.dart';
import 'package:test/test.dart';
-import 'package:flutter/services.dart';
-
void main() {
MockMethodChannel mockChannel;
@@ -58,6 +58,58 @@ void main() {
'originHeight': 4.0,
}));
});
+
+ test('sharing null file fails', () {
+ expect(
+ () => Share.shareFile(null),
+ throwsA(const TypeMatcher()),
+ );
+ verifyZeroInteractions(mockChannel);
+ });
+
+ test('sharing empty file fails', () {
+ expect(
+ () => Share.shareFile(null),
+ throwsA(const TypeMatcher()),
+ );
+ verifyZeroInteractions(mockChannel);
+ });
+
+ test('sharing non existing file fails', () {
+ expect(
+ () => Share.shareFile(File('/sdcard/nofile.txt')),
+ throwsA(const TypeMatcher()),
+ );
+ verifyZeroInteractions(mockChannel);
+ });
+
+ test('sharing file sets correct mimeType', () async {
+ final File file = File('tempfile-83649a.png');
+ try {
+ file.createSync();
+ await Share.shareFile(file);
+ verify(mockChannel.invokeMethod('shareFile', {
+ 'path': file.path,
+ 'mimeType': 'image/png',
+ }));
+ } finally {
+ file.deleteSync();
+ }
+ });
+
+ test('sharing file sets passed mimeType', () async {
+ final File file = File('tempfile-83649a.png');
+ try {
+ file.createSync();
+ await Share.shareFile(file, mimeType: '*/*');
+ verify(mockChannel.invokeMethod('shareFile', {
+ 'path': file.path,
+ 'mimeType': '*/*',
+ }));
+ } finally {
+ file.deleteSync();
+ }
+ });
}
class MockMethodChannel extends Mock implements MethodChannel {}