Skip to content

Commit c5e1eb6

Browse files
pboosMaskySaloisdenielcms103
authored andcommitted
[share] Add sharing file support (android & ios) (flutter#970)
Co-authored-by: Kifah Meeran <23234883+MaskyS@users.noreply.github.com> Co-authored-by: Aloïs Deniel <alois.deniel@gmail.com> Co-authored-by: Colin Stewart <colin@owlfish.com>
1 parent 63ca66b commit c5e1eb6

File tree

17 files changed

+665
-89
lines changed

17 files changed

+665
-89
lines changed

packages/share/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.6.5
2+
3+
* Added support for sharing files
4+
15
## 0.6.4+5
26

37
* Update package:e2e -> package:integration_test

packages/share/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,9 @@ sharing to email.
3939
``` dart
4040
Share.share('check out my website https://example.com', subject: 'Look what I made!');
4141
```
42+
43+
To share one or multiple files invoke the static `shareFiles` method anywhere in your Dart code. Optionally you can also pass in `text` and `subject`.
44+
``` dart
45+
Share.shareFiles(['${directory.path}/image.jpg'], text: 'Great picture');
46+
Share.shareFiles(['${directory.path}/image1.jpg', '${directory.path}/image2.jpg']);
47+
```

packages/share/android/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ android {
3131
lintOptions {
3232
disable 'InvalidPackage'
3333
}
34+
35+
dependencies {
36+
implementation 'androidx.core:core:1.3.1'
37+
implementation 'androidx.annotation:annotation:1.1.0'
38+
}
3439
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
22
package="io.flutter.plugins.share">
3+
<application>
4+
<provider
5+
android:name="io.flutter.plugins.share.ShareFileProvider"
6+
android:authorities="${applicationId}.flutter.share_provider"
7+
android:exported="false"
8+
android:grantUriPermissions="true">
9+
<meta-data
10+
android:name="android.support.FILE_PROVIDER_PATHS"
11+
android:resource="@xml/flutter_share_file_paths"/>
12+
</provider>
13+
</application>
314
</manifest>

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

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

77
import io.flutter.plugin.common.MethodCall;
88
import io.flutter.plugin.common.MethodChannel;
9+
import java.io.*;
10+
import java.util.List;
911
import java.util.Map;
1012

1113
/** Handles the method calls for the plugin. */
@@ -19,15 +21,37 @@ class MethodCallHandler implements MethodChannel.MethodCallHandler {
1921

2022
@Override
2123
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
22-
if (call.method.equals("share")) {
23-
if (!(call.arguments instanceof Map)) {
24-
throw new IllegalArgumentException("Map argument expected");
25-
}
26-
// Android does not support showing the share sheet at a particular point on screen.
27-
share.share((String) call.argument("text"), (String) call.argument("subject"));
28-
result.success(null);
29-
} else {
30-
result.notImplemented();
24+
switch (call.method) {
25+
case "share":
26+
expectMapArguments(call);
27+
// Android does not support showing the share sheet at a particular point on screen.
28+
share.share((String) call.argument("text"), (String) call.argument("subject"));
29+
result.success(null);
30+
break;
31+
case "shareFiles":
32+
expectMapArguments(call);
33+
34+
// Android does not support showing the share sheet at a particular point on screen.
35+
try {
36+
share.shareFiles(
37+
(List<String>) call.argument("paths"),
38+
(List<String>) call.argument("mimeTypes"),
39+
(String) call.argument("text"),
40+
(String) call.argument("subject"));
41+
result.success(null);
42+
} catch (IOException e) {
43+
result.error(e.getMessage(), null, null);
44+
}
45+
break;
46+
default:
47+
result.notImplemented();
48+
break;
49+
}
50+
}
51+
52+
private void expectMapArguments(MethodCall call) throws IllegalArgumentException {
53+
if (!(call.arguments instanceof Map)) {
54+
throw new IllegalArgumentException("Map argument expected");
3155
}
3256
}
3357
}

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

Lines changed: 190 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,36 @@
55
package io.flutter.plugins.share;
66

77
import android.app.Activity;
8+
import android.content.Context;
89
import android.content.Intent;
10+
import android.content.pm.PackageManager;
11+
import android.content.pm.ResolveInfo;
12+
import android.net.Uri;
13+
import android.os.Environment;
14+
import androidx.annotation.NonNull;
15+
import androidx.core.content.FileProvider;
16+
import java.io.File;
17+
import java.io.FileInputStream;
18+
import java.io.FileOutputStream;
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.OutputStream;
22+
import java.util.ArrayList;
23+
import java.util.List;
924

1025
/** Handles share intent. */
1126
class Share {
1227

28+
private Context context;
1329
private Activity activity;
1430

1531
/**
16-
* Constructs a Share object. The {@code activity} is used to start the share intent. It might be
17-
* null when constructing the {@link Share} object and set to non-null when an activity is
18-
* available using {@link #setActivity(Activity)}.
32+
* Constructs a Share object. The {@code context} and {@code activity} are used to start the share
33+
* intent. The {@code activity} might be null when constructing the {@link Share} object and set
34+
* to non-null when an activity is available using {@link #setActivity(Activity)}.
1935
*/
20-
Share(Activity activity) {
36+
Share(Context context, Activity activity) {
37+
this.context = context;
2138
this.activity = activity;
2239
}
2340

@@ -40,11 +57,177 @@ void share(String text, String subject) {
4057
shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
4158
shareIntent.setType("text/plain");
4259
Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */);
60+
startActivity(chooserIntent);
61+
}
62+
63+
void shareFiles(List<String> paths, List<String> mimeTypes, String text, String subject)
64+
throws IOException {
65+
if (paths == null || paths.isEmpty()) {
66+
throw new IllegalArgumentException("Non-empty path expected");
67+
}
68+
69+
clearExternalShareFolder();
70+
ArrayList<Uri> fileUris = getUrisForPaths(paths);
71+
72+
Intent shareIntent = new Intent();
73+
if (fileUris.isEmpty()) {
74+
share(text, subject);
75+
return;
76+
} else if (fileUris.size() == 1) {
77+
shareIntent.setAction(Intent.ACTION_SEND);
78+
shareIntent.putExtra(Intent.EXTRA_STREAM, fileUris.get(0));
79+
shareIntent.setType(
80+
!mimeTypes.isEmpty() && mimeTypes.get(0) != null ? mimeTypes.get(0) : "*/*");
81+
} else {
82+
shareIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
83+
shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, fileUris);
84+
shareIntent.setType(reduceMimeTypes(mimeTypes));
85+
}
86+
if (text != null) shareIntent.putExtra(Intent.EXTRA_TEXT, text);
87+
if (subject != null) shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
88+
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
89+
Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */);
90+
91+
List<ResolveInfo> resInfoList =
92+
getContext()
93+
.getPackageManager()
94+
.queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY);
95+
for (ResolveInfo resolveInfo : resInfoList) {
96+
String packageName = resolveInfo.activityInfo.packageName;
97+
for (Uri fileUri : fileUris) {
98+
getContext()
99+
.grantUriPermission(
100+
packageName,
101+
fileUri,
102+
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
103+
}
104+
}
105+
106+
startActivity(chooserIntent);
107+
}
108+
109+
private void startActivity(Intent intent) {
43110
if (activity != null) {
44-
activity.startActivity(chooserIntent);
111+
activity.startActivity(intent);
112+
} else if (context != null) {
113+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
114+
context.startActivity(intent);
115+
} else {
116+
throw new IllegalStateException("Both context and activity are null");
117+
}
118+
}
119+
120+
private ArrayList<Uri> getUrisForPaths(List<String> paths) throws IOException {
121+
ArrayList<Uri> uris = new ArrayList<>(paths.size());
122+
for (String path : paths) {
123+
File file = new File(path);
124+
if (!fileIsOnExternal(file)) {
125+
file = copyToExternalShareFolder(file);
126+
}
127+
128+
uris.add(
129+
FileProvider.getUriForFile(
130+
getContext(), getContext().getPackageName() + ".flutter.share_provider", file));
131+
}
132+
return uris;
133+
}
134+
135+
private String reduceMimeTypes(List<String> mimeTypes) {
136+
if (mimeTypes.size() > 1) {
137+
String reducedMimeType = mimeTypes.get(0);
138+
for (int i = 1; i < mimeTypes.size(); i++) {
139+
String mimeType = mimeTypes.get(i);
140+
if (!reducedMimeType.equals(mimeType)) {
141+
if (getMimeTypeBase(mimeType).equals(getMimeTypeBase(reducedMimeType))) {
142+
reducedMimeType = getMimeTypeBase(mimeType) + "/*";
143+
} else {
144+
reducedMimeType = "*/*";
145+
break;
146+
}
147+
}
148+
}
149+
return reducedMimeType;
150+
} else if (mimeTypes.size() == 1) {
151+
return mimeTypes.get(0);
45152
} else {
46-
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
47-
activity.startActivity(chooserIntent);
153+
return "*/*";
154+
}
155+
}
156+
157+
@NonNull
158+
private String getMimeTypeBase(String mimeType) {
159+
if (mimeType == null || !mimeType.contains("/")) {
160+
return "*";
161+
}
162+
163+
return mimeType.substring(0, mimeType.indexOf("/"));
164+
}
165+
166+
private boolean fileIsOnExternal(File file) {
167+
try {
168+
String filePath = file.getCanonicalPath();
169+
File externalDir = Environment.getExternalStorageDirectory();
170+
return externalDir != null && filePath.startsWith(externalDir.getCanonicalPath());
171+
} catch (IOException e) {
172+
return false;
173+
}
174+
}
175+
176+
@SuppressWarnings("ResultOfMethodCallIgnored")
177+
private void clearExternalShareFolder() {
178+
File folder = getExternalShareFolder();
179+
if (folder.exists()) {
180+
for (File file : folder.listFiles()) {
181+
file.delete();
182+
}
183+
folder.delete();
184+
}
185+
}
186+
187+
@SuppressWarnings("ResultOfMethodCallIgnored")
188+
private File copyToExternalShareFolder(File file) throws IOException {
189+
File folder = getExternalShareFolder();
190+
if (!folder.exists()) {
191+
folder.mkdirs();
192+
}
193+
194+
File newFile = new File(folder, file.getName());
195+
copy(file, newFile);
196+
return newFile;
197+
}
198+
199+
@NonNull
200+
private File getExternalShareFolder() {
201+
return new File(getContext().getExternalCacheDir(), "share");
202+
}
203+
204+
private Context getContext() {
205+
if (activity != null) {
206+
return activity;
207+
}
208+
if (context != null) {
209+
return context;
210+
}
211+
212+
throw new IllegalStateException("Both context and activity are null");
213+
}
214+
215+
private static void copy(File src, File dst) throws IOException {
216+
InputStream in = new FileInputStream(src);
217+
try {
218+
OutputStream out = new FileOutputStream(dst);
219+
try {
220+
// Transfer bytes from in to out
221+
byte[] buf = new byte[1024];
222+
int len;
223+
while ((len = in.read(buf)) > 0) {
224+
out.write(buf, 0, len);
225+
}
226+
} finally {
227+
out.close();
228+
}
229+
} finally {
230+
in.close();
48231
}
49232
}
50233
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright 2019 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.share;
6+
7+
import androidx.core.content.FileProvider;
8+
9+
/**
10+
* Providing a custom {@code FileProvider} prevents manifest {@code <provider>} name collisions.
11+
*
12+
* <p>See https://developer.android.com/guide/topics/manifest/provider-element.html for details.
13+
*/
14+
public class ShareFileProvider extends FileProvider {}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package io.flutter.plugins.share;
66

77
import android.app.Activity;
8+
import android.content.Context;
89
import io.flutter.embedding.engine.plugins.FlutterPlugin;
910
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
1011
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
@@ -22,12 +23,12 @@ public class SharePlugin implements FlutterPlugin, ActivityAware {
2223

2324
public static void registerWith(Registrar registrar) {
2425
SharePlugin plugin = new SharePlugin();
25-
plugin.setUpChannel(registrar.activity(), registrar.messenger());
26+
plugin.setUpChannel(registrar.context(), registrar.activity(), registrar.messenger());
2627
}
2728

2829
@Override
2930
public void onAttachedToEngine(FlutterPluginBinding binding) {
30-
setUpChannel(null, binding.getBinaryMessenger());
31+
setUpChannel(binding.getApplicationContext(), null, binding.getBinaryMessenger());
3132
}
3233

3334
@Override
@@ -57,9 +58,9 @@ public void onDetachedFromActivityForConfigChanges() {
5758
onDetachedFromActivity();
5859
}
5960

60-
private void setUpChannel(Activity activity, BinaryMessenger messenger) {
61+
private void setUpChannel(Context context, Activity activity, BinaryMessenger messenger) {
6162
methodChannel = new MethodChannel(messenger, CHANNEL);
62-
share = new Share(activity);
63+
share = new Share(context, activity);
6364
handler = new MethodCallHandler(share);
6465
methodChannel.setMethodCallHandler(handler);
6566
}
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/example/ios/Runner/Info.plist

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,11 @@
4545
</array>
4646
<key>UIViewControllerBasedStatusBarAppearance</key>
4747
<false/>
48+
<key>NSPhotoLibraryUsageDescription</key>
49+
<string>This app requires access to the photo library for sharing images.</string>
50+
<key>NSMicrophoneUsageDescription</key>
51+
<string>This app does not require access to the microphone for sharing images.</string>
52+
<key>NSCameraUsageDescription</key>
53+
<string>This app requires access to the camera for sharing images.</string>
4854
</dict>
4955
</plist>

0 commit comments

Comments
 (0)