From 69339300da877e290a0afc74d31ec4574cc309bf Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 2 Jan 2020 13:27:05 +0100 Subject: [PATCH 01/50] [share] Update example to use v2 plugins --- .../android/app/src/main/AndroidManifest.xml | 17 +++++++---------- .../shareexample/EmbeddingV1Activity.java | 18 ------------------ .../shareexample/EmbeddingV1ActivityTest.java | 13 ------------- .../plugins/shareexample/MainActivity.java | 19 ------------------- .../shareexample/MainActivityTest.java | 15 --------------- 5 files changed, 7 insertions(+), 75 deletions(-) delete mode 100644 packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java delete mode 100644 packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java delete mode 100644 packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivityTest.java diff --git a/packages/share/example/android/app/src/main/AndroidManifest.xml b/packages/share/example/android/app/src/main/AndroidManifest.xml index d5e5ec8bf39d..50a8c3c9f052 100644 --- a/packages/share/example/android/app/src/main/AndroidManifest.xml +++ b/packages/share/example/android/app/src/main/AndroidManifest.xml @@ -3,17 +3,10 @@ - - - - + @@ -21,5 +14,9 @@ + + diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java deleted file mode 100644 index 736ac546c55a..000000000000 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2017 The Chromium 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.shareexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index 958541165806..000000000000 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.shareexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java deleted file mode 100644 index 3717feb8ca7e..000000000000 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java +++ /dev/null @@ -1,19 +0,0 @@ -// 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.shareexample; - -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.share.SharePlugin; - -public class MainActivity extends FlutterActivity { - // TODO(cyanglaz): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. - // https://github.com/flutter/flutter/issues/42694 - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - super.configureFlutterEngine(flutterEngine); - flutterEngine.getPlugins().add(new SharePlugin()); - } -} diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivityTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivityTest.java deleted file mode 100644 index fcd936a7dd0f..000000000000 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivityTest.java +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2019 The Chromium 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.shareexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); -} From f84efb5a396db5dd438f84202209b3349b82765a Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 2 Jan 2020 15:13:48 +0100 Subject: [PATCH 02/50] [share] add file share support --- packages/share/README.md | 5 + packages/share/android/build.gradle | 5 + .../gradle/wrapper/gradle-wrapper.properties | 6 ++ .../android/src/main/AndroidManifest.xml | 11 ++ .../plugins/share/MethodCallHandler.java | 40 +++++-- .../java/io/flutter/plugins/share/Share.java | 102 ++++++++++++++++++ .../main/res/xml/flutter_share_file_paths.xml | 6 ++ packages/share/example/lib/main.dart | 71 ++++++++++-- packages/share/example/pubspec.yaml | 4 +- packages/share/ios/Classes/FLTSharePlugin.m | 86 ++++++++++++--- packages/share/lib/share.dart | 90 ++++++++++++++++ packages/share/test/share_test.dart | 53 +++++++++ 12 files changed, 445 insertions(+), 34 deletions(-) create mode 100644 packages/share/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/share/android/src/main/res/xml/flutter_share_file_paths.xml diff --git a/packages/share/README.md b/packages/share/README.md index 3a3aa06d9799..b1f7ffb71ab2 100644 --- a/packages/share/README.md +++ b/packages/share/README.md @@ -32,3 +32,8 @@ sharing to email. ``` dart Share.share('check out my website https://example.com', subject: 'Look what I made!'); ``` + +To share a file invoke the static `shareFile` method anywhere in your Dart code +``` dart +Share.shareFile(File('${directory.path}/image.jpg')); +``` \ No newline at end of file diff --git a/packages/share/android/build.gradle b/packages/share/android/build.gradle index 4783abe54c6c..d3fcf0462f10 100644 --- a/packages/share/android/build.gradle +++ b/packages/share/android/build.gradle @@ -31,6 +31,11 @@ android { lintOptions { disable 'InvalidPackage' } + + dependencies { + implementation 'androidx.core:core:1.1.0' + implementation 'androidx.annotation:annotation:1.1.0' + } } // TODO(cyanglaz): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348 diff --git a/packages/share/android/gradle/wrapper/gradle-wrapper.properties b/packages/share/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..dd860985ded6 --- /dev/null +++ b/packages/share/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jan 02 08:47:06 CET 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/packages/share/android/src/main/AndroidManifest.xml b/packages/share/android/src/main/AndroidManifest.xml index 407eae4d8128..332afcb872c6 100644 --- a/packages/share/android/src/main/AndroidManifest.xml +++ b/packages/share/android/src/main/AndroidManifest.xml @@ -1,3 +1,14 @@ + + + + + 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 f7e4d579e7a2..da09ed5fb023 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,7 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; +import java.io.*; import java.util.Map; /** Handles the method calls for the plugin. */ @@ -19,15 +20,36 @@ 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 "shareFile": + expectMapArguments(call); + // Android does not support showing the share sheet at a particular point on screen. + try { + share.shareFile( + (String) call.argument("path"), + (String) call.argument("mimeType"), + (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 8c9e833ee9d3..72c9e1ecc884 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 @@ -6,6 +6,18 @@ import android.app.Activity; import android.content.Intent; +import android.net.Uri; +import android.os.Environment; + +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 androidx.annotation.NonNull; +import androidx.core.content.FileProvider; /** Handles share intent. */ class Share { @@ -47,4 +59,94 @@ void share(String text, String subject) { activity.startActivity(chooserIntent); } } + + void shareFile(String path, String mimeType, String text, String subject) + 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( + activity, activity.getPackageName() + ".flutter.share_provider", + file); + + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri); + if (text != null) shareIntent.putExtra(Intent.EXTRA_TEXT, text); + if (subject != null) shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); + shareIntent.setType(mimeType != null ? mimeType : "*/*"); + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); + if (activity != null) { + activity.startActivity(chooserIntent); + } else { + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activity.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(activity.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/example/lib/main.dart b/packages/share/example/lib/main.dart index b68195cd3507..077b3675a6ee 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -4,7 +4,10 @@ // ignore_for_file: public_member_api_docs +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:share/share.dart'; void main() { @@ -19,6 +22,7 @@ class DemoApp extends StatefulWidget { class DemoAppState extends State { String text = ''; String subject = ''; + bool shareFile = false; @override Widget build(BuildContext context) { @@ -53,14 +57,45 @@ class DemoAppState extends State { subject = value; }), ), - const Padding(padding: EdgeInsets.only(top: 24.0)), + const Padding(padding: EdgeInsets.only(top: 12.0)), + InkWell( + onTap: () => + setState(() { + shareFile = !shareFile; + }), + child: Row( + children: [ + Checkbox( + value: shareFile, + onChanged: (bool value) => + setState(() { + shareFile = value; + }), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Share file", + style: Theme.of(context).textTheme.subhead, + ), + Text( + "Text file with share text as content", + style: TextStyle(color: Colors.black54), + ), + ], + ) + ], + ), + ), + const Padding(padding: EdgeInsets.only(top: 12.0)), Builder( builder: (BuildContext context) { return RaisedButton( child: const Text('Share'), onPressed: text.isEmpty ? null - : () { + : () async { // A builder is used to retrieve the context immediately // surrounding the RaisedButton. // @@ -69,11 +104,26 @@ class DemoAppState extends State { // 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); + + if (shareFile) { + // Could use image_picker here + // - after migration of it to v2 + // - https://github.com/flutter/flutter/issues/41839 + // - https://github.com/flutter/plugins/pull/2430) + final File file = await _createTextFile('sample.txt', text); + Share.shareFile(file, + text: text, + subject: subject, + sharePositionOrigin: + box.localToGlobal(Offset.zero) & + box.size); + } else { + Share.share(text, + subject: subject, + sharePositionOrigin: + box.localToGlobal(Offset.zero) & + box.size); + } }, ); }, @@ -83,4 +133,11 @@ class DemoAppState extends State { )), ); } + + Future _createTextFile(String filename, String content) async { + final Directory directory = await getApplicationDocumentsDirectory(); + final File file = File('${directory.path}/$filename'); + await file.writeAsString(content); + return file; + } } diff --git a/packages/share/example/pubspec.yaml b/packages/share/example/pubspec.yaml index 4e4fc49dca7a..38c52e64fcda 100644 --- a/packages/share/example/pubspec.yaml +++ b/packages/share/example/pubspec.yaml @@ -1,11 +1,14 @@ name: share_example description: Demonstrates how to use the share plugin. +version: 1.0.0+1 dependencies: flutter: sdk: flutter share: path: ../ + path_provider: + path: ../../path_provider/ dev_dependencies: flutter_driver: @@ -18,4 +21,3 @@ flutter: 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 335ba5b819e5..d876b30f74f3 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ b/packages/share/ios/Classes/FLTSharePlugin.m @@ -57,8 +57,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"]; NSString *shareSubject = arguments[@"subject"]; @@ -69,35 +80,42 @@ + (void)registerWithRegistrar:(NSObject *)registrar { return; } - NSNumber *originX = arguments[@"originX"]; - NSNumber *originY = arguments[@"originY"]; - NSNumber *originWidth = arguments[@"originWidth"]; - NSNumber *originHeight = arguments[@"originHeight"]; - - CGRect originRect = CGRectZero; - if (originX != nil && originY != nil && originWidth != nil && originHeight != nil) { - originRect = CGRectMake([originX doubleValue], [originY doubleValue], - [originWidth doubleValue], [originHeight doubleValue]); - } - - [self share:shareText + [self shareText:shareText subject:shareSubject 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"]; + + if (path.length == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty path expected" + details:nil]); + return; + } + + [self shareFile:path + withMimeType:mimeType + withSubject:subject + withText:text + withController:[UIApplication sharedApplication].keyWindow.rootViewController + atSource:originRect]; + result(nil); } else { result(FlutterMethodNotImplemented); } }]; } -+ (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 +123,38 @@ + (void)share:(NSString *)shareText [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)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 ff20d194f9e5..2685ee1412f2 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'; @@ -51,4 +52,93 @@ class Share { 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 c03f8fb439df..4b83d2d4866d 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,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 {} From 7d9eb8a92da9dd1a6ed37aab7e110440166f11e9 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Fri, 3 Jan 2020 16:06:21 +0100 Subject: [PATCH 03/50] [share] Fix: When sharing file, as well share the text --- packages/share/ios/Classes/FLTSharePlugin.m | 68 +++++++++++++++------ 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m index d876b30f74f3..928ef08849b7 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ b/packages/share/ios/Classes/FLTSharePlugin.m @@ -10,8 +10,11 @@ @interface ShareData : NSObject @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 +30,64 @@ - (instancetype)init { - (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 { + if (_path != nil && _mimeType != nil) { + if ([_mimeType hasPrefix:@"image/"]) { + UIImage *image = [UIImage imageWithContentsOfFile:_path]; + return image; + } else { + NSURL *url = [NSURL fileURLWithPath:_path]; + return url; + } + } + return _text; } - (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 != nil + && _mimeType != nil + && [_mimeType hasPrefix:@"image/"]) { + UIImage *image = [UIImage imageWithContentsOfFile:_path]; + return [self imageWithImage:image scaledToSize:suggestedSize]; + } + return nil; +} + +- (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 @@ -137,23 +180,10 @@ + (void)shareFile:(id)path 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]; - } - + NSArray *items = @[ + [[ShareData alloc] initWithSubject:subject text:text], + [[ShareData alloc] initWithFile:path mimeType:mimeType] + ]; [self share:items withController:controller atSource:origin]; } From e0def3131fbbcdabd5fd617803751e198e12dc37 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Fri, 3 Jan 2020 16:16:08 +0100 Subject: [PATCH 04/50] [share] Fix: async requirements --- packages/share/example/lib/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index 077b3675a6ee..3de30384ba6a 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -111,14 +111,14 @@ class DemoAppState extends State { // - https://github.com/flutter/flutter/issues/41839 // - https://github.com/flutter/plugins/pull/2430) final File file = await _createTextFile('sample.txt', text); - Share.shareFile(file, + await Share.shareFile(file, text: text, subject: subject, sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); } else { - Share.share(text, + await Share.share(text, subject: subject, sharePositionOrigin: box.localToGlobal(Offset.zero) & From 9abd37e02a05b2f7987b07b1d63920f010a4c619 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Fri, 3 Jan 2020 19:59:14 +0100 Subject: [PATCH 05/50] [share] Fix formatting & async --- packages/share/example/lib/main.dart | 1 + packages/share/ios/Classes/FLTSharePlugin.m | 34 ++++++++++----------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index 3de30384ba6a..f0c6012bae5e 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -4,6 +4,7 @@ // ignore_for_file: public_member_api_docs +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m index 928ef08849b7..9638792ed409 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ b/packages/share/ios/Classes/FLTSharePlugin.m @@ -71,23 +71,21 @@ - (NSString *)activityViewController:(UIActivityViewController *)activityViewCon } - (UIImage *)activityViewController:(UIActivityViewController *)activityViewController - thumbnailImageForActivityType:(UIActivityType)activityType - suggestedSize:(CGSize)suggestedSize { - if (_path != nil - && _mimeType != nil - && [_mimeType hasPrefix:@"image/"]) { - UIImage *image = [UIImage imageWithContentsOfFile:_path]; - return [self imageWithImage:image scaledToSize:suggestedSize]; - } - return nil; + thumbnailImageForActivityType:(UIActivityType)activityType + suggestedSize:(CGSize)suggestedSize { + if (_path != nil && _mimeType != nil && [_mimeType hasPrefix:@"image/"]) { + UIImage *image = [UIImage imageWithContentsOfFile:_path]; + return [self imageWithImage:image scaledToSize:suggestedSize]; + } + return nil; } - (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize{ - UIGraphicsBeginImageContext(newSize); - [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; - UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return newImage; + UIGraphicsBeginImageContext(newSize); + [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; + UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return newImage; } @end @@ -139,7 +137,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { message:@"Non-empty path expected" details:nil]); return; - } + } [self shareFile:path withMimeType:mimeType @@ -147,7 +145,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { withText:text withController:[UIApplication sharedApplication].keyWindow.rootViewController atSource:originRect]; - result(nil); + result(nil); } else { result(FlutterMethodNotImplemented); } @@ -181,8 +179,8 @@ + (void)shareFile:(id)path withController:(UIViewController *)controller atSource:(CGRect)origin { NSArray *items = @[ - [[ShareData alloc] initWithSubject:subject text:text], - [[ShareData alloc] initWithFile:path mimeType:mimeType] + [[ShareData alloc] initWithSubject:subject text:text], [[ShareData alloc] initWithFile:path + mimeType:mimeType] ]; [self share:items withController:controller atSource:origin]; } From 697c89b457f1ddf0276e506ca187325db964c909 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Fri, 3 Jan 2020 20:16:12 +0100 Subject: [PATCH 06/50] [share] Fix more formatting --- .../java/io/flutter/plugins/share/Share.java | 12 +++------ packages/share/example/lib/main.dart | 25 +++++++++---------- packages/share/ios/Classes/FLTSharePlugin.m | 8 +++--- 3 files changed, 20 insertions(+), 25 deletions(-) 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 72c9e1ecc884..50272c8af11b 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 @@ -8,7 +8,8 @@ import android.content.Intent; 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; @@ -16,9 +17,6 @@ import java.io.InputStream; import java.io.OutputStream; -import androidx.annotation.NonNull; -import androidx.core.content.FileProvider; - /** Handles share intent. */ class Share { @@ -60,8 +58,7 @@ void share(String text, String subject) { } } - void shareFile(String path, String mimeType, String text, String subject) - throws IOException { + void shareFile(String path, String mimeType, String text, String subject) throws IOException { if (path == null || path.isEmpty()) { throw new IllegalArgumentException("Non-empty path expected"); } @@ -74,8 +71,7 @@ void shareFile(String path, String mimeType, String text, String subject) Uri fileUri = FileProvider.getUriForFile( - activity, activity.getPackageName() + ".flutter.share_provider", - file); + activity, activity.getPackageName() + ".flutter.share_provider", file); Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index f0c6012bae5e..dcf741d9d017 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -60,18 +60,16 @@ class DemoAppState extends State { ), const Padding(padding: EdgeInsets.only(top: 12.0)), InkWell( - onTap: () => - setState(() { - shareFile = !shareFile; - }), + onTap: () => setState(() { + shareFile = !shareFile; + }), child: Row( children: [ Checkbox( value: shareFile, - onChanged: (bool value) => - setState(() { - shareFile = value; - }), + onChanged: (bool value) => setState(() { + shareFile = value; + }), ), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -111,19 +109,20 @@ class DemoAppState extends State { // - after migration of it to v2 // - https://github.com/flutter/flutter/issues/41839 // - https://github.com/flutter/plugins/pull/2430) - final File file = await _createTextFile('sample.txt', text); + final File file = + await _createTextFile('sample.txt', text); await Share.shareFile(file, text: text, subject: subject, sharePositionOrigin: - box.localToGlobal(Offset.zero) & - box.size); + box.localToGlobal(Offset.zero) & + box.size); } else { await Share.share(text, subject: subject, sharePositionOrigin: - box.localToGlobal(Offset.zero) & - box.size); + box.localToGlobal(Offset.zero) & + box.size); } }, ); diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m index 9638792ed409..b205ae8cdb79 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ b/packages/share/ios/Classes/FLTSharePlugin.m @@ -14,7 +14,8 @@ @interface ShareData : NSObject @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)initWithFile:(NSString *)path + mimeType:(NSString *)mimeType NS_DESIGNATED_INITIALIZER; - (instancetype)init __attribute__((unavailable("Use initWithSubject:text: instead"))); @@ -36,8 +37,7 @@ - (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text { return self; } -- (instancetype)initWithFile:(NSString *)path - mimeType:(NSString *)mimeType { +- (instancetype)initWithFile:(NSString *)path mimeType:(NSString *)mimeType { self = [super init]; if (self) { _path = path; @@ -80,7 +80,7 @@ - (UIImage *)activityViewController:(UIActivityViewController *)activityViewCont return nil; } -- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize{ +- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize { UIGraphicsBeginImageContext(newSize); [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); From 66766d0a2759316a70bea5c576855aeb3f3d0860 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Fri, 3 Jan 2020 20:56:51 +0100 Subject: [PATCH 07/50] [share] Fix problems with nextMajor check by removing version from example --- .../share/android/gradle/wrapper/gradle-wrapper.properties | 6 ------ packages/share/example/pubspec.yaml | 1 - 2 files changed, 7 deletions(-) delete mode 100644 packages/share/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/share/android/gradle/wrapper/gradle-wrapper.properties b/packages/share/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index dd860985ded6..000000000000 --- a/packages/share/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Thu Jan 02 08:47:06 CET 2020 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/packages/share/example/pubspec.yaml b/packages/share/example/pubspec.yaml index 38c52e64fcda..9931acdf3c63 100644 --- a/packages/share/example/pubspec.yaml +++ b/packages/share/example/pubspec.yaml @@ -1,6 +1,5 @@ name: share_example description: Demonstrates how to use the share plugin. -version: 1.0.0+1 dependencies: flutter: From 589c83e7b76baeb43da845288c924de4f42e9f55 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 15 Jan 2020 13:53:38 +0100 Subject: [PATCH 08/50] [share] Update documentation formatting --- packages/share/lib/share.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index 2685ee1412f2..d495cc89f689 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -56,7 +56,7 @@ class Share { /// 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 + /// It uses the `ACTION_SEND` Intent on Android and `UIActivityViewController` /// on iOS. /// /// The optional `sharePositionOrigin` parameter can be used to specify a global From a82ee3d63a7cac1685f0a5226207ad21a475a119 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 15 Jan 2020 14:07:36 +0100 Subject: [PATCH 09/50] [share] example: Revert to using v1 of the plugin --- .../android/app/src/main/AndroidManifest.xml | 17 ++++++++------- .../shareexample/EmbeddingV1Activity.java | 18 ++++++++++++++++ .../shareexample/EmbeddingV1ActivityTest.java | 13 ++++++++++++ .../plugins/shareexample/MainActivity.java | 21 +++++++++++++++++++ .../shareexample/MainActivityTest.java | 15 +++++++++++++ 5 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java create mode 100644 packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java create mode 100644 packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java create mode 100644 packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivityTest.java diff --git a/packages/share/example/android/app/src/main/AndroidManifest.xml b/packages/share/example/android/app/src/main/AndroidManifest.xml index 50a8c3c9f052..d5e5ec8bf39d 100644 --- a/packages/share/example/android/app/src/main/AndroidManifest.xml +++ b/packages/share/example/android/app/src/main/AndroidManifest.xml @@ -3,10 +3,17 @@ - - + + + @@ -14,9 +21,5 @@ - - diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java new file mode 100644 index 000000000000..736ac546c55a --- /dev/null +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java @@ -0,0 +1,18 @@ +// Copyright 2017 The Chromium 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.shareexample; + +import android.os.Bundle; +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class EmbeddingV1Activity extends FlutterActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java new file mode 100644 index 000000000000..958541165806 --- /dev/null +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java @@ -0,0 +1,13 @@ +package io.flutter.plugins.shareexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.e2e.FlutterRunner; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterRunner.class) +public class EmbeddingV1ActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(EmbeddingV1Activity.class); +} diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java new file mode 100644 index 000000000000..5447e79f9f8b --- /dev/null +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java @@ -0,0 +1,21 @@ +// 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.shareexample; + +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.pathprovider.PathProviderPlugin; +import io.flutter.plugins.share.SharePlugin; + +public class MainActivity extends FlutterActivity { + // TODO(cyanglaz): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. + // https://github.com/flutter/flutter/issues/42694 + @Override + public void configureFlutterEngine(FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + flutterEngine.getPlugins().add(new PathProviderPlugin()); + flutterEngine.getPlugins().add(new SharePlugin()); + } +} diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivityTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivityTest.java new file mode 100644 index 000000000000..fcd936a7dd0f --- /dev/null +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivityTest.java @@ -0,0 +1,15 @@ +// Copyright 2019 The Chromium 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.shareexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.e2e.FlutterRunner; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterRunner.class) +public class MainActivityTest { + @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); +} From e3685a2e34b4ab65632ad25d091c0124bcadf46c Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 16 Jan 2020 08:10:57 +0100 Subject: [PATCH 10/50] [share] Update example on shareFile to not share text and file (which creates two files on iOS) --- packages/share/example/lib/main.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index dcf741d9d017..a69ecc8c49b1 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -108,11 +108,10 @@ class DemoAppState extends State { // Could use image_picker here // - after migration of it to v2 // - https://github.com/flutter/flutter/issues/41839 - // - https://github.com/flutter/plugins/pull/2430) + // - https://github.com/flutter/plugins/pull/2430 final File file = await _createTextFile('sample.txt', text); await Share.shareFile(file, - text: text, subject: subject, sharePositionOrigin: box.localToGlobal(Offset.zero) & From 696453fd6c38cc22b3c8036ea33ddf9588b756c0 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 16 Jan 2020 09:18:28 +0100 Subject: [PATCH 11/50] [share] Update example with image_picker --- .../plugins/shareexample/MainActivity.java | 4 +- packages/share/example/ios/Runner/Info.plist | 6 ++ packages/share/example/lib/main.dart | 93 ++++++++++--------- packages/share/example/pubspec.yaml | 3 +- 4 files changed, 59 insertions(+), 47 deletions(-) diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java index 5447e79f9f8b..dd704bdc3cc6 100644 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java @@ -6,7 +6,7 @@ import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.pathprovider.PathProviderPlugin; +import io.flutter.plugins.imagepicker.ImagePickerPlugin; import io.flutter.plugins.share.SharePlugin; public class MainActivity extends FlutterActivity { @@ -15,7 +15,7 @@ public class MainActivity extends FlutterActivity { @Override public void configureFlutterEngine(FlutterEngine flutterEngine) { super.configureFlutterEngine(flutterEngine); - flutterEngine.getPlugins().add(new PathProviderPlugin()); + flutterEngine.getPlugins().add(new ImagePickerPlugin()); flutterEngine.getPlugins().add(new SharePlugin()); } } diff --git a/packages/share/example/ios/Runner/Info.plist b/packages/share/example/ios/Runner/Info.plist index ac44e05ef845..71656105a1fa 100644 --- a/packages/share/example/ios/Runner/Info.plist +++ b/packages/share/example/ios/Runner/Info.plist @@ -45,5 +45,11 @@ UIViewControllerBasedStatusBarAppearance + NSPhotoLibraryUsageDescription + This app requires access to the photo library for sharing images. + NSMicrophoneUsageDescription + This app does not require access to the microphone for sharing images. + NSCameraUsageDescription + This app requires access to the camera for sharing images. diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index a69ecc8c49b1..961423e6cf48 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -4,11 +4,10 @@ // ignore_for_file: public_member_api_docs -import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:share/share.dart'; void main() { @@ -23,7 +22,7 @@ class DemoApp extends StatefulWidget { class DemoAppState extends State { String text = ''; String subject = ''; - bool shareFile = false; + File imageFile; @override Widget build(BuildContext context) { @@ -37,6 +36,7 @@ class DemoAppState extends State { padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( decoration: const InputDecoration( @@ -59,34 +59,21 @@ class DemoAppState extends State { }), ), const Padding(padding: EdgeInsets.only(top: 12.0)), - InkWell( - onTap: () => setState(() { - shareFile = !shareFile; - }), - child: Row( - children: [ - Checkbox( - value: shareFile, - onChanged: (bool value) => setState(() { - shareFile = value; - }), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Share file", - style: Theme.of(context).textTheme.subhead, - ), - Text( - "Text file with share text as content", - style: TextStyle(color: Colors.black54), - ), - ], + _buildImagePreview(), + imageFile == null + ? ListTile( + leading: Icon(Icons.add), + title: Text("Add image"), + onTap: () async { + final image = await ImagePicker.pickImage( + source: ImageSource.gallery, + ); + setState(() { + this.imageFile = image; + }); + }, ) - ], - ), - ), + : Container(), const Padding(padding: EdgeInsets.only(top: 12.0)), Builder( builder: (BuildContext context) { @@ -104,14 +91,9 @@ class DemoAppState extends State { // has its position and size after it's built. final RenderBox box = context.findRenderObject(); - if (shareFile) { - // Could use image_picker here - // - after migration of it to v2 - // - https://github.com/flutter/flutter/issues/41839 - // - https://github.com/flutter/plugins/pull/2430 - final File file = - await _createTextFile('sample.txt', text); - await Share.shareFile(file, + if (imageFile != null) { + await Share.shareFile(imageFile, + text: text, subject: subject, sharePositionOrigin: box.localToGlobal(Offset.zero) & @@ -133,10 +115,35 @@ class DemoAppState extends State { ); } - Future _createTextFile(String filename, String content) async { - final Directory directory = await getApplicationDocumentsDirectory(); - final File file = File('${directory.path}/$filename'); - await file.writeAsString(content); - return file; + Widget _buildImagePreview() { + if (imageFile == null) { + return Container(); + } + return Stack( + children: [ + 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: () { + setState(() { + imageFile = null; + }); + }, + ), + ), + ), + ], + ); } } diff --git a/packages/share/example/pubspec.yaml b/packages/share/example/pubspec.yaml index 9931acdf3c63..8213af2f03d0 100644 --- a/packages/share/example/pubspec.yaml +++ b/packages/share/example/pubspec.yaml @@ -6,8 +6,7 @@ dependencies: sdk: flutter share: path: ../ - path_provider: - path: ../../path_provider/ + image_picker: ^0.6.3+1 dev_dependencies: flutter_driver: From 906531082a9d88d8ef5be5e3b16c1c03afa35705 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 16 Jan 2020 11:13:28 +0100 Subject: [PATCH 12/50] [share] Add support to share multiple files --- packages/share/README.md | 5 + .../plugins/share/MethodCallHandler.java | 10 +- .../java/io/flutter/plugins/share/Share.java | 71 +++++++++-- packages/share/example/lib/main.dart | 118 +++++++++++------- packages/share/ios/Classes/FLTSharePlugin.m | 41 ++++-- packages/share/lib/share.dart | 52 ++++++-- 6 files changed, 210 insertions(+), 87 deletions(-) diff --git a/packages/share/README.md b/packages/share/README.md index b1f7ffb71ab2..d452f9434816 100644 --- a/packages/share/README.md +++ b/packages/share/README.md @@ -36,4 +36,9 @@ Share.share('check out my website https://example.com', subject: 'Look what I ma To share a file invoke the static `shareFile` method anywhere in your Dart code ``` dart Share.shareFile(File('${directory.path}/image.jpg')); +``` + +To share multiple files invoke the static `shareFiles` method anywhere in your Dart code +``` dart +Share.shareFiles([File('${directory.path}/image1.jpg', File('${directory.path}/image2.jpg'])); ``` \ No newline at end of file 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 da09ed5fb023..02841d3a4ae2 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 @@ -7,6 +7,7 @@ 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. */ @@ -27,13 +28,14 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { share.share((String) call.argument("text"), (String) call.argument("subject")); result.success(null); break; - case "shareFile": + case "shareFiles": expectMapArguments(call); + // Android does not support showing the share sheet at a particular point on screen. try { - share.shareFile( - (String) call.argument("path"), - (String) call.argument("mimeType"), + share.shareFiles( + (List) call.argument("paths"), + (List) call.argument("mimeTypes"), (String) call.argument("text"), (String) call.argument("subject")); result.success(null); 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 50272c8af11b..8447655d5dd5 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 @@ -16,6 +16,8 @@ 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 { @@ -58,27 +60,27 @@ void share(String text, String subject) { } } - void shareFile(String path, String mimeType, String text, String subject) throws IOException { - if (path == null || path.isEmpty()) { + void shareFiles(List paths, List mimeTypes, String text, String subject) throws IOException { + if (paths == null || paths.isEmpty()) { throw new IllegalArgumentException("Non-empty path expected"); } - File file = new File(path); clearExternalShareFolder(); - if (!fileIsOnExternal(file)) { - file = copyToExternalShareFolder(file); - } - - Uri fileUri = - FileProvider.getUriForFile( - activity, activity.getPackageName() + ".flutter.share_provider", file); + ArrayList fileUris = getUrisForPaths(paths); Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri); + 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.setType(mimeType != null ? mimeType : "*/*"); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); if (activity != null) { @@ -89,6 +91,49 @@ void shareFile(String path, String mimeType, String text, String subject) throws } } + private ArrayList getUrisForPaths(List paths) throws IOException { + ArrayList uris = new ArrayList<>(paths.size()); + for (String path : paths) { + File file = new File(path); + if (!fileIsOnExternal(file)) { + file = copyToExternalShareFolder(file); + } + + uris.add(FileProvider.getUriForFile( + activity, activity.getPackageName() + ".flutter.share_provider", file)); + } + return uris; + } + + private String reduceMimeTypes(List 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(); diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index 961423e6cf48..5fd2ee8ac776 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -22,7 +22,7 @@ class DemoApp extends StatefulWidget { class DemoAppState extends State { String text = ''; String subject = ''; - File imageFile; + List imageFiles = []; @override Widget build(BuildContext context) { @@ -59,21 +59,19 @@ class DemoAppState extends State { }), ), const Padding(padding: EdgeInsets.only(top: 12.0)), - _buildImagePreview(), - imageFile == null - ? ListTile( - leading: Icon(Icons.add), - title: Text("Add image"), - onTap: () async { - final image = await ImagePicker.pickImage( - source: ImageSource.gallery, - ); - setState(() { - this.imageFile = image; - }); - }, - ) - : Container(), + _buildImagePreviews(), + ListTile( + leading: Icon(Icons.add), + title: Text("Add image"), + onTap: () async { + final image = await ImagePicker.pickImage( + source: ImageSource.gallery, + ); + setState(() { + imageFiles.add(image); + }); + }, + ), const Padding(padding: EdgeInsets.only(top: 12.0)), Builder( builder: (BuildContext context) { @@ -91,13 +89,22 @@ class DemoAppState extends State { // has its position and size after it's built. final RenderBox box = context.findRenderObject(); - if (imageFile != null) { - await Share.shareFile(imageFile, - text: text, - subject: subject, - sharePositionOrigin: - box.localToGlobal(Offset.zero) & - box.size); + if (imageFiles.isNotEmpty) { + if (imageFiles.length == 1) { + await Share.shareFile(imageFiles[0], + text: text, + subject: subject, + sharePositionOrigin: + box.localToGlobal(Offset.zero) & + box.size); + } else { + await Share.shareFiles(imageFiles, + text: text, + subject: subject, + sharePositionOrigin: + box.localToGlobal(Offset.zero) & + box.size); + } } else { await Share.share(text, subject: subject, @@ -115,35 +122,50 @@ class DemoAppState extends State { ); } - Widget _buildImagePreview() { - if (imageFile == null) { - return Container(); + Widget _buildImagePreviews() { + if (imageFiles.isEmpty) return Container(); + + List imageWidgets = []; + for (int i = 0; i < imageFiles.length; i++) { + imageWidgets.add(_buildImagePreview(i)); } - return Stack( - children: [ - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 200, - maxHeight: 200, + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row(children: imageWidgets), + ); + } + + Widget _buildImagePreview(int position) { + File imageFile = imageFiles[position]; + return Padding( + padding: const EdgeInsets.all(8.0), + child: Stack( + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 200, + maxHeight: 200, + ), + child: Image.file(imageFile), ), - 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: () { - setState(() { - imageFile = null; - }); - }, + Positioned( + right: 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + child: Icon(Icons.delete), + onPressed: () { + setState(() { + imageFiles.removeAt(position); + }); + }, + ), ), ), - ), - ], + ], + ), ); } } diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m index b205ae8cdb79..633b8c52b60f 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ b/packages/share/ios/Classes/FLTSharePlugin.m @@ -126,21 +126,30 @@ + (void)registerWithRegistrar:(NSObject *)registrar { withController:[UIApplication sharedApplication].keyWindow.rootViewController atSource:originRect]; result(nil); - } else if ([@"shareFile" isEqualToString:call.method]) { - NSString *path = arguments[@"path"]; - NSString *mimeType = arguments[@"mimeType"]; + } else if ([@"shareFiles" isEqualToString:call.method]) { + NSArray *paths = arguments[@"paths"]; + NSArray *mimeTypes = arguments[@"mimeTypes"]; NSString *subject = arguments[@"subject"]; NSString *text = arguments[@"text"]; - if (path.length == 0) { + if (paths.count == 0) { result([FlutterError errorWithCode:@"error" - message:@"Non-empty path expected" + message:@"Non-empty paths expected" details:nil]); return; } - [self shareFile:path - withMimeType:mimeType + for (NSString *path in paths) { + if (path.length == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty paths expected" + details:nil]); + return; + } + } + + [self shareFiles:paths + withMimeType:mimeTypes withSubject:subject withText:text withController:[UIApplication sharedApplication].keyWindow.rootViewController @@ -172,16 +181,22 @@ + (void)shareText:(NSString *)shareText [self share:@[ data ] withController:controller atSource:origin]; } -+ (void)shareFile:(id)path - withMimeType:(id)mimeType ++ (void)shareFiles:(NSArray *)paths + withMimeType:(NSArray *)mimeTypes withSubject:(NSString *)subject withText:(NSString *)text withController:(UIViewController *)controller atSource:(CGRect)origin { - NSArray *items = @[ - [[ShareData alloc] initWithSubject:subject text:text], [[ShareData alloc] initWithFile:path - mimeType:mimeType] - ]; + NSMutableArray *items = [[NSMutableArray alloc] init]; + + if (text != nil || subject != nil) { + [items addObject:[[ShareData alloc] initWithSubject:subject text:text]]; + } + + for (int i = 0; i < [paths count]; i++) { + [items addObject:[[ShareData alloc] initWithFile:paths[i] mimeType:mimeTypes[i]]]; + } + [self share:items withController:controller atSource:origin]; } diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index d495cc89f689..1eec471d3b05 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -53,7 +53,7 @@ class Share { return channel.invokeMethod('share', params); } - /// Summons the platform's share sheet to share a file. + /// Summons the platform's share sheet to share a single file. /// /// Wraps the platform's native share dialog. Can share a file. /// It uses the `ACTION_SEND` Intent on Android and `UIActivityViewController` @@ -65,16 +65,50 @@ class Share { /// /// May throw [PlatformException] or [FormatException] /// from [MethodChannel]. - static Future shareFile(File file, - {String mimeType, - String subject, - String text, - Rect sharePositionOrigin}) { + static Future shareFile( + File file, { + String mimeType, + String subject, + String text, + Rect sharePositionOrigin, + }) { assert(file != null); assert(file.existsSync()); + + shareFiles([file], + mimeTypes: [mimeType ?? _mimeTypeForFile(file)], + subject: subject, + text: text, + sharePositionOrigin: sharePositionOrigin, + ); + } + + /// 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 shareFiles( + List files, { + List mimeTypes, + String subject, + String text, + Rect sharePositionOrigin, + }) { + assert(files != null); + assert(files.isNotEmpty); + assert(files.every((file) => file.existsSync())); final Map params = { - 'path': file.path, - 'mimeType': mimeType ?? _mimeTypeForFile(file), + 'paths': files.map((file) => file.path).toList(), + 'mimeTypes': mimeTypes ?? + files.map((file) => _mimeTypeForFile(file)).toList(), }; if (subject != null) params['subject'] = subject; @@ -87,7 +121,7 @@ class Share { params['originHeight'] = sharePositionOrigin.height; } - return channel.invokeMethod('shareFile', params); + return channel.invokeMethod('shareFiles', params); } static String _mimeTypeForFile(File file) { From 333bad72a624ad27195047912d1962a6677e181c Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 16 Jan 2020 11:19:55 +0100 Subject: [PATCH 13/50] [share] Update tests --- packages/share/test/share_test.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/share/test/share_test.dart b/packages/share/test/share_test.dart index 4b83d2d4866d..e3a4d03dc87e 100644 --- a/packages/share/test/share_test.dart +++ b/packages/share/test/share_test.dart @@ -87,9 +87,9 @@ void main() { try { file.createSync(); await Share.shareFile(file); - verify(mockChannel.invokeMethod('shareFile', { - 'path': file.path, - 'mimeType': 'image/png', + verify(mockChannel.invokeMethod('shareFiles', { + 'paths': [file.path], + 'mimeTypes': ['image/png'], })); } finally { file.deleteSync(); @@ -101,9 +101,9 @@ void main() { try { file.createSync(); await Share.shareFile(file, mimeType: '*/*'); - verify(mockChannel.invokeMethod('shareFile', { - 'path': file.path, - 'mimeType': '*/*', + verify(mockChannel.invokeMethod('shareFiles', { + 'paths': [file.path], + 'mimeTypes': ['*/*'], })); } finally { file.deleteSync(); From c7739d783ddb092960661eb61441734f6ffb45f4 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 16 Jan 2020 11:28:00 +0100 Subject: [PATCH 14/50] [share] Fix formatting --- .../src/main/java/io/flutter/plugins/share/Share.java | 10 ++++++---- packages/share/lib/share.dart | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) 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 8447655d5dd5..5dc121c19a39 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 @@ -60,7 +60,8 @@ void share(String text, String subject) { } } - void shareFiles(List paths, List mimeTypes, String text, String subject) throws IOException { + void shareFiles(List paths, List mimeTypes, String text, String subject) + throws IOException { if (paths == null || paths.isEmpty()) { throw new IllegalArgumentException("Non-empty path expected"); } @@ -99,8 +100,9 @@ private ArrayList getUrisForPaths(List paths) throws IOException { file = copyToExternalShareFolder(file); } - uris.add(FileProvider.getUriForFile( - activity, activity.getPackageName() + ".flutter.share_provider", file)); + uris.add( + FileProvider.getUriForFile( + activity, activity.getPackageName() + ".flutter.share_provider", file)); } return uris; } @@ -120,7 +122,7 @@ private String reduceMimeTypes(List mimeTypes) { } } return reducedMimeType; - } else if(mimeTypes.size() == 1) { + } else if (mimeTypes.size() == 1) { return mimeTypes.get(0); } else { return "*/*"; diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index 1eec471d3b05..6bc2e4418923 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -75,7 +75,8 @@ class Share { assert(file != null); assert(file.existsSync()); - shareFiles([file], + return shareFiles( + [file], mimeTypes: [mimeType ?? _mimeTypeForFile(file)], subject: subject, text: text, @@ -107,8 +108,8 @@ class Share { assert(files.every((file) => file.existsSync())); final Map params = { 'paths': files.map((file) => file.path).toList(), - 'mimeTypes': mimeTypes ?? - files.map((file) => _mimeTypeForFile(file)).toList(), + 'mimeTypes': + mimeTypes ?? files.map((file) => _mimeTypeForFile(file)).toList(), }; if (subject != null) params['subject'] = subject; From 8c9f55f671a285638b0c9e58385a8a83bf00cdb6 Mon Sep 17 00:00:00 2001 From: Kifah Meeran <23234883+MaskyS@users.noreply.github.com> Date: Wed, 18 Mar 2020 14:00:55 +0400 Subject: [PATCH 15/50] Fix Permission Denial error --- .../src/main/java/io/flutter/plugins/share/Share.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 5dc121c19a39..decf6f562f71 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 @@ -6,6 +6,8 @@ import android.app.Activity; 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; @@ -84,6 +86,13 @@ void shareFiles(List paths, List mimeTypes, String text, String 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 resInfoList = activity.getPackageManager().queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + activity.grantUriPermission(packageName, fileUris.get(0), Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + if (activity != null) { activity.startActivity(chooserIntent); } else { From 2b27e0cf83cc0974fc478dff6a382b41bc301d29 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 18 Mar 2020 13:12:51 +0100 Subject: [PATCH 16/50] Fix formatting --- .../src/main/java/io/flutter/plugins/share/Share.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 decf6f562f71..d611411fd59e 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 @@ -87,10 +87,16 @@ void shareFiles(List paths, List mimeTypes, String text, String shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); - List resInfoList = activity.getPackageManager().queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY); + List resInfoList = + activity + .getPackageManager() + .queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolveInfo : resInfoList) { String packageName = resolveInfo.activityInfo.packageName; - activity.grantUriPermission(packageName, fileUris.get(0), Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); + activity.grantUriPermission( + packageName, + fileUris.get(0), + Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); } if (activity != null) { From e2cdec1f0b144e0113f075a58146e60d0dd8e58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aloi=CC=88s=20Deniel?= Date: Thu, 23 Apr 2020 06:27:57 +0200 Subject: [PATCH 17/50] [share] Allowing shared images on iOS to have specific actions (save to gallery, affect to contact). --- packages/share/ios/Classes/FLTSharePlugin.m | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m index 633b8c52b60f..9587dbc373d7 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ b/packages/share/ios/Classes/FLTSharePlugin.m @@ -194,7 +194,21 @@ + (void)shareFiles:(NSArray *)paths } for (int i = 0; i < [paths count]; i++) { - [items addObject:[[ShareData alloc] initWithFile:paths[i] mimeType:mimeTypes[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]; From 4ecd3c3538d73fca0dd24763d9615ae254d77e76 Mon Sep 17 00:00:00 2001 From: Colin Stewart Date: Fri, 1 May 2020 16:40:17 +0200 Subject: [PATCH 18/50] Resolving conflict with open_file Added tools:replace to allow coexistence with the open_file plugin. --- packages/share/android/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/share/android/src/main/AndroidManifest.xml b/packages/share/android/src/main/AndroidManifest.xml index 332afcb872c6..d386d7a1be2d 100644 --- a/packages/share/android/src/main/AndroidManifest.xml +++ b/packages/share/android/src/main/AndroidManifest.xml @@ -5,7 +5,8 @@ android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.flutter.share_provider" android:exported="false" - android:grantUriPermissions="true"> + android:grantUriPermissions="true" + tools:replace="android:authorities"> From 9c1518a0b20d474ce048b8871d7e30e0bedbbc3f Mon Sep 17 00:00:00 2001 From: Colin Stewart Date: Fri, 1 May 2020 16:44:00 +0200 Subject: [PATCH 19/50] Added missing namespace. --- packages/share/android/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/share/android/src/main/AndroidManifest.xml b/packages/share/android/src/main/AndroidManifest.xml index d386d7a1be2d..fac48da792fd 100644 --- a/packages/share/android/src/main/AndroidManifest.xml +++ b/packages/share/android/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + package="io.flutter.plugins.share" + xmlns:tools="http://schemas.android.com/tools"> Date: Fri, 1 May 2020 20:34:07 +0200 Subject: [PATCH 20/50] More attempts to fix Manifest --- packages/share/android/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/share/android/src/main/AndroidManifest.xml b/packages/share/android/src/main/AndroidManifest.xml index fac48da792fd..469af85da9be 100644 --- a/packages/share/android/src/main/AndroidManifest.xml +++ b/packages/share/android/src/main/AndroidManifest.xml @@ -10,7 +10,8 @@ tools:replace="android:authorities"> + android:resource="@xml/flutter_share_file_paths" + tools:replace="android:resource"/> From 57777e92032c6fc819709ec6d3106affe034ab08 Mon Sep 17 00:00:00 2001 From: Colin Stewart Date: Fri, 1 May 2020 21:05:12 +0200 Subject: [PATCH 21/50] Created custom provider to avoid name conflicts. --- .../flutter/plugins/share/ShareFileProvider.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java 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 000000000000..87e4e42a03d4 --- /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 } name collisions. + * + *

See https://developer.android.com/guide/topics/manifest/provider-element.html for details. + */ +public class ShareFileProvider extends FileProvider {} From 52e8b30b49e5392f9e5140db131d1a31e53140ac Mon Sep 17 00:00:00 2001 From: Colin Stewart Date: Fri, 1 May 2020 21:07:27 +0200 Subject: [PATCH 22/50] Updated to use the custom provider. --- packages/share/android/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/share/android/src/main/AndroidManifest.xml b/packages/share/android/src/main/AndroidManifest.xml index 469af85da9be..c51fcd62751c 100644 --- a/packages/share/android/src/main/AndroidManifest.xml +++ b/packages/share/android/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> Date: Fri, 1 May 2020 21:12:43 +0200 Subject: [PATCH 23/50] Remove unneeded edits. --- packages/share/android/src/main/AndroidManifest.xml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/share/android/src/main/AndroidManifest.xml b/packages/share/android/src/main/AndroidManifest.xml index c51fcd62751c..c141a5c67928 100644 --- a/packages/share/android/src/main/AndroidManifest.xml +++ b/packages/share/android/src/main/AndroidManifest.xml @@ -1,17 +1,14 @@ + package="io.flutter.plugins.share"> + android:grantUriPermissions="true"> + android:resource="@xml/flutter_share_file_paths"/> From 3ad335148ccde32bd246a9cf1408ef045dbbdb26 Mon Sep 17 00:00:00 2001 From: Colin Stewart Date: Sat, 2 May 2020 14:27:57 +0200 Subject: [PATCH 24/50] Updated as per the contribution guidelines. --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index b693b0bfb3ae..b908d17f32bb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -49,3 +49,4 @@ Luigi Agosti Quentin Le Guennec Koushik Ravikumar Nissim Dsilva +Colin Stewart From 86e746d76dcd4db26b863c27a774fac001a4a00a Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 19 Aug 2020 07:51:08 +0200 Subject: [PATCH 25/50] [share] Update dependencies --- packages/share/android/build.gradle | 2 +- .../share/android/src/main/res/xml/flutter_share_file_paths.xml | 2 +- packages/share/example/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/share/android/build.gradle b/packages/share/android/build.gradle index 3e8b93d10539..7506f4db8261 100644 --- a/packages/share/android/build.gradle +++ b/packages/share/android/build.gradle @@ -33,7 +33,7 @@ android { } dependencies { - implementation 'androidx.core:core:1.1.0' + implementation 'androidx.core:core:1.3.1' implementation 'androidx.annotation:annotation:1.1.0' } } 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 index ce94d87583e4..e68bf916a30b 100644 --- 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 @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/packages/share/example/pubspec.yaml b/packages/share/example/pubspec.yaml index 9b73676ec75b..006544390d44 100644 --- a/packages/share/example/pubspec.yaml +++ b/packages/share/example/pubspec.yaml @@ -6,7 +6,7 @@ dependencies: sdk: flutter share: path: ../ - image_picker: ^0.6.3+1 + image_picker: ^0.6.7+4 dev_dependencies: flutter_driver: From fd23afdef8eb6df1b5db19dde8a52b67fed26f6b Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 19 Aug 2020 07:55:34 +0200 Subject: [PATCH 26/50] [share] Fix formatting --- packages/share/ios/Classes/FLTSharePlugin.m | 29 ++++++++++----------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m index 9587dbc373d7..47afa5ecc309 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ b/packages/share/ios/Classes/FLTSharePlugin.m @@ -194,21 +194,20 @@ + (void)shareFiles:(NSArray *)paths } 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]]; - } + 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]; From 9494310ee0f51df16570a813de70967ae79e4f04 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 19 Aug 2020 08:19:31 +0200 Subject: [PATCH 27/50] [share] Update deprecated code in example --- packages/share/example/lib/main.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index 5fd2ee8ac776..f8e72cd819c2 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -64,12 +64,15 @@ class DemoAppState extends State { leading: Icon(Icons.add), title: Text("Add image"), onTap: () async { - final image = await ImagePicker.pickImage( + final imagePicker = ImagePicker(); + final pickedFile = await imagePicker.getImage( source: ImageSource.gallery, ); - setState(() { - imageFiles.add(image); - }); + if (pickedFile != null) { + setState(() { + imageFiles.add(File(pickedFile.path)); + }); + } }, ), const Padding(padding: EdgeInsets.only(top: 12.0)), From 1b052565f0b4c8e601c7fd37f7564ceb07e656b1 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 19 Aug 2020 08:38:53 +0200 Subject: [PATCH 28/50] [share] Fix uninitialized variable --- packages/share/ios/Classes/FLTSharePlugin.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m index 47afa5ecc309..fe98da56068e 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ b/packages/share/ios/Classes/FLTSharePlugin.m @@ -104,7 +104,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { NSNumber *originWidth = arguments[@"originWidth"]; NSNumber *originHeight = arguments[@"originHeight"]; - CGRect originRect; + CGRect originRect = CGRectZero; if (originX != nil && originY != nil && originWidth != nil && originHeight != nil) { originRect = CGRectMake([originX doubleValue], [originY doubleValue], [originWidth doubleValue], [originHeight doubleValue]); From 2a9da4b768391fb834b9009ed02f174c812831d6 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 20 Aug 2020 08:03:06 +0200 Subject: [PATCH 29/50] [share] use mime package for mime type lookup --- packages/share/lib/share.dart | 50 ++--------------------------------- packages/share/pubspec.yaml | 1 + 2 files changed, 3 insertions(+), 48 deletions(-) diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index 6bc2e4418923..d0e2ddf65b97 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -8,6 +8,7 @@ import 'dart:ui'; 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 { @@ -127,53 +128,6 @@ class Share { 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; + return lookupMimeType(file.path); } } diff --git a/packages/share/pubspec.yaml b/packages/share/pubspec.yaml index 8da4d85bf842..78e392098e94 100644 --- a/packages/share/pubspec.yaml +++ b/packages/share/pubspec.yaml @@ -18,6 +18,7 @@ flutter: dependencies: meta: ^1.0.5 + mime: ^0.9.7 flutter: sdk: flutter From 4567fdbca7ba1867df69e4aabb01d1da491716a8 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 20 Aug 2020 08:06:33 +0200 Subject: [PATCH 30/50] [share] Return `application/octet-stream` in case mime returns `null` --- packages/share/lib/share.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index d0e2ddf65b97..0cf0cde94e0b 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -128,6 +128,6 @@ class Share { static String _mimeTypeForFile(File file) { assert(file != null); - return lookupMimeType(file.path); + return lookupMimeType(file.path) ?? 'application/octet-stream'; } } From b8fb0362cb29bd22526e47cb8369eb4487e054cc Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 20 Aug 2020 08:28:37 +0200 Subject: [PATCH 31/50] [share] Remove library dependency on `dart:io` to allow future support of web --- packages/share/example/lib/main.dart | 20 ++++++++++---------- packages/share/lib/share.dart | 27 ++++++++++++--------------- packages/share/test/share_test.dart | 20 +++++++------------- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index f8e72cd819c2..04929fb5a821 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -22,7 +22,7 @@ class DemoApp extends StatefulWidget { class DemoAppState extends State { String text = ''; String subject = ''; - List imageFiles = []; + List imagePaths = []; @override Widget build(BuildContext context) { @@ -70,7 +70,7 @@ class DemoAppState extends State { ); if (pickedFile != null) { setState(() { - imageFiles.add(File(pickedFile.path)); + imagePaths.add(pickedFile.path); }); } }, @@ -92,16 +92,16 @@ class DemoAppState extends State { // has its position and size after it's built. final RenderBox box = context.findRenderObject(); - if (imageFiles.isNotEmpty) { - if (imageFiles.length == 1) { - await Share.shareFile(imageFiles[0], + if (imagePaths.isNotEmpty) { + if (imagePaths.length == 1) { + await Share.shareFile('', text: text, subject: subject, sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); } else { - await Share.shareFiles(imageFiles, + await Share.shareFiles(imagePaths, text: text, subject: subject, sharePositionOrigin: @@ -126,10 +126,10 @@ class DemoAppState extends State { } Widget _buildImagePreviews() { - if (imageFiles.isEmpty) return Container(); + if (imagePaths.isEmpty) return Container(); List imageWidgets = []; - for (int i = 0; i < imageFiles.length; i++) { + for (int i = 0; i < imagePaths.length; i++) { imageWidgets.add(_buildImagePreview(i)); } @@ -140,7 +140,7 @@ class DemoAppState extends State { } Widget _buildImagePreview(int position) { - File imageFile = imageFiles[position]; + File imageFile = File(imagePaths[position]); return Padding( padding: const EdgeInsets.all(8.0), child: Stack( @@ -161,7 +161,7 @@ class DemoAppState extends State { child: Icon(Icons.delete), onPressed: () { setState(() { - imageFiles.removeAt(position); + imagePaths.removeAt(position); }); }, ), diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index 0cf0cde94e0b..0ac49a06464f 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io'; import 'dart:ui'; import 'package:flutter/services.dart'; @@ -67,18 +66,17 @@ class Share { /// May throw [PlatformException] or [FormatException] /// from [MethodChannel]. static Future shareFile( - File file, { + String path, { String mimeType, String subject, String text, Rect sharePositionOrigin, }) { - assert(file != null); - assert(file.existsSync()); + assert(path != null); return shareFiles( - [file], - mimeTypes: [mimeType ?? _mimeTypeForFile(file)], + [path], + mimeTypes: [mimeType ?? _mimeTypeForPath(path)], subject: subject, text: text, sharePositionOrigin: sharePositionOrigin, @@ -98,19 +96,18 @@ class Share { /// May throw [PlatformException] or [FormatException] /// from [MethodChannel]. static Future shareFiles( - List files, { + List paths, { List mimeTypes, String subject, String text, Rect sharePositionOrigin, }) { - assert(files != null); - assert(files.isNotEmpty); - assert(files.every((file) => file.existsSync())); + assert(paths != null); + assert(paths.isNotEmpty); final Map params = { - 'paths': files.map((file) => file.path).toList(), + 'paths': paths, 'mimeTypes': - mimeTypes ?? files.map((file) => _mimeTypeForFile(file)).toList(), + mimeTypes ?? paths.map((String path) => _mimeTypeForPath(path)).toList(), }; if (subject != null) params['subject'] = subject; @@ -126,8 +123,8 @@ class Share { return channel.invokeMethod('shareFiles', params); } - static String _mimeTypeForFile(File file) { - assert(file != null); - return lookupMimeType(file.path) ?? 'application/octet-stream'; + static String _mimeTypeForPath(String path) { + assert(path != null); + return lookupMimeType(path) ?? 'application/octet-stream'; } } diff --git a/packages/share/test/share_test.dart b/packages/share/test/share_test.dart index e3a4d03dc87e..d5378d9ab262 100644 --- a/packages/share/test/share_test.dart +++ b/packages/share/test/share_test.dart @@ -74,21 +74,14 @@ void main() { 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'); + final String path = 'tempfile-83649a.png'; + final File file = File(path); try { file.createSync(); - await Share.shareFile(file); + await Share.shareFile(path); verify(mockChannel.invokeMethod('shareFiles', { - 'paths': [file.path], + 'paths': [path], 'mimeTypes': ['image/png'], })); } finally { @@ -97,10 +90,11 @@ void main() { }); test('sharing file sets passed mimeType', () async { - final File file = File('tempfile-83649a.png'); + final String path = 'tempfile-83649a.png'; + final File file = File(path); try { file.createSync(); - await Share.shareFile(file, mimeType: '*/*'); + await Share.shareFile(path, mimeType: '*/*'); verify(mockChannel.invokeMethod('shareFiles', { 'paths': [file.path], 'mimeTypes': ['*/*'], From e92ecc3805b20c4c7bbacf3fb70f943fdfff2f2d Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Fri, 21 Aug 2020 07:49:57 +0200 Subject: [PATCH 32/50] [share] Update version to 0.6.5 --- packages/share/CHANGELOG.md | 4 ++++ packages/share/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md index 8c3814d2f559..c4ee830ed34f 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/pubspec.yaml b/packages/share/pubspec.yaml index f1f74e5f4bb2..918087b139ec 100644 --- a/packages/share/pubspec.yaml +++ b/packages/share/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/share # 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: From 16c9b25d6f0c1be243b30a5132ab162b90324a0c Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Fri, 21 Aug 2020 07:59:09 +0200 Subject: [PATCH 33/50] [share] Formatting --- packages/share/lib/share.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index 0ac49a06464f..6c78c9b014a8 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -106,8 +106,8 @@ class Share { assert(paths.isNotEmpty); final Map params = { 'paths': paths, - 'mimeTypes': - mimeTypes ?? paths.map((String path) => _mimeTypeForPath(path)).toList(), + 'mimeTypes': mimeTypes ?? + paths.map((String path) => _mimeTypeForPath(path)).toList(), }; if (subject != null) params['subject'] = subject; From a426f935cac3f96c985c51df9721575cd3adcd63 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Fri, 21 Aug 2020 08:30:53 +0200 Subject: [PATCH 34/50] [share] [BUILD] From 28b6de59cf9c8a7b793907fad3ab17bc57498d86 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 26 Aug 2020 08:24:56 +0200 Subject: [PATCH 35/50] [share] Remove `shareFile()` as it does not more than `shareFiles()` --- packages/share/README.md | 10 +++------- packages/share/example/lib/main.dart | 21 ++++++------------- packages/share/lib/share.dart | 30 ---------------------------- 3 files changed, 9 insertions(+), 52 deletions(-) diff --git a/packages/share/README.md b/packages/share/README.md index 8948099dc096..750fca6a5b18 100644 --- a/packages/share/README.md +++ b/packages/share/README.md @@ -40,12 +40,8 @@ sharing to email. Share.share('check out my website https://example.com', subject: 'Look what I made!'); ``` -To share a file invoke the static `shareFile` method anywhere in your Dart code +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.shareFile(File('${directory.path}/image.jpg')); -``` - -To share multiple files invoke the static `shareFiles` method anywhere in your Dart code -``` dart -Share.shareFiles([File('${directory.path}/image1.jpg', File('${directory.path}/image2.jpg'])); +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/example/lib/main.dart b/packages/share/example/lib/main.dart index 04929fb5a821..e1d246d87fbc 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -93,21 +93,12 @@ class DemoAppState extends State { final RenderBox box = context.findRenderObject(); if (imagePaths.isNotEmpty) { - if (imagePaths.length == 1) { - await Share.shareFile('', - text: text, - subject: subject, - sharePositionOrigin: - box.localToGlobal(Offset.zero) & - box.size); - } else { - await Share.shareFiles(imagePaths, - text: text, - subject: subject, - sharePositionOrigin: - box.localToGlobal(Offset.zero) & - box.size); - } + await Share.shareFiles(imagePaths, + text: text, + subject: subject, + sharePositionOrigin: + box.localToGlobal(Offset.zero) & + box.size); } else { await Share.share(text, subject: subject, diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index 6c78c9b014a8..e20c39a73d2a 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -53,36 +53,6 @@ class Share { return channel.invokeMethod('share', params); } - /// Summons the platform's share sheet to share a single 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( - String path, { - String mimeType, - String subject, - String text, - Rect sharePositionOrigin, - }) { - assert(path != null); - - return shareFiles( - [path], - mimeTypes: [mimeType ?? _mimeTypeForPath(path)], - subject: subject, - text: text, - sharePositionOrigin: sharePositionOrigin, - ); - } - /// Summons the platform's share sheet to share multiple files. /// /// Wraps the platform's native share dialog. Can share a file. From 271cf5e24e4619994966851a8fe36af663fea447 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 26 Aug 2020 08:31:34 +0200 Subject: [PATCH 36/50] [share] example: wrap within SingleChildScrollView --- packages/share/example/lib/main.dart | 154 ++++++++++++++------------- 1 file changed, 78 insertions(+), 76 deletions(-) diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index e1d246d87fbc..2f583c2e6a89 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -32,85 +32,87 @@ class DemoAppState extends State { appBar: AppBar( title: const Text('Share Plugin Demo'), ), - body: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 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: [ + TextField( + decoration: const InputDecoration( + labelText: 'Share text:', + hintText: 'Enter some text and/or link to share', + ), + maxLines: 2, + onChanged: (String value) => setState(() { + text = value; + }), + ), + 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(() { - text = value; - }), - ), - TextField( - decoration: const InputDecoration( - labelText: 'Share subject:', - hintText: 'Enter subject to share (optional)', + const Padding(padding: EdgeInsets.only(top: 12.0)), + _buildImagePreviews(), + 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); + }); + } + }, ), - maxLines: 2, - onChanged: (String value) => setState(() { - subject = value; - }), - ), - const Padding(padding: EdgeInsets.only(top: 12.0)), - _buildImagePreviews(), - 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 - ? null - : () 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(); + const Padding(padding: EdgeInsets.only(top: 12.0)), + Builder( + builder: (BuildContext context) { + return RaisedButton( + child: const Text('Share'), + onPressed: text.isEmpty + ? null + : () 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); - } - }, - ); - }, - ), - ], + 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); + } + }, + ); + }, + ), + ], + ), ), )), ); From dd414d81e88b63f429ced8287f661bf2adf162cc Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 26 Aug 2020 08:48:59 +0200 Subject: [PATCH 37/50] [share] example: Extract methods and widgets --- .../share/example/lib/image_previews.dart | 67 ++++++++++++ packages/share/example/lib/main.dart | 103 +++++------------- 2 files changed, 96 insertions(+), 74 deletions(-) create mode 100644 packages/share/example/lib/image_previews.dart diff --git a/packages/share/example/lib/image_previews.dart b/packages/share/example/lib/image_previews.dart new file mode 100644 index 000000000000..b855d838fb54 --- /dev/null +++ b/packages/share/example/lib/image_previews.dart @@ -0,0 +1,67 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class ImagePreviews extends StatelessWidget { + final List imagePaths; + final Function(int) onDelete; + + const ImagePreviews(this.imagePaths, {Key key, this.onDelete}) + : super(key: key); + + @override + Widget build(BuildContext context) { + if (imagePaths.isEmpty) return Container(); + + List 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: [ + 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 2f583c2e6a89..9ff4e72520e2 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -4,12 +4,12 @@ // ignore_for_file: public_member_api_docs -import 'dart:io'; - 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()); } @@ -59,7 +59,7 @@ class DemoAppState extends State { }), ), const Padding(padding: EdgeInsets.only(top: 12.0)), - _buildImagePreviews(), + ImagePreviews(imagePaths, onDelete: _onDeleteImage), ListTile( leading: Icon(Icons.add), title: Text("Add image"), @@ -80,34 +80,8 @@ class DemoAppState extends State { builder: (BuildContext context) { return RaisedButton( child: const Text('Share'), - onPressed: text.isEmpty - ? null - : () 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); - } - }, + onPressed: + text.isEmpty ? null : () => _onShare(context), ); }, ), @@ -118,50 +92,31 @@ class DemoAppState extends State { ); } - Widget _buildImagePreviews() { - if (imagePaths.isEmpty) return Container(); - - List imageWidgets = []; - for (int i = 0; i < imagePaths.length; i++) { - imageWidgets.add(_buildImagePreview(i)); - } - - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row(children: imageWidgets), - ); + _onDeleteImage(int position) { + setState(() { + imagePaths.removeAt(position); + }); } - Widget _buildImagePreview(int position) { - File imageFile = File(imagePaths[position]); - return Padding( - padding: const EdgeInsets.all(8.0), - child: Stack( - children: [ - 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: () { - 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); + } } } From 4b3b6f8866d5a2b45eba2e33ec4d6a800d826c43 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 26 Aug 2020 08:51:23 +0200 Subject: [PATCH 38/50] [share] Formatting --- packages/share/lib/share.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index e20c39a73d2a..0c13003481d2 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -31,8 +31,7 @@ class Share { /// /// May throw [PlatformException] or [FormatException] /// from [MethodChannel]. - static Future share( - String text, { + static Future share(String text, { String subject, Rect sharePositionOrigin, }) { @@ -65,8 +64,7 @@ class Share { /// /// May throw [PlatformException] or [FormatException] /// from [MethodChannel]. - static Future shareFiles( - List paths, { + static Future shareFiles(List paths, { List mimeTypes, String subject, String text, From b77cc0fffe353c3c6ddf53095092b6e5ecb373eb Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 26 Aug 2020 09:07:35 +0200 Subject: [PATCH 39/50] [share] Formatting --- packages/share/lib/share.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index 0c13003481d2..e20c39a73d2a 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -31,7 +31,8 @@ class Share { /// /// May throw [PlatformException] or [FormatException] /// from [MethodChannel]. - static Future share(String text, { + static Future share( + String text, { String subject, Rect sharePositionOrigin, }) { @@ -64,7 +65,8 @@ class Share { /// /// May throw [PlatformException] or [FormatException] /// from [MethodChannel]. - static Future shareFiles(List paths, { + static Future shareFiles( + List paths, { List mimeTypes, String subject, String text, From 14b9338632133f740f89dd9b8bfe679d3bfa2273 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 26 Aug 2020 09:28:24 +0200 Subject: [PATCH 40/50] [share] Update tests --- packages/share/lib/share.dart | 1 + packages/share/test/share_test.dart | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index e20c39a73d2a..4a3ff6f1de09 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -74,6 +74,7 @@ class Share { }) { assert(paths != null); assert(paths.isNotEmpty); + assert(paths.every((element) => element != null && element.isNotEmpty)); final Map params = { 'paths': paths, 'mimeTypes': mimeTypes ?? diff --git a/packages/share/test/share_test.dart b/packages/share/test/share_test.dart index d5378d9ab262..e862d1baf579 100644 --- a/packages/share/test/share_test.dart +++ b/packages/share/test/share_test.dart @@ -60,7 +60,7 @@ void main() { test('sharing null file fails', () { expect( - () => Share.shareFile(null), + () => Share.shareFiles([null]), throwsA(const TypeMatcher()), ); verifyZeroInteractions(mockChannel); @@ -68,7 +68,7 @@ void main() { test('sharing empty file fails', () { expect( - () => Share.shareFile(null), + () => Share.shareFiles(['']), throwsA(const TypeMatcher()), ); verifyZeroInteractions(mockChannel); @@ -79,7 +79,7 @@ void main() { final File file = File(path); try { file.createSync(); - await Share.shareFile(path); + await Share.shareFiles([path]); verify(mockChannel.invokeMethod('shareFiles', { 'paths': [path], 'mimeTypes': ['image/png'], @@ -94,7 +94,7 @@ void main() { final File file = File(path); try { file.createSync(); - await Share.shareFile(path, mimeType: '*/*'); + await Share.shareFiles([path], mimeTypes: ['*/*']); verify(mockChannel.invokeMethod('shareFiles', { 'paths': [file.path], 'mimeTypes': ['*/*'], From 88e572377ccfc7a874c6bd85f47068aa85da8629 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Wed, 26 Aug 2020 09:58:01 +0200 Subject: [PATCH 41/50] [share] example: Add public member api docs --- packages/share/example/lib/image_previews.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/share/example/lib/image_previews.dart b/packages/share/example/lib/image_previews.dart index b855d838fb54..ca8d3215a8ec 100644 --- a/packages/share/example/lib/image_previews.dart +++ b/packages/share/example/lib/image_previews.dart @@ -3,10 +3,16 @@ 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 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); From 9f1f775029a2ed7ed09bf576d16f8debca90a31e Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 27 Aug 2020 07:53:33 +0200 Subject: [PATCH 42/50] [share] android: handle empty fileUris list --- .../src/main/java/io/flutter/plugins/share/Share.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 d611411fd59e..0b084a826460 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 @@ -72,7 +72,10 @@ void shareFiles(List paths, List mimeTypes, String text, String ArrayList fileUris = getUrisForPaths(paths); Intent shareIntent = new Intent(); - if (fileUris.size() == 1) { + 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( From d93dec4ad307d617aa2a846d9ae1f99d3af8840c Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 27 Aug 2020 08:23:00 +0200 Subject: [PATCH 43/50] [share] android: Handle activity == null --- .../java/io/flutter/plugins/share/Share.java | 30 +++++++++++-------- .../io/flutter/plugins/share/SharePlugin.java | 10 ++++--- 2 files changed, 23 insertions(+), 17 deletions(-) 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 0b084a826460..817aad2a5d11 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,6 +5,7 @@ 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; @@ -24,14 +25,16 @@ /** 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; } @@ -54,12 +57,7 @@ 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 */); - if (activity != null) { - activity.startActivity(chooserIntent); - } else { - chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(chooserIntent); - } + startActivity(chooserIntent); } void shareFiles(List paths, List mimeTypes, String text, String subject) @@ -102,11 +100,17 @@ void shareFiles(List paths, List mimeTypes, String text, String 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 { - chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(chooserIntent); + throw new IllegalStateException("Both context and activity are null"); } } 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 fdb9dc4fe644..b7b6a87e3f08 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,8 @@ 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 +24,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(), binding.getBinaryMessenger()); } @Override @@ -57,9 +59,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); } From 2403c5a0077f675524431b15c28d5b3e40998db8 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 27 Aug 2020 08:23:24 +0200 Subject: [PATCH 44/50] [share] android: do `grantUriPermission` for each file --- .../src/main/java/io/flutter/plugins/share/Share.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 817aad2a5d11..09066004a274 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 @@ -94,10 +94,12 @@ void shareFiles(List paths, List mimeTypes, String text, String .queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolveInfo : resInfoList) { String packageName = resolveInfo.activityInfo.packageName; - activity.grantUriPermission( - packageName, - fileUris.get(0), - Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); + for (Uri fileUri : fileUris) { + activity.grantUriPermission( + packageName, + fileUri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); + } } startActivity(chooserIntent); From 65921258e9a217f3ee6f87b7418322e22017e133 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 27 Aug 2020 08:46:23 +0200 Subject: [PATCH 45/50] [share] android: Fix wrongly callled method (part of activity == null fix) --- .../src/main/java/io/flutter/plugins/share/SharePlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b7b6a87e3f08..aae6c3b64651 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 @@ -29,7 +29,7 @@ public static void registerWith(Registrar registrar) { @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - setUpChannel(binding.getApplicationContext(), binding.getBinaryMessenger()); + setUpChannel(binding.getApplicationContext(),null, binding.getBinaryMessenger()); } @Override From 33b1aa13170c3fe3eaaa5c3c570fc767d44bce46 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 27 Aug 2020 08:46:50 +0200 Subject: [PATCH 46/50] [share] Various small improvements from PR review --- .../java/io/flutter/plugins/share/Share.java | 4 ++- .../share/example/lib/image_previews.dart | 4 ++- packages/share/ios/Classes/FLTSharePlugin.m | 31 ++++++++++--------- 3 files changed, 22 insertions(+), 17 deletions(-) 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 09066004a274..ab2b5f204359 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 @@ -155,7 +155,9 @@ private String reduceMimeTypes(List mimeTypes) { @NonNull private String getMimeTypeBase(String mimeType) { - if (mimeType == null || !mimeType.contains("/")) return "*"; + if (mimeType == null || !mimeType.contains("/")) { + return "*"; + } return mimeType.substring(0, mimeType.indexOf("/")); } diff --git a/packages/share/example/lib/image_previews.dart b/packages/share/example/lib/image_previews.dart index ca8d3215a8ec..61ecec43bdc7 100644 --- a/packages/share/example/lib/image_previews.dart +++ b/packages/share/example/lib/image_previews.dart @@ -18,7 +18,9 @@ class ImagePreviews extends StatelessWidget { @override Widget build(BuildContext context) { - if (imagePaths.isEmpty) return Container(); + if (imagePaths.isEmpty) { + return Container(); + } List imageWidgets = []; for (int i = 0; i < imagePaths.length; i++) { diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m index fe98da56068e..37cb1d4c7aeb 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ b/packages/share/ios/Classes/FLTSharePlugin.m @@ -52,17 +52,17 @@ - (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activity - (id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(UIActivityType)activityType { - if (_path != nil && _mimeType != nil) { - if ([_mimeType hasPrefix:@"image/"]) { - UIImage *image = [UIImage imageWithContentsOfFile:_path]; - return image; - } else { - NSURL *url = [NSURL fileURLWithPath:_path]; - return url; - } + if (!_path || !_mimeType) { + return _text; } - 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 @@ -73,11 +73,12 @@ - (NSString *)activityViewController:(UIActivityViewController *)activityViewCon - (UIImage *)activityViewController:(UIActivityViewController *)activityViewController thumbnailImageForActivityType:(UIActivityType)activityType suggestedSize:(CGSize)suggestedSize { - if (_path != nil && _mimeType != nil && [_mimeType hasPrefix:@"image/"]) { - UIImage *image = [UIImage imageWithContentsOfFile:_path]; - return [self imageWithImage:image scaledToSize:suggestedSize]; + if (!_path || !_mimeType || ![_mimeType hasPrefix:@"image/"]) { + return nil; } - return nil; + + UIImage *image = [UIImage imageWithContentsOfFile:_path]; + return [self imageWithImage:image scaledToSize:suggestedSize]; } - (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize { @@ -105,7 +106,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { NSNumber *originHeight = arguments[@"originHeight"]; CGRect originRect = CGRectZero; - if (originX != nil && originY != nil && originWidth != nil && originHeight != nil) { + if (originX && originY && originWidth && originHeight) { originRect = CGRectMake([originX doubleValue], [originY doubleValue], [originWidth doubleValue], [originHeight doubleValue]); } @@ -189,7 +190,7 @@ + (void)shareFiles:(NSArray *)paths atSource:(CGRect)origin { NSMutableArray *items = [[NSMutableArray alloc] init]; - if (text != nil || subject != nil) { + if (text || subject) { [items addObject:[[ShareData alloc] initWithSubject:subject text:text]]; } From 6bc2f6b634ae2fc1282c0b5832c411aea369a07f Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 27 Aug 2020 08:53:12 +0200 Subject: [PATCH 47/50] [share] android: Improve calls where activity could be null to use context --- .../java/io/flutter/plugins/share/Share.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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 ab2b5f204359..2b8ff2c92d37 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 @@ -89,13 +89,13 @@ void shareFiles(List paths, List mimeTypes, String text, String Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); List resInfoList = - activity + getContext() .getPackageManager() .queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolveInfo : resInfoList) { String packageName = resolveInfo.activityInfo.packageName; for (Uri fileUri : fileUris) { - activity.grantUriPermission( + getContext().grantUriPermission( packageName, fileUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); @@ -126,7 +126,7 @@ private ArrayList getUrisForPaths(List paths) throws IOException { uris.add( FileProvider.getUriForFile( - activity, activity.getPackageName() + ".flutter.share_provider", file)); + getContext(), getContext().getPackageName() + ".flutter.share_provider", file)); } return uris; } @@ -197,7 +197,18 @@ private File copyToExternalShareFolder(File file) throws IOException { @NonNull private File getExternalShareFolder() { - return new File(activity.getExternalCacheDir(), "share"); + 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 { From f00cf550cbb0b8b9f8cf415efe865e843ba7ffe6 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 27 Aug 2020 09:00:39 +0200 Subject: [PATCH 48/50] [share] example: allow sharing of only images --- packages/share/example/lib/main.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index 9ff4e72520e2..d6f1a1622b3c 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -80,8 +80,9 @@ class DemoAppState extends State { builder: (BuildContext context) { return RaisedButton( child: const Text('Share'), - onPressed: - text.isEmpty ? null : () => _onShare(context), + onPressed: text.isEmpty && imagePaths.isEmpty + ? null + : () => _onShare(context), ); }, ), From 7ea12d6895c72aa97512617e5ee2dbf9b7be8739 Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 27 Aug 2020 09:00:55 +0200 Subject: [PATCH 49/50] [share] ios: correct error message --- packages/share/ios/Classes/FLTSharePlugin.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m index 37cb1d4c7aeb..837623a0119a 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ b/packages/share/ios/Classes/FLTSharePlugin.m @@ -143,7 +143,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { for (NSString *path in paths) { if (path.length == 0) { result([FlutterError errorWithCode:@"error" - message:@"Non-empty paths expected" + message:@"Each path must not be empty" details:nil]); return; } From 12aee1f4bb8627990f59c92db193eeeeff48333f Mon Sep 17 00:00:00 2001 From: Patrick Boos Date: Thu, 27 Aug 2020 09:48:12 +0200 Subject: [PATCH 50/50] [share] android: formatting --- .../main/java/io/flutter/plugins/share/Share.java | 15 ++++++++------- .../io/flutter/plugins/share/SharePlugin.java | 3 +-- 2 files changed, 9 insertions(+), 9 deletions(-) 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 2b8ff2c92d37..eb856bf572ee 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 @@ -29,9 +29,9 @@ class Share { private Activity 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)}. + * 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(Context context, Activity activity) { this.context = context; @@ -95,10 +95,11 @@ void shareFiles(List paths, List mimeTypes, String text, String 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); + getContext() + .grantUriPermission( + packageName, + fileUri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); } } 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 aae6c3b64651..bd7dfc22a3cd 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 @@ -6,7 +6,6 @@ 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; @@ -29,7 +28,7 @@ public static void registerWith(Registrar registrar) { @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - setUpChannel(binding.getApplicationContext(),null, binding.getBinaryMessenger()); + setUpChannel(binding.getApplicationContext(), null, binding.getBinaryMessenger()); } @Override