Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ internal class Share(
shareIntent.apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, uri ?: text)
putExtra(Intent.EXTRA_TEXT, text ?: uri)
if (!subject.isNullOrBlank()) putExtra(Intent.EXTRA_SUBJECT, subject)
if (!title.isNullOrBlank()) putExtra(Intent.EXTRA_TITLE, title)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@
#import "LinkPresentation/LPMetadataProvider.h"

static NSString *const PLATFORM_CHANNEL = @"dev.fluttercommunity.plus/share";
static NSString *const kSharePlusPlaceholderActivityType =
@"dev.fluttercommunity.share_plus.placeholder";

/// Apps that open or save a file. Text and multimedia apps are everything else.
static BOOL FPPSharePlusIsFileDestination(NSString *activityType) {
if (activityType.length == 0) {
return NO;
}
return [activityType isEqualToString:UIActivityTypeAirDrop] ||
[activityType isEqualToString:UIActivityTypeCopyToPasteboard] ||
[activityType isEqualToString:UIActivityTypePrint] ||
[activityType isEqualToString:UIActivityTypeMarkupAsPDF] ||
[activityType isEqualToString:UIActivityTypeOpenInIBooks] ||
[activityType isEqualToString:UIActivityTypeSaveToCameraRoll] ||
[activityType
isEqualToString:@"com.apple.DocumentManagerUICore.SaveToFiles"];
}

static UIViewController *RootViewController(void) {
if (@available(iOS 13, *)) { // UIApplication.keyWindow is deprecated
Expand Down Expand Up @@ -108,10 +125,22 @@ @interface SharePlusData : NSObject <UIActivityItemSource>
@property(readonly, nonatomic, copy) NSString *text;
@property(readonly, nonatomic, copy) NSString *path;
@property(readonly, nonatomic, copy) NSString *mimeType;
@property(readonly, nonatomic, copy) NSString *url;
@property(readonly, nonatomic, assign) BOOL airDropAsUrl;
@property(readonly, nonatomic, assign) BOOL messageAndMailOnly;

- (instancetype)initWithSubject:(NSString *)subject
text:(NSString *)text NS_DESIGNATED_INITIALIZER;

- (instancetype)initWithSubject:(NSString *)subject
text:(NSString *)text
messageAndMailOnly:(BOOL)messageAndMailOnly NS_DESIGNATED_INITIALIZER;

- (instancetype)initWithSubject:(NSString *)subject
text:(NSString *)text
url:(NSString *)url
airDropAsUrl:(BOOL)airDropAsUrl NS_DESIGNATED_INITIALIZER;

- (instancetype)initWithFile:(NSString *)path
mimeType:(NSString *)mimeType NS_DESIGNATED_INITIALIZER;

Expand All @@ -132,10 +161,29 @@ - (instancetype)init {
}

- (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text {
return [self initWithSubject:subject text:text messageAndMailOnly:NO];
}

- (instancetype)initWithSubject:(NSString *)subject
text:(NSString *)text
messageAndMailOnly:(BOOL)messageAndMailOnly {
self = [self initWithSubject:subject text:text url:nil airDropAsUrl:NO];
if (self) {
_messageAndMailOnly = messageAndMailOnly;
}
return self;
}

- (instancetype)initWithSubject:(NSString *)subject
text:(NSString *)text
url:(NSString *)url
airDropAsUrl:(BOOL)airDropAsUrl {
self = [super init];
if (self) {
_subject = [subject isKindOfClass:NSNull.class] ? @"" : subject;
_text = text;
_url = url;
_airDropAsUrl = airDropAsUrl;
}
return self;
}
Expand Down Expand Up @@ -165,19 +213,38 @@ - (id)activityViewControllerPlaceholderItem:
(UIActivityViewController *)activityViewController {
return [self
activityViewController:activityViewController
itemForActivityType:@"dev.fluttercommunity.share_plus.placeholder"];
itemForActivityType:kSharePlusPlaceholderActivityType];
}

- (id)activityViewController:(UIActivityViewController *)activityViewController
itemForActivityType:(UIActivityType)activityType {
if (!_path || !_mimeType) {
// Caption for text and multimedia apps. nil (not NSNull) so the header
// and file apps do not count a Plain Text item. Unknown types, including
// Notes, still receive the string.
if (_messageAndMailOnly) {
if (activityType == nil ||
[activityType isEqualToString:kSharePlusPlaceholderActivityType] ||
FPPSharePlusIsFileDestination(activityType)) {
return nil;
}
return _text;
}
// Placeholder stays text so the sheet uses the host app icon, not Safari.
// AirDrop gets the URL so the other device opens the link. Messages,
// Mail, and the placeholder stay on text so the sheet keeps the host
// app icon and SMS/email still receive the sentence.
if (_airDropAsUrl && _url.length > 0 &&
[activityType isEqualToString:UIActivityTypeAirDrop]) {
return [NSURL URLWithString:_url];
}
return _text;
}

// If the shared file is an image return an UIImage for the placeholder
// to show a preview.
if ([activityType
isEqualToString:@"dev.fluttercommunity.share_plus.placeholder"] &&
isEqualToString:kSharePlusPlaceholderActivityType] &&
[_mimeType hasPrefix:@"image/"]) {
UIImage *image = [UIImage imageWithContentsOfFile:_path];
return image;
Expand Down Expand Up @@ -292,6 +359,7 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
NSArray *paths = arguments[@"paths"];
NSArray *mimeTypes = arguments[@"mimeTypes"];
NSString *uri = arguments[@"uri"];
NSString *airDrop = arguments[@"airDrop"];

// Use title field for consistency with Android.
// Subject field should only be used on email subjects.
Expand Down Expand Up @@ -367,6 +435,9 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {

if (uri) {
[self shareUri:uri
airDrop:airDrop
text:shareText
subject:shareTitle
excludedActivityTypes:excludedActivityTypes
withController:topViewController
atSource:originRect
Expand Down Expand Up @@ -462,13 +533,46 @@ + (void)share:(NSArray *)shareItems
}

+ (void)shareUri:(NSString *)uri
airDrop:(NSString *)airDrop
text:(NSString *)text
subject:(NSString *)subject
excludedActivityTypes:(NSArray<UIActivityType> *)excludedActivityTypes
withController:(UIViewController *)controller
atSource:(CGRect)origin
toResult:(FlutterResult)result {
if ([airDrop isEqualToString:@"url"]) {
NSString *body = text;
if (!body || [body isKindOfClass:[NSNull class]] || body.length == 0) {
body = subject;
}
if (!body || [body isKindOfClass:[NSNull class]] || body.length == 0) {
body = uri;
}
NSObject *data = [[SharePlusData alloc] initWithSubject:subject
text:body
url:uri
airDropAsUrl:YES];
[self share:@[ data ]
withSubject:subject
excludedActivityTypes:excludedActivityTypes
withController:controller
atSource:origin
toResult:result];
return;
}
if ([airDrop isEqualToString:@"text"]) {
NSObject *data = [[SharePlusData alloc] initWithSubject:subject text:uri];
[self share:@[ data ]
withSubject:subject
excludedActivityTypes:excludedActivityTypes
withController:controller
atSource:origin
toResult:result];
return;
}
NSURL *data = [NSURL URLWithString:uri];
[self share:@[ data ]
withSubject:nil
withSubject:subject
excludedActivityTypes:excludedActivityTypes
withController:controller
atSource:origin
Expand Down Expand Up @@ -509,7 +613,9 @@ + (void)shareFiles:(NSArray *)paths
subject:subject]];
}
if (text != nil) {
NSObject *data = [[SharePlusData alloc] initWithSubject:subject text:text];
NSObject *data = [[SharePlusData alloc] initWithSubject:subject
text:text
messageAndMailOnly:YES];
[items addObject:data];
}

Expand Down
9 changes: 7 additions & 2 deletions packages/share_plus/share_plus/lib/share_plus.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export 'package:share_plus_platform_interface/share_plus_platform_interface.dart
ShareResultStatus,
XFile,
ShareParams,
ShareAirDropAs,
CupertinoActivityType;

export 'src/share_plus_linux.dart';
Expand Down Expand Up @@ -79,8 +80,12 @@ class SharePlus {
);
}

if (params.uri != null && params.text != null) {
throw ArgumentError('uri and text cannot be provided at the same time');
if (params.uri != null &&
params.text != null &&
params.airDrop != ShareAirDropAs.url) {
throw ArgumentError(
'uri and text cannot be provided at the same time unless airDrop is ShareAirDropAs.url',
);
}

if (params.text != null && params.text!.isEmpty) {
Expand Down
10 changes: 5 additions & 5 deletions packages/share_plus/share_plus/lib/src/share_plus_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ class SharePlusWebPlugin extends SharePlatform {
);
}

if (uri != null && text != null) {
throw ArgumentError('Only one of uri or text can be provided');
}

if (uri != null) {
if (uri != null && text != null && title != null) {
data = ShareData(text: text, title: title);
} else if (uri != null && text != null) {
data = ShareData(text: text);
} else if (uri != null) {
data = ShareData(url: uri);
} else if (webFiles.isNotEmpty && text != null && title != null) {
data = ShareData(text: text, title: title, files: webFiles.toJS);
Expand Down
17 changes: 17 additions & 0 deletions packages/share_plus/share_plus/test/share_plus_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ void main() {
},
);

test(
'share allows uri and text when airDrop is url',
() async {
final params = ShareParams(
uri: Uri.parse('https://example.com'),
text: 'Open this link https://example.com',
title: 'Open this link',
airDrop: ShareAirDropAs.url,
);
final result = await sharePlus.share(params);
expect(result, ShareResult.unavailable);
expect(fakePlatform.lastParams?.uri, params.uri);
expect(fakePlatform.lastParams?.text, params.text);
expect(fakePlatform.lastParams?.airDrop, ShareAirDropAs.url);
},
);

test('share throws ArgumentError if text is empty', () async {
expect(
() => sharePlus.share(ShareParams(text: '')),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class MethodChannelShare extends SharePlatform {
if (params.subject != null) 'subject': params.subject,
if (params.title != null) 'title': params.title,
if (params.uri != null) 'uri': params.uri.toString(),
if (params.airDrop != ShareAirDropAs.inherit)
'airDrop': params.airDrop.name,
};

if (params.sharePositionOrigin != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ class SharePlatform extends PlatformInterface {
class ShareParams {
/// The text to share
///
/// Cannot be provided at the same time as [uri],
/// as the share method will use one or the other.
/// Can be provided with [uri] when [airDrop] is [ShareAirDropAs.url]:
/// Messages, Mail, and other text destinations receive this string, while
/// AirDrop still receives the URL.
///
/// Can be used together with [files],
/// but it depends on the receiving app if they support
/// loading files and text from a share action.
/// Some apps only support one or the other.
/// Can be used together with [files]. On iOS that string is a caption for
/// text and multimedia destinations (Messages, Mail, Notes, and other
/// share extensions). File destinations (Save to Files, AirDrop, Copy,
/// Print, Books, Camera Roll, Markup as PDF) and the sheet header do not
/// receive it. Android puts it in EXTRA_TEXT beside the file.
///
/// * Supported platforms: All
final String? text;
Expand Down Expand Up @@ -94,13 +96,17 @@ class ShareParams {
///
/// On other platforms it behaves like sharing text.
///
/// Cannot be used in combination with [text].
/// Can be used with [text] when [airDrop] is [ShareAirDropAs.url].
///
/// * Supported platforms: iOS, Android
/// Falls back to sharing the URI as text on other platforms.
final Uri? uri;

/// Share multiple files, can be used in combination with [text]
/// Share multiple files, can be used in combination with [text].
///
/// On iOS, [text] goes to text and multimedia destinations. Save to Files,
/// AirDrop, Copy, Print, Books, Camera Roll, Markup as PDF, and the sheet
/// header stay file-only. Unknown activity types receive [text].
///
/// Android supports all natively available MIME types (wildcards like image/*
/// are also supported) and it's considered best practice to avoid mixing
Expand Down Expand Up @@ -145,6 +151,22 @@ class ShareParams {
/// Parameter ignored on other platforms.
final List<CupertinoActivityType>? excludedCupertinoActivities;

/// How AirDrop should deliver [uri] on iOS.
///
/// * [ShareAirDropAs.inherit] keeps the current URI share (raw `NSURL`,
/// Safari / page preview on the sheet).
/// * [ShareAirDropAs.url] wraps the URI like a text share so the host app
/// icon stays in the header. AirDrop receives the URL. Messages, Mail,
/// and other destinations receive [text] when it is set.
/// * [ShareAirDropAs.text] sends the URI string as plain text (Notes).
///
/// Android has no AirDrop. [uri] + [title] already go out as EXTRA_TEXT /
/// EXTRA_TITLE (Nearby Share equivalent). This parameter is ignored there.
///
/// * Supported platforms: iOS
/// Parameter ignored on other platforms.
final ShareAirDropAs airDrop;

ShareParams({
this.text,
this.subject,
Expand All @@ -157,6 +179,7 @@ class ShareParams {
this.downloadFallbackEnabled = true,
this.mailToFallbackEnabled = true,
this.excludedCupertinoActivities,
this.airDrop = ShareAirDropAs.inherit,
});
}

Expand Down Expand Up @@ -222,6 +245,18 @@ enum ShareResultStatus {
///
/// See also:
/// [UIActivity.ActivityType](https://developer.apple.com/documentation/uikit/uiactivity/activitytype)
/// How iOS AirDrop should deliver a shared [ShareParams.uri].
enum ShareAirDropAs {
/// Keep the current URI-share behaviour (raw URL item).
inherit,

/// Deliver the URI as a URL (opens the link instead of Notes).
url,

/// Deliver the URI string as plain text.
text,
}

enum CupertinoActivityType {
postToFacebook,
postToTwitter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
// found in the LICENSE file.

export 'package:cross_file/cross_file.dart';

export 'platform_interface/share_plus_platform.dart';
Loading
Loading