[share] Add sharing file support (android & ios) (#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>
diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md
index 8c3814d..c4ee830 100644
--- a/packages/share/CHANGELOG.md
+++ b/packages/share/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.6.5
+
+* Added support for sharing files
+
 ## 0.6.4+5
 
 * Update package:e2e -> package:integration_test
diff --git a/packages/share/README.md b/packages/share/README.md
index 14be8da..750fca6 100644
--- a/packages/share/README.md
+++ b/packages/share/README.md
@@ -39,3 +39,9 @@
 ``` 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']);
+```
\ No newline at end of file
diff --git a/packages/share/android/build.gradle b/packages/share/android/build.gradle
index e154b06..7506f4d 100644
--- a/packages/share/android/build.gradle
+++ b/packages/share/android/build.gradle
@@ -31,4 +31,9 @@
     lintOptions {
         disable 'InvalidPackage'
     }
+
+    dependencies {
+        implementation 'androidx.core:core:1.3.1'
+        implementation 'androidx.annotation:annotation:1.1.0'
+    }
 }
diff --git a/packages/share/android/src/main/AndroidManifest.xml b/packages/share/android/src/main/AndroidManifest.xml
index 407eae4..c141a5c 100644
--- a/packages/share/android/src/main/AndroidManifest.xml
+++ b/packages/share/android/src/main/AndroidManifest.xml
@@ -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>
+    </application>
 </manifest>
diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java b/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java
index f7e4d57..02841d3 100644
--- a/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java
+++ b/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java
@@ -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. */
@@ -19,15 +21,37 @@
 
   @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");
     }
   }
 }
diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java b/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java
index 8c9e833..eb856bf 100644
--- a/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java
+++ b/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java
@@ -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;
   }
 
@@ -40,11 +57,177 @@
     shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
     shareIntent.setType("text/plain");
     Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */);
-    if (activity != null) {
-      activity.startActivity(chooserIntent);
+    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 {
-      chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-      activity.startActivity(chooserIntent);
+      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(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 {
+      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();
     }
   }
 }
diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java b/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java
new file mode 100644
index 0000000..87e4e42
--- /dev/null
+++ b/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java
@@ -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 {}
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 fdb9dc4..bd7dfc2 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,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;
@@ -22,12 +23,12 @@
 
   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
@@ -57,9 +58,9 @@
     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);
   }
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 0000000..e68bf91
--- /dev/null
+++ b/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml
@@ -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>
diff --git a/packages/share/example/ios/Runner/Info.plist b/packages/share/example/ios/Runner/Info.plist
index ac44e05..7165610 100644
--- a/packages/share/example/ios/Runner/Info.plist
+++ b/packages/share/example/ios/Runner/Info.plist
@@ -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>
diff --git a/packages/share/example/lib/image_previews.dart b/packages/share/example/lib/image_previews.dart
new file mode 100644
index 0000000..61ecec4
--- /dev/null
+++ b/packages/share/example/lib/image_previews.dart
@@ -0,0 +1,75 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
+/// Widget for displaying a preview of images
+class ImagePreviews extends StatelessWidget {
+  /// The image paths of the displayed images
+  final List<String> imagePaths;
+
+  /// Callback when an image should be removed
+  final Function(int) onDelete;
+
+  /// Creates a widget for preview of images. [imagePaths] can not be empty
+  /// and all contained paths need to be non empty.
+  const ImagePreviews(this.imagePaths, {Key key, this.onDelete})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    if (imagePaths.isEmpty) {
+      return Container();
+    }
+
+    List<Widget> imageWidgets = [];
+    for (int i = 0; i < imagePaths.length; i++) {
+      imageWidgets.add(_ImagePreview(
+        imagePaths[i],
+        onDelete: onDelete != null ? () => onDelete(i) : null,
+      ));
+    }
+
+    return SingleChildScrollView(
+      scrollDirection: Axis.horizontal,
+      child: Row(children: imageWidgets),
+    );
+  }
+}
+
+class _ImagePreview extends StatelessWidget {
+  final String imagePath;
+  final VoidCallback onDelete;
+
+  const _ImagePreview(this.imagePath, {Key key, this.onDelete})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    File imageFile = File(imagePath);
+    return Padding(
+      padding: const EdgeInsets.all(8.0),
+      child: Stack(
+        children: <Widget>[
+          ConstrainedBox(
+            constraints: BoxConstraints(
+              maxWidth: 200,
+              maxHeight: 200,
+            ),
+            child: Image.file(imageFile),
+          ),
+          Positioned(
+            right: 0,
+            child: Padding(
+              padding: const EdgeInsets.all(8.0),
+              child: FloatingActionButton(
+                  backgroundColor: Colors.red,
+                  child: Icon(Icons.delete),
+                  onPressed: onDelete),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart
index b68195c..d6f1a16 100644
--- a/packages/share/example/lib/main.dart
+++ b/packages/share/example/lib/main.dart
@@ -5,8 +5,11 @@
 // ignore_for_file: public_member_api_docs
 
 import 'package:flutter/material.dart';
+import 'package:image_picker/image_picker.dart';
 import 'package:share/share.dart';
 
+import 'image_previews.dart';
+
 void main() {
   runApp(DemoApp());
 }
@@ -19,6 +22,7 @@
 class DemoAppState extends State<DemoApp> {
   String text = '';
   String subject = '';
+  List<String> imagePaths = [];
 
   @override
   Widget build(BuildContext context) {
@@ -28,59 +32,92 @@
           appBar: AppBar(
             title: const Text('Share Plugin Demo'),
           ),
-          body: Padding(
-            padding: const EdgeInsets.all(24.0),
-            child: Column(
-              mainAxisAlignment: MainAxisAlignment.center,
-              children: <Widget>[
-                TextField(
-                  decoration: const InputDecoration(
-                    labelText: 'Share text:',
-                    hintText: 'Enter some text and/or link to share',
+          body: SingleChildScrollView(
+            child: Padding(
+              padding: const EdgeInsets.all(24.0),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: <Widget>[
+                  TextField(
+                    decoration: const InputDecoration(
+                      labelText: 'Share text:',
+                      hintText: 'Enter some text and/or link to share',
+                    ),
+                    maxLines: 2,
+                    onChanged: (String value) => setState(() {
+                      text = value;
+                    }),
                   ),
-                  maxLines: 2,
-                  onChanged: (String value) => setState(() {
-                    text = value;
-                  }),
-                ),
-                TextField(
-                  decoration: const InputDecoration(
-                    labelText: 'Share subject:',
-                    hintText: 'Enter subject to share (optional)',
+                  TextField(
+                    decoration: const InputDecoration(
+                      labelText: 'Share subject:',
+                      hintText: 'Enter subject to share (optional)',
+                    ),
+                    maxLines: 2,
+                    onChanged: (String value) => setState(() {
+                      subject = value;
+                    }),
                   ),
-                  maxLines: 2,
-                  onChanged: (String value) => setState(() {
-                    subject = value;
-                  }),
-                ),
-                const Padding(padding: EdgeInsets.only(top: 24.0)),
-                Builder(
-                  builder: (BuildContext context) {
-                    return RaisedButton(
-                      child: const Text('Share'),
-                      onPressed: text.isEmpty
-                          ? null
-                          : () {
-                              // A builder is used to retrieve the context immediately
-                              // surrounding the RaisedButton.
-                              //
-                              // The context's `findRenderObject` returns the first
-                              // RenderObject in its descendent tree when it's not
-                              // a RenderObjectWidget. The RaisedButton's RenderObject
-                              // has its position and size after it's built.
-                              final RenderBox box = context.findRenderObject();
-                              Share.share(text,
-                                  subject: subject,
-                                  sharePositionOrigin:
-                                      box.localToGlobal(Offset.zero) &
-                                          box.size);
-                            },
-                    );
-                  },
-                ),
-              ],
+                  const Padding(padding: EdgeInsets.only(top: 12.0)),
+                  ImagePreviews(imagePaths, onDelete: _onDeleteImage),
+                  ListTile(
+                    leading: Icon(Icons.add),
+                    title: Text("Add image"),
+                    onTap: () async {
+                      final imagePicker = ImagePicker();
+                      final pickedFile = await imagePicker.getImage(
+                        source: ImageSource.gallery,
+                      );
+                      if (pickedFile != null) {
+                        setState(() {
+                          imagePaths.add(pickedFile.path);
+                        });
+                      }
+                    },
+                  ),
+                  const Padding(padding: EdgeInsets.only(top: 12.0)),
+                  Builder(
+                    builder: (BuildContext context) {
+                      return RaisedButton(
+                        child: const Text('Share'),
+                        onPressed: text.isEmpty && imagePaths.isEmpty
+                            ? null
+                            : () => _onShare(context),
+                      );
+                    },
+                  ),
+                ],
+              ),
             ),
           )),
     );
   }
+
+  _onDeleteImage(int position) {
+    setState(() {
+      imagePaths.removeAt(position);
+    });
+  }
+
+  _onShare(BuildContext context) async {
+    // A builder is used to retrieve the context immediately
+    // surrounding the RaisedButton.
+    //
+    // The context's `findRenderObject` returns the first
+    // RenderObject in its descendent tree when it's not
+    // a RenderObjectWidget. The RaisedButton's RenderObject
+    // has its position and size after it's built.
+    final RenderBox box = context.findRenderObject();
+
+    if (imagePaths.isNotEmpty) {
+      await Share.shareFiles(imagePaths,
+          text: text,
+          subject: subject,
+          sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size);
+    } else {
+      await Share.share(text,
+          subject: subject,
+          sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size);
+    }
+  }
 }
diff --git a/packages/share/example/pubspec.yaml b/packages/share/example/pubspec.yaml
index 4830b71..8b86239 100644
--- a/packages/share/example/pubspec.yaml
+++ b/packages/share/example/pubspec.yaml
@@ -6,6 +6,7 @@
     sdk: flutter
   share:
     path: ../
+  image_picker: ^0.6.7+4
 
 dev_dependencies:
   flutter_driver:
@@ -20,4 +21,3 @@
 environment:
   sdk: ">=2.0.0-dev.28.0 <3.0.0"
   flutter: ">=1.9.1+hotfix.2 <2.0.0"
-
diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m
index 335ba5b..837623a 100644
--- a/packages/share/ios/Classes/FLTSharePlugin.m
+++ b/packages/share/ios/Classes/FLTSharePlugin.m
@@ -10,8 +10,12 @@
 
 @property(readonly, nonatomic, copy) NSString *subject;
 @property(readonly, nonatomic, copy) NSString *text;
+@property(readonly, nonatomic, copy) NSString *path;
+@property(readonly, nonatomic, copy) NSString *mimeType;
 
 - (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text NS_DESIGNATED_INITIALIZER;
+- (instancetype)initWithFile:(NSString *)path
+                    mimeType:(NSString *)mimeType NS_DESIGNATED_INITIALIZER;
 
 - (instancetype)init __attribute__((unavailable("Use initWithSubject:text: instead")));
 
@@ -27,24 +31,62 @@
 - (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text {
   self = [super init];
   if (self) {
-    _subject = subject;
+    _subject = [subject isKindOfClass:NSNull.class] ? @"" : subject;
     _text = text;
   }
   return self;
 }
 
+- (instancetype)initWithFile:(NSString *)path mimeType:(NSString *)mimeType {
+  self = [super init];
+  if (self) {
+    _path = path;
+    _mimeType = mimeType;
+  }
+  return self;
+}
+
 - (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController {
   return @"";
 }
 
 - (id)activityViewController:(UIActivityViewController *)activityViewController
          itemForActivityType:(UIActivityType)activityType {
-  return _text;
+  if (!_path || !_mimeType) {
+    return _text;
+  }
+
+  if ([_mimeType hasPrefix:@"image/"]) {
+    UIImage *image = [UIImage imageWithContentsOfFile:_path];
+    return image;
+  } else {
+    NSURL *url = [NSURL fileURLWithPath:_path];
+    return url;
+  }
 }
 
 - (NSString *)activityViewController:(UIActivityViewController *)activityViewController
               subjectForActivityType:(UIActivityType)activityType {
-  return [_subject isKindOfClass:NSNull.class] ? @"" : _subject;
+  return _subject;
+}
+
+- (UIImage *)activityViewController:(UIActivityViewController *)activityViewController
+      thumbnailImageForActivityType:(UIActivityType)activityType
+                      suggestedSize:(CGSize)suggestedSize {
+  if (!_path || !_mimeType || ![_mimeType hasPrefix:@"image/"]) {
+    return nil;
+  }
+
+  UIImage *image = [UIImage imageWithContentsOfFile:_path];
+  return [self imageWithImage:image scaledToSize:suggestedSize];
+}
+
+- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize {
+  UIGraphicsBeginImageContext(newSize);
+  [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
+  UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
+  UIGraphicsEndImageContext();
+  return newImage;
 }
 
 @end
@@ -57,8 +99,19 @@
                                   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 = CGRectZero;
+    if (originX && originY && originWidth && originHeight) {
+      originRect = CGRectMake([originX doubleValue], [originY doubleValue],
+                              [originWidth doubleValue], [originHeight doubleValue]);
+    }
+
     if ([@"share" isEqualToString:call.method]) {
-      NSDictionary *arguments = [call arguments];
       NSString *shareText = arguments[@"text"];
       NSString *shareSubject = arguments[@"subject"];
 
@@ -69,19 +122,37 @@
         return;
       }
 
-      NSNumber *originX = arguments[@"originX"];
-      NSNumber *originY = arguments[@"originY"];
-      NSNumber *originWidth = arguments[@"originWidth"];
-      NSNumber *originHeight = arguments[@"originHeight"];
+      [self shareText:shareText
+                 subject:shareSubject
+          withController:[UIApplication sharedApplication].keyWindow.rootViewController
+                atSource:originRect];
+      result(nil);
+    } else if ([@"shareFiles" isEqualToString:call.method]) {
+      NSArray *paths = arguments[@"paths"];
+      NSArray *mimeTypes = arguments[@"mimeTypes"];
+      NSString *subject = arguments[@"subject"];
+      NSString *text = arguments[@"text"];
 
-      CGRect originRect = CGRectZero;
-      if (originX != nil && originY != nil && originWidth != nil && originHeight != nil) {
-        originRect = CGRectMake([originX doubleValue], [originY doubleValue],
-                                [originWidth doubleValue], [originHeight doubleValue]);
+      if (paths.count == 0) {
+        result([FlutterError errorWithCode:@"error"
+                                   message:@"Non-empty paths expected"
+                                   details:nil]);
+        return;
       }
 
-      [self share:shareText
-                 subject:shareSubject
+      for (NSString *path in paths) {
+        if (path.length == 0) {
+          result([FlutterError errorWithCode:@"error"
+                                     message:@"Each path must not be empty"
+                                     details:nil]);
+          return;
+        }
+      }
+
+      [self shareFiles:paths
+            withMimeType:mimeTypes
+             withSubject:subject
+                withText:text
           withController:[UIApplication sharedApplication].keyWindow.rootViewController
                 atSource:originRect];
       result(nil);
@@ -91,13 +162,11 @@
   }];
 }
 
-+ (void)share:(NSString *)shareText
-           subject:(NSString *)subject
++ (void)share:(NSArray *)shareItems
     withController:(UIViewController *)controller
           atSource:(CGRect)origin {
-  ShareData *data = [[ShareData alloc] initWithSubject:subject text:shareText];
   UIActivityViewController *activityViewController =
-      [[UIActivityViewController alloc] initWithActivityItems:@[ data ] applicationActivities:nil];
+      [[UIActivityViewController alloc] initWithActivityItems:shareItems applicationActivities:nil];
   activityViewController.popoverPresentationController.sourceView = controller.view;
   if (!CGRectIsEmpty(origin)) {
     activityViewController.popoverPresentationController.sourceRect = origin;
@@ -105,4 +174,44 @@
   [controller presentViewController:activityViewController animated:YES completion:nil];
 }
 
++ (void)shareText:(NSString *)shareText
+           subject:(NSString *)subject
+    withController:(UIViewController *)controller
+          atSource:(CGRect)origin {
+  ShareData *data = [[ShareData alloc] initWithSubject:subject text:shareText];
+  [self share:@[ data ] withController:controller atSource:origin];
+}
+
++ (void)shareFiles:(NSArray *)paths
+      withMimeType:(NSArray *)mimeTypes
+       withSubject:(NSString *)subject
+          withText:(NSString *)text
+    withController:(UIViewController *)controller
+          atSource:(CGRect)origin {
+  NSMutableArray *items = [[NSMutableArray alloc] init];
+
+  if (text || subject) {
+    [items addObject:[[ShareData alloc] initWithSubject:subject text:text]];
+  }
+
+  for (int i = 0; i < [paths count]; i++) {
+    NSString *path = paths[i];
+    NSString *pathExtension = [path pathExtension];
+    NSString *mimeType = mimeTypes[i];
+    if ([pathExtension.lowercaseString isEqualToString:@"jpg"] ||
+        [pathExtension.lowercaseString isEqualToString:@"jpeg"] ||
+        [pathExtension.lowercaseString isEqualToString:@"png"] ||
+        [mimeType.lowercaseString isEqualToString:@"image/jpg"] ||
+        [mimeType.lowercaseString isEqualToString:@"image/jpeg"] ||
+        [mimeType.lowercaseString isEqualToString:@"image/png"]) {
+      UIImage *image = [UIImage imageWithContentsOfFile:path];
+      [items addObject:image];
+    } else {
+      [items addObject:[[ShareData alloc] initWithFile:path mimeType:mimeType]];
+    }
+  }
+
+  [self share:items withController:controller atSource:origin];
+}
+
 @end
diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart
index ff20d19..4a3ff6f 100644
--- a/packages/share/lib/share.dart
+++ b/packages/share/lib/share.dart
@@ -7,6 +7,7 @@
 
 import 'package:flutter/services.dart';
 import 'package:meta/meta.dart' show visibleForTesting;
+import 'package:mime/mime.dart' show lookupMimeType;
 
 /// Plugin for summoning a platform share sheet.
 class Share {
@@ -51,4 +52,50 @@
 
     return channel.invokeMethod<void>('share', params);
   }
+
+  /// Summons the platform's share sheet to share multiple files.
+  ///
+  /// 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<void> shareFiles(
+    List<String> paths, {
+    List<String> mimeTypes,
+    String subject,
+    String text,
+    Rect sharePositionOrigin,
+  }) {
+    assert(paths != null);
+    assert(paths.isNotEmpty);
+    assert(paths.every((element) => element != null && element.isNotEmpty));
+    final Map<String, dynamic> params = <String, dynamic>{
+      'paths': paths,
+      'mimeTypes': mimeTypes ??
+          paths.map((String path) => _mimeTypeForPath(path)).toList(),
+    };
+
+    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('shareFiles', params);
+  }
+
+  static String _mimeTypeForPath(String path) {
+    assert(path != null);
+    return lookupMimeType(path) ?? 'application/octet-stream';
+  }
 }
diff --git a/packages/share/pubspec.yaml b/packages/share/pubspec.yaml
index f5e545c..918087b 100644
--- a/packages/share/pubspec.yaml
+++ b/packages/share/pubspec.yaml
@@ -5,7 +5,7 @@
 # 0.6.y+z is compatible with 1.0.0, if you land a breaking change bump
 # the version to 2.0.0.
 # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0
-version: 0.6.4+5
+version: 0.6.5
 
 flutter:
   plugin:
@@ -18,6 +18,7 @@
 
 dependencies:
   meta: ^1.0.5
+  mime: ^0.9.7
   flutter:
     sdk: flutter
 
diff --git a/packages/share/test/share_test.dart b/packages/share/test/share_test.dart
index c03f8fb..e862d1b 100644
--- a/packages/share/test/share_test.dart
+++ b/packages/share/test/share_test.dart
@@ -2,6 +2,7 @@
 // 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_test/flutter_test.dart' show TestWidgetsFlutterBinding;
@@ -56,6 +57,52 @@
       'originHeight': 4.0,
     }));
   });
+
+  test('sharing null file fails', () {
+    expect(
+      () => Share.shareFiles([null]),
+      throwsA(const TypeMatcher<AssertionError>()),
+    );
+    verifyZeroInteractions(mockChannel);
+  });
+
+  test('sharing empty file fails', () {
+    expect(
+      () => Share.shareFiles(['']),
+      throwsA(const TypeMatcher<AssertionError>()),
+    );
+    verifyZeroInteractions(mockChannel);
+  });
+
+  test('sharing file sets correct mimeType', () async {
+    final String path = 'tempfile-83649a.png';
+    final File file = File(path);
+    try {
+      file.createSync();
+      await Share.shareFiles([path]);
+      verify(mockChannel.invokeMethod('shareFiles', <String, dynamic>{
+        'paths': [path],
+        'mimeTypes': ['image/png'],
+      }));
+    } finally {
+      file.deleteSync();
+    }
+  });
+
+  test('sharing file sets passed mimeType', () async {
+    final String path = 'tempfile-83649a.png';
+    final File file = File(path);
+    try {
+      file.createSync();
+      await Share.shareFiles([path], mimeTypes: ['*/*']);
+      verify(mockChannel.invokeMethod('shareFiles', <String, dynamic>{
+        'paths': [file.path],
+        'mimeTypes': ['*/*'],
+      }));
+    } finally {
+      file.deleteSync();
+    }
+  });
 }
 
 class MockMethodChannel extends Mock implements MethodChannel {}