Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
6933930
[share] Update example to use v2 plugins
pboos Jan 2, 2020
f84efb5
[share] add file share support
pboos Jan 2, 2020
7d9eb8a
[share] Fix: When sharing file, as well share the text
pboos Jan 3, 2020
e0def31
[share] Fix: async requirements
pboos Jan 3, 2020
9abd37e
[share] Fix formatting & async
pboos Jan 3, 2020
697c89b
[share] Fix more formatting
pboos Jan 3, 2020
66766d0
[share] Fix problems with nextMajor check by removing version from ex…
pboos Jan 3, 2020
589c83e
[share] Update documentation formatting
pboos Jan 15, 2020
a82ee3d
[share] example: Revert to using v1 of the plugin
pboos Jan 15, 2020
e3685a2
[share] Update example on shareFile to not share text and file (which…
pboos Jan 16, 2020
696453f
[share] Update example with image_picker
pboos Jan 16, 2020
9065310
[share] Add support to share multiple files
pboos Jan 16, 2020
333bad7
[share] Update tests
pboos Jan 16, 2020
c7739d7
[share] Fix formatting
pboos Jan 16, 2020
8c9f55f
Fix Permission Denial error
MaskyS Mar 18, 2020
0fb2f9a
Merge pull request #1 from MaskyS/patch-1
pboos Mar 18, 2020
2b27e0c
Fix formatting
pboos Mar 18, 2020
e2cdec1
[share] Allowing shared images on iOS to have specific actions (save …
aloisdeniel Apr 23, 2020
4ecd3c3
Resolving conflict with open_file
cms103 May 1, 2020
9c1518a
Added missing namespace.
cms103 May 1, 2020
bab3a2b
More attempts to fix Manifest
cms103 May 1, 2020
57777e9
Created custom provider to avoid name conflicts.
cms103 May 1, 2020
52e8b30
Updated to use the custom provider.
cms103 May 1, 2020
64c77cd
Remove unneeded edits.
cms103 May 1, 2020
3ad3351
Updated as per the contribution guidelines.
cms103 May 2, 2020
4e8a17e
Merge pull request #2 from aloisdeniel/feature/shareFile
pboos Jun 16, 2020
d886085
Merge pull request #3 from cms103/feature/shareFile
pboos Jun 16, 2020
2804c23
Merge branch 'master' into feature/shareFile
pboos Aug 19, 2020
86e746d
[share] Update dependencies
pboos Aug 19, 2020
fd23afd
[share] Fix formatting
pboos Aug 19, 2020
9494310
[share] Update deprecated code in example
pboos Aug 19, 2020
1b05256
[share] Fix uninitialized variable
pboos Aug 19, 2020
2a9da4b
[share] use mime package for mime type lookup
pboos Aug 20, 2020
4567fdb
[share] Return `application/octet-stream` in case mime returns `null`
pboos Aug 20, 2020
b8fb036
[share] Remove library dependency on `dart:io` to allow future suppor…
pboos Aug 20, 2020
bebd768
Merge branch 'master' into feature/shareFile
pboos Aug 20, 2020
e92ecc3
[share] Update version to 0.6.5
pboos Aug 21, 2020
16c9b25
[share] Formatting
pboos Aug 21, 2020
a426f93
[share] [BUILD]
pboos Aug 21, 2020
c4de8e8
Merge branch 'master' into feature/shareFile
pboos Aug 25, 2020
28b6de5
[share] Remove `shareFile()` as it does not more than `shareFiles()`
pboos Aug 26, 2020
271cf5e
[share] example: wrap within SingleChildScrollView
pboos Aug 26, 2020
dd414d8
[share] example: Extract methods and widgets
pboos Aug 26, 2020
4b3b6f8
[share] Formatting
pboos Aug 26, 2020
b77cc0f
[share] Formatting
pboos Aug 26, 2020
14b9338
[share] Update tests
pboos Aug 26, 2020
88e5723
[share] example: Add public member api docs
pboos Aug 26, 2020
9f1f775
[share] android: handle empty fileUris list
pboos Aug 27, 2020
d93dec4
[share] android: Handle activity == null
pboos Aug 27, 2020
2403c5a
[share] android: do `grantUriPermission` for each file
pboos Aug 27, 2020
6592125
[share] android: Fix wrongly callled method (part of activity == null…
pboos Aug 27, 2020
33b1aa1
[share] Various small improvements from PR review
pboos Aug 27, 2020
6bc2f6b
[share] android: Improve calls where activity could be null to use co…
pboos Aug 27, 2020
f00cf55
[share] example: allow sharing of only images
pboos Aug 27, 2020
7ea12d6
[share] ios: correct error message
pboos Aug 27, 2020
12aee1f
[share] android: formatting
pboos Aug 27, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/share/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.5

* Added support for sharing files

## 0.6.4+5

* Update package:e2e -> package:integration_test
Expand Down
6 changes: 6 additions & 0 deletions packages/share/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ sharing to email.
``` dart
Share.share('check out my website https://example.com', subject: 'Look what I made!');
```

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`.
``` dart
Share.shareFiles(['${directory.path}/image.jpg'], text: 'Great picture');
Share.shareFiles(['${directory.path}/image1.jpg', '${directory.path}/image2.jpg']);
```
5 changes: 5 additions & 0 deletions packages/share/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ android {
lintOptions {
disable 'InvalidPackage'
}

dependencies {
implementation 'androidx.core:core:1.3.1'
implementation 'androidx.annotation:annotation:1.1.0'
}
}
11 changes: 11 additions & 0 deletions packages/share/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.flutter.plugins.share">
<application>
<provider
android:name="io.flutter.plugins.share.ShareFileProvider"
android:authorities="${applicationId}.flutter.share_provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/flutter_share_file_paths"/>
</provider>
Comment on lines +4 to +12
Copy link

@Nimrodda Nimrodda Aug 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if merge rule markers can be of use here instead of introducing another provider 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. This was as well what I hated most in this plugin. But wonder what other provider to merge it with and if meta-data could make problems if merged with another. Specially if the other package as well uses FILE_PROVIDER_PATHS with a different config. Basically I know too little about how to handle this here. If anyone has more knowledge and can tell me how (or even create a PR on to update my feature branch) that would be great :)

</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import java.io.*;
import java.util.List;
import java.util.Map;

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

@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.share((String) call.argument("text"), (String) call.argument("subject"));
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.share((String) call.argument("text"), (String) call.argument("subject"));
result.success(null);
break;
case "shareFiles":
expectMapArguments(call);

// Android does not support showing the share sheet at a particular point on screen.
try {
share.shareFiles(
(List<String>) call.argument("paths"),
(List<String>) call.argument("mimeTypes"),
(String) call.argument("text"),
(String) call.argument("subject"));
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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,36 @@
package io.flutter.plugins.share;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

/** Handles share intent. */
class Share {

private Context context;
private Activity activity;

/**
* Constructs a Share object. The {@code activity} is used to start the share intent. It might be
* null when constructing the {@link Share} object and set to non-null when an activity is
* available using {@link #setActivity(Activity)}.
* Constructs a Share object. The {@code context} and {@code activity} are used to start the share
* intent. The {@code activity} might be null when constructing the {@link Share} object and set
* to non-null when an activity is available using {@link #setActivity(Activity)}.
*/
Share(Activity activity) {
Share(Context context, Activity activity) {
this.context = context;
this.activity = activity;
}

Expand All @@ -40,11 +57,177 @@ void share(String text, String subject) {
shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
shareIntent.setType("text/plain");
Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */);
startActivity(chooserIntent);
}

void shareFiles(List<String> paths, List<String> mimeTypes, String text, String subject)
throws IOException {
if (paths == null || paths.isEmpty()) {
throw new IllegalArgumentException("Non-empty path expected");
}

clearExternalShareFolder();
ArrayList<Uri> fileUris = getUrisForPaths(paths);

Intent shareIntent = new Intent();
if (fileUris.isEmpty()) {
share(text, subject);
return;
} else if (fileUris.size() == 1) {
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, fileUris.get(0));
shareIntent.setType(
!mimeTypes.isEmpty() && mimeTypes.get(0) != null ? mimeTypes.get(0) : "*/*");
} else {
shareIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, fileUris);
shareIntent.setType(reduceMimeTypes(mimeTypes));
}
if (text != null) shareIntent.putExtra(Intent.EXTRA_TEXT, text);
if (subject != null) shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */);

List<ResolveInfo> resInfoList =
getContext()
.getPackageManager()
.queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
for (Uri fileUri : fileUris) {
getContext()
.grantUriPermission(
packageName,
fileUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}

startActivity(chooserIntent);
}

private void startActivity(Intent intent) {
if (activity != null) {
activity.startActivity(chooserIntent);
activity.startActivity(intent);
} else if (context != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
} else {
throw new IllegalStateException("Both context and activity are null");
}
}

private ArrayList<Uri> getUrisForPaths(List<String> paths) throws IOException {
ArrayList<Uri> uris = new ArrayList<>(paths.size());
for (String path : paths) {
File file = new File(path);
if (!fileIsOnExternal(file)) {
file = copyToExternalShareFolder(file);
}

uris.add(
FileProvider.getUriForFile(
getContext(), getContext().getPackageName() + ".flutter.share_provider", file));
}
return uris;
}

private String reduceMimeTypes(List<String> mimeTypes) {
if (mimeTypes.size() > 1) {
String reducedMimeType = mimeTypes.get(0);
for (int i = 1; i < mimeTypes.size(); i++) {
String mimeType = mimeTypes.get(i);
if (!reducedMimeType.equals(mimeType)) {
if (getMimeTypeBase(mimeType).equals(getMimeTypeBase(reducedMimeType))) {
reducedMimeType = getMimeTypeBase(mimeType) + "/*";
} else {
reducedMimeType = "*/*";
break;
}
}
}
return reducedMimeType;
} else if (mimeTypes.size() == 1) {
return mimeTypes.get(0);
} else {
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(chooserIntent);
return "*/*";
}
}

@NonNull
private String getMimeTypeBase(String mimeType) {
if (mimeType == null || !mimeType.contains("/")) {
return "*";
}

return mimeType.substring(0, mimeType.indexOf("/"));
}

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(getContext().getExternalCacheDir(), "share");
}

private Context getContext() {
if (activity != null) {
return activity;
}
if (context != null) {
return context;
}

throw new IllegalStateException("Both context and activity are null");
}

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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugins.share;

import androidx.core.content.FileProvider;

/**
* Providing a custom {@code FileProvider} prevents manifest {@code <provider>} name collisions.
*
* <p>See https://developer.android.com/guide/topics/manifest/provider-element.html for details.
*/
public class ShareFileProvider extends FileProvider {}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package io.flutter.plugins.share;

import android.app.Activity;
import android.content.Context;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
Expand All @@ -22,12 +23,12 @@ public class SharePlugin implements FlutterPlugin, ActivityAware {

public static void registerWith(Registrar registrar) {
SharePlugin plugin = new SharePlugin();
plugin.setUpChannel(registrar.activity(), registrar.messenger());
plugin.setUpChannel(registrar.context(), registrar.activity(), registrar.messenger());
}

@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
setUpChannel(null, binding.getBinaryMessenger());
setUpChannel(binding.getApplicationContext(), null, binding.getBinaryMessenger());
}

@Override
Expand Down Expand Up @@ -57,9 +58,9 @@ public void onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity();
}

private void setUpChannel(Activity activity, BinaryMessenger messenger) {
private void setUpChannel(Context context, Activity activity, BinaryMessenger messenger) {
methodChannel = new MethodChannel(messenger, CHANNEL);
share = new Share(activity);
share = new Share(context, activity);
handler = new MethodCallHandler(share);
methodChannel.setMethodCallHandler(handler);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external" path="."/>
<external-files-path name="external_files" path="."/>
<external-cache-path name="external_cache" path="."/>
</paths>
6 changes: 6 additions & 0 deletions packages/share/example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,11 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to the photo library for sharing images.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app does not require access to the microphone for sharing images.</string>
<key>NSCameraUsageDescription</key>
<string>This app requires access to the camera for sharing images.</string>
</dict>
</plist>
Loading