diff --git a/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m b/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m index a3afbfa5c3..76c6b658e7 100644 --- a/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m +++ b/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m @@ -39,6 +39,59 @@ return viewController; } +static NSDictionary *activityTypes; + +static void initializeActivityTypeMapping(void) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *originalTypes = + [[NSMutableDictionary alloc] initWithDictionary:@{ + @"postToFacebook" : UIActivityTypePostToFacebook, + @"postToTwitter" : UIActivityTypePostToTwitter, + @"postToWeibo" : UIActivityTypePostToWeibo, + @"message" : UIActivityTypeMessage, + @"mail" : UIActivityTypeMail, + @"print" : UIActivityTypePrint, + @"copyToPasteboard" : UIActivityTypeCopyToPasteboard, + @"assignToContact" : UIActivityTypeAssignToContact, + @"saveToCameraRoll" : UIActivityTypeSaveToCameraRoll, + @"addToReadingList" : UIActivityTypeAddToReadingList, + @"postToFlickr" : UIActivityTypePostToFlickr, + @"postToVimeo" : UIActivityTypePostToVimeo, + @"postToTencentWeibo" : UIActivityTypePostToTencentWeibo, + @"airDrop" : UIActivityTypeAirDrop, + @"openInIBooks" : UIActivityTypeOpenInIBooks, + @"markupAsPDF" : UIActivityTypeMarkupAsPDF, + }]; + + if (@available(iOS 15.4, *)) { + originalTypes[@"sharePlay"] = UIActivityTypeSharePlay; + } + + if (@available(iOS 16.0, *)) { + originalTypes[@"collaborationInviteWithLink"] = + UIActivityTypeCollaborationInviteWithLink; + } + + if (@available(iOS 16.0, *)) { + originalTypes[@"collaborationCopyLink"] = + UIActivityTypeCollaborationCopyLink; + } + if (@available(iOS 16.4, *)) { + originalTypes[@"addToHomeScreen"] = UIActivityTypeAddToHomeScreen; + } + activityTypes = originalTypes; + }); +} + +static UIActivityType activityTypeForString(NSString *activityTypeString) { + initializeActivityTypeMapping(); + if ([activityTypes.allKeys containsObject:activityTypeString]) { + return activityTypes[activityTypeString]; + } + return nil; +} + // We need the companion to avoid ARC deadlock @interface UIActivityViewSuccessCompanion : NSObject @@ -254,6 +307,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { NSNumber *originY = arguments[@"originY"]; NSNumber *originWidth = arguments[@"originWidth"]; NSNumber *originHeight = arguments[@"originHeight"]; + NSArray *excludedActivityType = arguments[@"excludedActivityType"]; CGRect originRect = CGRectZero; if (originX && originY && originWidth && originHeight) { @@ -284,10 +338,11 @@ + (void)registerWithRegistrar:(NSObject *)registrar { TopViewControllerForViewController(rootViewController); [self shareText:shareText - subject:shareSubject - withController:topViewController - atSource:originRect - toResult:result]; + subject:shareSubject + excludedActivityType:excludedActivityType + withController:topViewController + atSource:originRect + toResult:result]; } else if ([@"shareFiles" isEqualToString:call.method]) { NSArray *paths = arguments[@"paths"]; NSArray *mimeTypes = arguments[@"mimeTypes"]; @@ -320,12 +375,13 @@ + (void)registerWithRegistrar:(NSObject *)registrar { UIViewController *topViewController = TopViewControllerForViewController(rootViewController); [self shareFiles:paths - withMimeType:mimeTypes - withSubject:subject - withText:text - withController:topViewController - atSource:originRect - toResult:result]; + withMimeType:mimeTypes + withSubject:subject + withText:text + excludedActivityType:excludedActivityType + withController:topViewController + atSource:originRect + toResult:result]; } else if ([@"shareUri" isEqualToString:call.method]) { NSString *uri = arguments[@"uri"]; @@ -347,9 +403,10 @@ + (void)registerWithRegistrar:(NSObject *)registrar { TopViewControllerForViewController(rootViewController); [self shareUri:uri - withController:topViewController - atSource:originRect - toResult:result]; + excludedActivityType:excludedActivityType + withController:topViewController + atSource:originRect + toResult:result]; } else { result(FlutterMethodNotImplemented); } @@ -357,10 +414,11 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } + (void)share:(NSArray *)shareItems - withSubject:(NSString *)subject - withController:(UIViewController *)controller - atSource:(CGRect)origin - toResult:(FlutterResult)result { + withSubject:(NSString *)subject + excludedActivityType:(NSArray *)excludedActivityType + withController:(UIViewController *)controller + atSource:(CGRect)origin + toResult:(FlutterResult)result { UIActivityViewSuccessController *activityViewController = [[UIActivityViewSuccessController alloc] initWithActivityItems:shareItems applicationActivities:nil]; @@ -369,7 +427,14 @@ + (void)share:(NSArray *)shareItems if (![subject isKindOfClass:[NSNull class]]) { [activityViewController setValue:subject forKey:@"subject"]; } - + NSMutableArray *excludedActivityTypes = [[NSMutableArray alloc] init]; + for (NSString *type in excludedActivityType) { + UIActivityType activityType = activityTypeForString(type); + if (activityType != nil) { + [excludedActivityTypes addObject:activityType]; + } + } + activityViewController.excludedActivityTypes = excludedActivityTypes; activityViewController.popoverPresentationController.sourceView = controller.view; BOOL isCoordinateSpaceOfSourceView = @@ -413,38 +478,43 @@ + (void)share:(NSArray *)shareItems } + (void)shareUri:(NSString *)uri - withController:(UIViewController *)controller - atSource:(CGRect)origin - toResult:(FlutterResult)result { + excludedActivityType:(NSArray *)excludedActivityType + withController:(UIViewController *)controller + atSource:(CGRect)origin + toResult:(FlutterResult)result { NSURL *data = [NSURL URLWithString:uri]; [self share:@[ data ] - withSubject:nil - withController:controller - atSource:origin - toResult:result]; + withSubject:nil + excludedActivityType:excludedActivityType + withController:controller + atSource:origin + toResult:result]; } + (void)shareText:(NSString *)shareText - subject:(NSString *)subject - withController:(UIViewController *)controller - atSource:(CGRect)origin - toResult:(FlutterResult)result { + subject:(NSString *)subject + excludedActivityType:(NSArray *)excludedActivityType + withController:(UIViewController *)controller + atSource:(CGRect)origin + toResult:(FlutterResult)result { NSObject *data = [[SharePlusData alloc] initWithSubject:subject text:shareText]; [self share:@[ data ] - withSubject:subject - withController:controller - atSource:origin - toResult:result]; + withSubject:subject + excludedActivityType:excludedActivityType + withController:controller + atSource:origin + toResult:result]; } + (void)shareFiles:(NSArray *)paths - withMimeType:(NSArray *)mimeTypes - withSubject:(NSString *)subject - withText:(NSString *)text - withController:(UIViewController *)controller - atSource:(CGRect)origin - toResult:(FlutterResult)result { + withMimeType:(NSArray *)mimeTypes + withSubject:(NSString *)subject + withText:(NSString *)text + excludedActivityType:(NSArray *)excludedActivityType + withController:(UIViewController *)controller + atSource:(CGRect)origin + toResult:(FlutterResult)result { NSMutableArray *items = [[NSMutableArray alloc] init]; for (int i = 0; i < [paths count]; i++) { @@ -460,10 +530,11 @@ + (void)shareFiles:(NSArray *)paths } [self share:items - withSubject:subject - withController:controller - atSource:origin - toResult:result]; + withSubject:subject + excludedActivityType:excludedActivityType + withController:controller + atSource:origin + toResult:result]; } @end diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index 52e50d624d..c8ba1b2440 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -8,7 +8,7 @@ import 'dart:ui'; import 'package:share_plus_platform_interface/share_plus_platform_interface.dart'; export 'package:share_plus_platform_interface/share_plus_platform_interface.dart' - show ShareResult, ShareResultStatus, XFile; + show ShareResult, ShareResultStatus, XFile, CupertinoActivityType; export 'src/share_plus_linux.dart'; export 'src/share_plus_windows.dart' @@ -30,6 +30,10 @@ class Share { /// origin rect for the share sheet to popover from on iPads and Macs. It has no effect /// on other devices. /// + /// The optional [excludedActivityType] parameter is used to exclude services on iOS and macOS that + /// you feel are not suitable for your content. + /// It has no effect on other platforms. + /// /// May throw [PlatformException] /// from [MethodChannel]. /// @@ -37,10 +41,12 @@ class Share { static Future shareUri( Uri uri, { Rect? sharePositionOrigin, + List? excludedActivityType, }) async { return _platform.shareUri( uri, sharePositionOrigin: sharePositionOrigin, + excludedActivityType: excludedActivityType, ); } @@ -57,6 +63,10 @@ class Share { /// origin rect for the share sheet to popover from on iPads and Macs. It has no effect /// on other devices. /// + /// The optional [excludedActivityType] parameter is used to exclude services on iOS and macOS that + /// you feel are not suitable for your content. + /// It has no effect on other platforms. + /// /// May throw [PlatformException] or [FormatException] /// from [MethodChannel]. /// @@ -83,12 +93,14 @@ class Share { String text, { String? subject, Rect? sharePositionOrigin, + List? excludedActivityType, }) async { assert(text.isNotEmpty); return _platform.share( text, subject: subject, sharePositionOrigin: sharePositionOrigin, + excludedActivityType: excludedActivityType, ); } @@ -111,6 +123,10 @@ class Share { /// origin rect for the share sheet to popover from on iPads and Macs. It has no effect /// on other devices. /// + /// The optional [excludedActivityType] parameter is used to exclude services on iOS and macOS that + /// you feel are not suitable for your content. + /// It has no effect on other platforms. + /// /// The optional parameter [fileNameOverrides] can be used to override the names of shared files /// When set, the list length must match the number of [files] to share. /// This is useful when sharing files that were created by [`XFile.fromData`](https://github.com/flutter/packages/blob/754de1918a339270b70971b6841cf1e04dd71050/packages/cross_file/lib/src/types/io.dart#L43), @@ -125,6 +141,7 @@ class Share { String? subject, String? text, Rect? sharePositionOrigin, + List? excludedActivityType, List? fileNameOverrides, }) async { assert(files.isNotEmpty); @@ -133,6 +150,7 @@ class Share { subject: subject, text: text, sharePositionOrigin: sharePositionOrigin, + excludedActivityType: excludedActivityType, fileNameOverrides: fileNameOverrides, ); } diff --git a/packages/share_plus/share_plus/lib/src/share_plus_linux.dart b/packages/share_plus/share_plus/lib/src/share_plus_linux.dart index 03132bddc0..7cc2d5965b 100644 --- a/packages/share_plus/share_plus/lib/src/share_plus_linux.dart +++ b/packages/share_plus/share_plus/lib/src/share_plus_linux.dart @@ -24,6 +24,7 @@ class SharePlusLinuxPlugin extends SharePlatform { String? subject, String? text, Rect? sharePositionOrigin, + List? excludedActivityType, }) async { throw UnimplementedError( 'shareUri() has not been implemented on Linux. Use share().'); @@ -35,6 +36,7 @@ class SharePlusLinuxPlugin extends SharePlatform { String text, { String? subject, Rect? sharePositionOrigin, + List? excludedActivityType, }) async { final queryParameters = { if (subject != null) 'subject': subject, @@ -68,6 +70,7 @@ class SharePlusLinuxPlugin extends SharePlatform { String? subject, String? text, Rect? sharePositionOrigin, + List? excludedActivityType, List? fileNameOverrides, }) { throw UnimplementedError( diff --git a/packages/share_plus/share_plus/lib/src/share_plus_web.dart b/packages/share_plus/share_plus/lib/src/share_plus_web.dart index 45d7968aee..fec316e0e3 100644 --- a/packages/share_plus/share_plus/lib/src/share_plus_web.dart +++ b/packages/share_plus/share_plus/lib/src/share_plus_web.dart @@ -32,6 +32,7 @@ class SharePlusWebPlugin extends SharePlatform { Future shareUri( Uri uri, { Rect? sharePositionOrigin, + List? excludedActivityType, }) async { final data = ShareData( url: uri.toString(), @@ -76,6 +77,7 @@ class SharePlusWebPlugin extends SharePlatform { String text, { String? subject, Rect? sharePositionOrigin, + List? excludedActivityType, }) async { final ShareData data; if (subject != null && subject.isNotEmpty) { @@ -160,6 +162,7 @@ class SharePlusWebPlugin extends SharePlatform { String? text, Rect? sharePositionOrigin, List? fileNameOverrides, + List? excludedActivityType, }) async { assert( fileNameOverrides == null || files.length == fileNameOverrides.length); diff --git a/packages/share_plus/share_plus/lib/src/share_plus_windows.dart b/packages/share_plus/share_plus/lib/src/share_plus_windows.dart index 5a660e4763..b1a7946790 100644 --- a/packages/share_plus/share_plus/lib/src/share_plus_windows.dart +++ b/packages/share_plus/share_plus/lib/src/share_plus_windows.dart @@ -28,6 +28,7 @@ class SharePlusWindowsPlugin extends SharePlatform { Uri uri, { String? subject, String? text, + List? excludedActivityType, Rect? sharePositionOrigin, }) async { throw UnimplementedError( @@ -39,6 +40,7 @@ class SharePlusWindowsPlugin extends SharePlatform { Future share( String text, { String? subject, + List? excludedActivityType, Rect? sharePositionOrigin, }) async { final queryParameters = { @@ -73,6 +75,7 @@ class SharePlusWindowsPlugin extends SharePlatform { String? subject, String? text, Rect? sharePositionOrigin, + List? excludedActivityType, List? fileNameOverrides, }) { throw UnimplementedError( diff --git a/packages/share_plus/share_plus_platform_interface/lib/method_channel/method_channel_share.dart b/packages/share_plus/share_plus_platform_interface/lib/method_channel/method_channel_share.dart index bb7fa696c6..20f3a9ce81 100644 --- a/packages/share_plus/share_plus_platform_interface/lib/method_channel/method_channel_share.dart +++ b/packages/share_plus/share_plus_platform_interface/lib/method_channel/method_channel_share.dart @@ -9,6 +9,7 @@ import 'dart:io'; // ignore: unnecessary_import import 'dart:ui'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show visibleForTesting; import 'package:mime/mime.dart' show extensionFromMime, lookupMimeType; @@ -27,6 +28,7 @@ class MethodChannelShare extends SharePlatform { Future shareUri( Uri uri, { Rect? sharePositionOrigin, + List? excludedActivityType, }) async { final params = {'uri': uri.toString()}; @@ -37,6 +39,11 @@ class MethodChannelShare extends SharePlatform { params['originHeight'] = sharePositionOrigin.height; } + if (excludedActivityType != null) { + final activityTypes = excludedActivityType.map((e) => e.value).toList(); + params['excludedActivityType'] = activityTypes; + } + final result = await channel.invokeMethod('shareUri', params) ?? 'dev.fluttercommunity.plus/share/unavailable'; @@ -49,6 +56,7 @@ class MethodChannelShare extends SharePlatform { String text, { String? subject, Rect? sharePositionOrigin, + List? excludedActivityType, }) async { assert(text.isNotEmpty); final params = { @@ -63,6 +71,11 @@ class MethodChannelShare extends SharePlatform { params['originHeight'] = sharePositionOrigin.height; } + if (excludedActivityType != null) { + final activityTypes = excludedActivityType.map((e) => e.value).toList(); + params['excludedActivityType'] = activityTypes; + } + final result = await channel.invokeMethod('share', params) ?? 'dev.fluttercommunity.plus/share/unavailable'; @@ -76,6 +89,7 @@ class MethodChannelShare extends SharePlatform { String? subject, String? text, Rect? sharePositionOrigin, + List? excludedActivityType, List? fileNameOverrides, }) async { assert(files.isNotEmpty); @@ -109,6 +123,11 @@ class MethodChannelShare extends SharePlatform { params['originHeight'] = sharePositionOrigin.height; } + if (excludedActivityType != null) { + final activityTypes = excludedActivityType.map((e) => e.value).toList(); + params['excludedActivityType'] = activityTypes; + } + final result = await channel.invokeMethod('shareFiles', params) ?? 'dev.fluttercommunity.plus/share/unavailable'; diff --git a/packages/share_plus/share_plus_platform_interface/lib/platform_interface/share_plus_platform.dart b/packages/share_plus/share_plus_platform_interface/lib/platform_interface/share_plus_platform.dart index f1840624ef..18d8821bac 100644 --- a/packages/share_plus/share_plus_platform_interface/lib/platform_interface/share_plus_platform.dart +++ b/packages/share_plus/share_plus_platform_interface/lib/platform_interface/share_plus_platform.dart @@ -35,10 +35,12 @@ class SharePlatform extends PlatformInterface { Future shareUri( Uri uri, { Rect? sharePositionOrigin, + List? excludedActivityType, }) { return _instance.shareUri( uri, sharePositionOrigin: sharePositionOrigin, + excludedActivityType: excludedActivityType, ); } @@ -47,11 +49,13 @@ class SharePlatform extends PlatformInterface { String text, { String? subject, Rect? sharePositionOrigin, + List? excludedActivityType, }) async { return await _instance.share( text, subject: subject, sharePositionOrigin: sharePositionOrigin, + excludedActivityType: excludedActivityType, ); } @@ -61,6 +65,7 @@ class SharePlatform extends PlatformInterface { String? subject, String? text, Rect? sharePositionOrigin, + List? excludedActivityType, List? fileNameOverrides, }) async { return _instance.shareXFiles( @@ -68,6 +73,7 @@ class SharePlatform extends PlatformInterface { subject: subject, text: text, sharePositionOrigin: sharePositionOrigin, + excludedActivityType: excludedActivityType, fileNameOverrides: fileNameOverrides, ); } @@ -126,3 +132,34 @@ enum ShareResultStatus { /// but the user action can not be determined unavailable, } + +/// An abstract class that you subclass to implement app-specific services +/// for iOS and macOS. +/// +/// https://developer.apple.com/documentation/uikit/uiactivity/activitytype +enum CupertinoActivityType { + postToFacebook, + postToTwitter, + postToWeibo, + message, + mail, + print, + copyToPasteboard, + assignToContact, + saveToCameraRoll, + addToReadingList, + postToFlickr, + postToVimeo, + postToTencentWeibo, + airDrop, + openInIBooks, + markupAsPDF, + sharePlay, + collaborationInviteWithLink, + collaborationCopyLink, + addToHomeScreen, +} + +extension Value on CupertinoActivityType { + String get value => toString().split('.').last; +} diff --git a/packages/share_plus/share_plus_platform_interface/test/share_plus_platform_interface_test.dart b/packages/share_plus/share_plus_platform_interface/test/share_plus_platform_interface_test.dart index 03c796bddc..cd4dee00ca 100644 --- a/packages/share_plus/share_plus_platform_interface/test/share_plus_platform_interface_test.dart +++ b/packages/share_plus/share_plus_platform_interface/test/share_plus_platform_interface_test.dart @@ -65,6 +65,7 @@ void main() { await sharePlatform.shareUri( Uri.parse('https://pub.dev/packages/share_plus'), sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + excludedActivityType: [CupertinoActivityType.message], ); verify(mockChannel.invokeMethod('shareUri', { 'uri': 'https://pub.dev/packages/share_plus', @@ -72,12 +73,14 @@ void main() { 'originY': 2.0, 'originWidth': 3.0, 'originHeight': 4.0, + 'excludedActivityType': [CupertinoActivityType.message.value], })); await sharePlatform.share( 'some text to share', subject: 'some subject to share', sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + excludedActivityType: [CupertinoActivityType.message], ); verify(mockChannel.invokeMethod('share', { 'text': 'some text to share', @@ -86,6 +89,7 @@ void main() { 'originY': 2.0, 'originWidth': 3.0, 'originHeight': 4.0, + 'excludedActivityType': [CupertinoActivityType.message.value], })); await withFile('tempfile-83649a.png', (File fd) async { @@ -94,6 +98,7 @@ void main() { subject: 'some subject to share', text: 'some text to share', sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + excludedActivityType: [CupertinoActivityType.message], ); verify(mockChannel.invokeMethod( 'shareFiles', @@ -106,6 +111,7 @@ void main() { 'originY': 2.0, 'originWidth': 3.0, 'originHeight': 4.0, + 'excludedActivityType': [CupertinoActivityType.message.value], }, )); }); @@ -155,6 +161,7 @@ void main() { 'some text to share', subject: 'some subject to share', sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + excludedActivityType: [CupertinoActivityType.message], ); verify(mockChannel.invokeMethod('share', { 'text': 'some text to share', @@ -163,6 +170,7 @@ void main() { 'originY': 2.0, 'originWidth': 3.0, 'originHeight': 4.0, + 'excludedActivityType': [CupertinoActivityType.message.value], })); await withFile('tempfile-83649e.png', (File fd) async {