Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 0920d20

Browse files
authored
Merge pull request #1 from KomodoPlatform/share-shareFile
add native file-sharing feature
2 parents d4fd64f + c6dcd2e commit 0920d20

File tree

8 files changed

+358
-26
lines changed

8 files changed

+358
-26
lines changed

packages/share/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ Then invoke the static `share` method anywhere in your Dart code
2222
``` dart
2323
Share.share('check out my website https://example.com');
2424
```
25+
26+
To share a file invoke the static `shareFile` method anywhere in your Dart code
27+
``` dart
28+
Share.shareFile(File('${directory.path}/image.jpg'));
29+
```

packages/share/android/build.gradle

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ buildscript {
2121
}
2222

2323
dependencies {
24-
classpath 'com.android.tools.build:gradle:3.3.0'
24+
classpath 'com.android.tools.build:gradle:3.3.2'
2525
}
2626
}
2727

@@ -35,7 +35,7 @@ rootProject.allprojects {
3535
apply plugin: 'com.android.library'
3636

3737
android {
38-
compileSdkVersion 27
38+
compileSdkVersion 28
3939

4040
defaultConfig {
4141
minSdkVersion 16
@@ -45,3 +45,7 @@ android {
4545
disable 'InvalidPackage'
4646
}
4747
}
48+
49+
dependencies {
50+
implementation 'androidx.core:core:1.0.0'
51+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
22
package="io.flutter.plugins.share">
3+
4+
<application>
5+
<provider
6+
android:name="androidx.core.content.FileProvider"
7+
android:authorities="${applicationId}.flutter.share_provider"
8+
android:exported="false"
9+
android:grantUriPermissions="true">
10+
<meta-data
11+
android:name="android.support.FILE_PROVIDER_PATHS"
12+
android:resource="@xml/flutter_share_file_paths" />
13+
</provider>
14+
</application>
315
</manifest>

packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java

Lines changed: 127 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@
66

77
import android.content.Intent;
88
import io.flutter.plugin.common.MethodCall;
9+
import android.net.Uri;
10+
import android.os.Environment;
11+
import androidx.annotation.NonNull;
12+
import androidx.core.content.FileProvider;
913
import io.flutter.plugin.common.MethodChannel;
1014
import io.flutter.plugin.common.PluginRegistry.Registrar;
15+
import java.io.*;
1116
import java.util.Map;
1217

1318
/** Plugin method host for presenting a share sheet via Intent */
@@ -29,15 +34,36 @@ private SharePlugin(Registrar registrar) {
2934

3035
@Override
3136
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
32-
if (call.method.equals("share")) {
33-
if (!(call.arguments instanceof Map)) {
34-
throw new IllegalArgumentException("Map argument expected");
35-
}
36-
// Android does not support showing the share sheet at a particular point on screen.
37-
share((String) call.argument("text"));
38-
result.success(null);
39-
} else {
40-
result.notImplemented();
37+
switch (call.method) {
38+
case "share":
39+
expectMapArguments(call);
40+
// Android does not support showing the share sheet at a particular point on screen.
41+
share((String) call.argument("text"));
42+
result.success(null);
43+
break;
44+
case "shareFile":
45+
expectMapArguments(call);
46+
// Android does not support showing the share sheet at a particular point on screen.
47+
try {
48+
shareFile(
49+
(String) call.argument("path"),
50+
(String) call.argument("mimeType"),
51+
(String) call.argument("subject"),
52+
(String) call.argument("text"));
53+
result.success(null);
54+
} catch (IOException e) {
55+
result.error(e.getMessage(), null, null);
56+
}
57+
break;
58+
default:
59+
result.notImplemented();
60+
break;
61+
}
62+
}
63+
64+
private void expectMapArguments(MethodCall call) throws IllegalArgumentException {
65+
if (!(call.arguments instanceof Map)) {
66+
throw new IllegalArgumentException("Map argument expected");
4167
}
4268
}
4369

@@ -58,4 +84,96 @@ private void share(String text) {
5884
mRegistrar.context().startActivity(chooserIntent);
5985
}
6086
}
87+
88+
89+
private void shareFile(String path, String mimeType, String subject, String text)
90+
throws IOException {
91+
if (path == null || path.isEmpty()) {
92+
throw new IllegalArgumentException("Non-empty path expected");
93+
}
94+
95+
File file = new File(path);
96+
clearExternalShareFolder();
97+
if (!fileIsOnExternal(file)) {
98+
file = copyToExternalShareFolder(file);
99+
}
100+
101+
Uri fileUri =
102+
FileProvider.getUriForFile(
103+
mRegistrar.context(),
104+
mRegistrar.context().getPackageName() + ".flutter.share_provider",
105+
file);
106+
107+
Intent shareIntent = new Intent();
108+
shareIntent.setAction(Intent.ACTION_SEND);
109+
shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
110+
if (subject != null) shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
111+
if (text != null) shareIntent.putExtra(Intent.EXTRA_TEXT, text);
112+
shareIntent.setType(mimeType != null ? mimeType : "*/*");
113+
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
114+
Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */);
115+
if (mRegistrar.activity() != null) {
116+
mRegistrar.activity().startActivity(chooserIntent);
117+
} else {
118+
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
119+
mRegistrar.context().startActivity(chooserIntent);
120+
}
121+
}
122+
123+
private boolean fileIsOnExternal(File file) {
124+
try {
125+
String filePath = file.getCanonicalPath();
126+
File externalDir = Environment.getExternalStorageDirectory();
127+
return externalDir != null && filePath.startsWith(externalDir.getCanonicalPath());
128+
} catch (IOException e) {
129+
return false;
130+
}
131+
}
132+
133+
@SuppressWarnings("ResultOfMethodCallIgnored")
134+
private void clearExternalShareFolder() {
135+
File folder = getExternalShareFolder();
136+
if (folder.exists()) {
137+
for (File file : folder.listFiles()) {
138+
file.delete();
139+
}
140+
folder.delete();
141+
}
142+
}
143+
144+
@SuppressWarnings("ResultOfMethodCallIgnored")
145+
private File copyToExternalShareFolder(File file) throws IOException {
146+
File folder = getExternalShareFolder();
147+
if (!folder.exists()) {
148+
folder.mkdirs();
149+
}
150+
151+
File newFile = new File(folder, file.getName());
152+
copy(file, newFile);
153+
return newFile;
154+
}
155+
156+
@NonNull
157+
private File getExternalShareFolder() {
158+
return new File(mRegistrar.context().getExternalCacheDir(), "share");
159+
}
160+
161+
private static void copy(File src, File dst) throws IOException {
162+
InputStream in = new FileInputStream(src);
163+
try {
164+
OutputStream out = new FileOutputStream(dst);
165+
try {
166+
// Transfer bytes from in to out
167+
byte[] buf = new byte[1024];
168+
int len;
169+
while ((len = in.read(buf)) > 0) {
170+
out.write(buf, 0, len);
171+
}
172+
} finally {
173+
out.close();
174+
}
175+
} finally {
176+
in.close();
177+
}
178+
}
61179
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<paths>
3+
<external-path name="external" path="."/>
4+
<external-files-path name="external_files" path="."/>
5+
<external-cache-path name="external_cache" path="."/>
6+
</paths>

packages/share/ios/Classes/SharePlugin.m

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,19 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
1414
binaryMessenger:registrar.messenger];
1515

1616
[shareChannel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) {
17+
NSDictionary *arguments = [call arguments];
18+
NSNumber *originX = arguments[@"originX"];
19+
NSNumber *originY = arguments[@"originY"];
20+
NSNumber *originWidth = arguments[@"originWidth"];
21+
NSNumber *originHeight = arguments[@"originHeight"];
22+
23+
CGRect originRect;
24+
if (originX != nil && originY != nil && originWidth != nil && originHeight != nil) {
25+
originRect = CGRectMake([originX doubleValue], [originY doubleValue],
26+
[originWidth doubleValue], [originHeight doubleValue]);
27+
}
28+
1729
if ([@"share" isEqualToString:call.method]) {
18-
NSDictionary *arguments = [call arguments];
1930
NSString *shareText = arguments[@"text"];
2031

2132
if (shareText.length == 0) {
@@ -25,18 +36,27 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
2536
return;
2637
}
2738

28-
NSNumber *originX = arguments[@"originX"];
29-
NSNumber *originY = arguments[@"originY"];
30-
NSNumber *originWidth = arguments[@"originWidth"];
31-
NSNumber *originHeight = arguments[@"originHeight"];
39+
[self share:@[ shareText ]
40+
withController:[UIApplication sharedApplication].keyWindow.rootViewController
41+
atSource:originRect];
42+
result(nil);
43+
} else if ([@"shareFile" isEqualToString:call.method]) {
44+
NSString *path = arguments[@"path"];
45+
NSString *mimeType = arguments[@"mimeType"];
46+
NSString *subject = arguments[@"subject"];
47+
NSString *text = arguments[@"text"];
3248

33-
CGRect originRect;
34-
if (originX != nil && originY != nil && originWidth != nil && originHeight != nil) {
35-
originRect = CGRectMake([originX doubleValue], [originY doubleValue],
36-
[originWidth doubleValue], [originHeight doubleValue]);
49+
if (path.length == 0) {
50+
result([FlutterError errorWithCode:@"error"
51+
message:@"Non-empty path expected"
52+
details:nil]);
53+
return;
3754
}
3855

39-
[self share:shareText
56+
[self shareFile:path
57+
withMimeType:mimeType
58+
withSubject:subject
59+
withText:text
4060
withController:[UIApplication sharedApplication].keyWindow.rootViewController
4161
atSource:originRect];
4262
result(nil);
@@ -46,17 +66,42 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
4666
}];
4767
}
4868

49-
+ (void)share:(id)sharedItems
69+
+ (void)share:(NSArray *)shareItems
5070
withController:(UIViewController *)controller
5171
atSource:(CGRect)origin {
5272
UIActivityViewController *activityViewController =
53-
[[UIActivityViewController alloc] initWithActivityItems:@[ sharedItems ]
54-
applicationActivities:nil];
73+
[[UIActivityViewController alloc] initWithActivityItems:shareItems applicationActivities:nil];
5574
activityViewController.popoverPresentationController.sourceView = controller.view;
5675
if (!CGRectIsEmpty(origin)) {
5776
activityViewController.popoverPresentationController.sourceRect = origin;
5877
}
5978
[controller presentViewController:activityViewController animated:YES completion:nil];
6079
}
6180

81+
+ (void)shareFile:(id)path
82+
withMimeType:(id)mimeType
83+
withSubject:(NSString *)subject
84+
withText:(NSString *)text
85+
withController:(UIViewController *)controller
86+
atSource:(CGRect)origin {
87+
NSMutableArray *items = [[NSMutableArray alloc] init];
88+
89+
if (subject != nil && subject.length != 0) {
90+
[items addObject:subject];
91+
}
92+
if (text != nil && text.length != 0) {
93+
[items addObject:text];
94+
}
95+
96+
if ([mimeType hasPrefix:@"image/"]) {
97+
UIImage *image = [UIImage imageWithContentsOfFile:path];
98+
[items addObject:image];
99+
} else {
100+
NSURL *url = [NSURL fileURLWithPath:path];
101+
[items addObject:url];
102+
}
103+
104+
[self share:items withController:controller atSource:origin];
105+
}
106+
62107
@end

0 commit comments

Comments
 (0)