From b81f01bed1c082cff415e9f0f2b297dbd3a846af Mon Sep 17 00:00:00 2001 From: Yongle Date: Sat, 18 Jan 2025 08:22:26 +0800 Subject: [PATCH 1/2] feat(share_plus): Add the intent options for the Android platform --- packages/share_plus/share_plus/README.md | 20 ++ .../plus/share/MethodCallHandler.kt | 26 +++ .../dev/fluttercommunity/plus/share/Share.kt | 9 +- .../share_plus/example/lib/main.dart | 66 ++++-- .../share_plus/share_plus/lib/share_plus.dart | 65 +++--- .../share_plus/lib/src/share_plus_linux.dart | 35 ++-- .../share_plus/lib/src/share_plus_web.dart | 28 ++- .../lib/src/share_plus_windows.dart | 35 ++-- .../method_channel/method_channel_share.dart | 51 +++-- .../lib/options/android_intent_flag.dart | 196 ++++++++++++++++++ .../lib/options/android_intent_options.dart | 43 ++++ .../lib/options/platform_options.dart | 7 + .../share_plus_platform.dart | 33 +-- .../lib/share_plus_platform_interface.dart | 3 + .../share_plus_platform_interface_test.dart | 133 +++++++----- 15 files changed, 562 insertions(+), 188 deletions(-) create mode 100644 packages/share_plus/share_plus_platform_interface/lib/options/android_intent_flag.dart create mode 100644 packages/share_plus/share_plus_platform_interface/lib/options/android_intent_options.dart create mode 100644 packages/share_plus/share_plus_platform_interface/lib/options/platform_options.dart diff --git a/packages/share_plus/share_plus/README.md b/packages/share_plus/share_plus/README.md index 282bea03a7..5a3eca91ea 100644 --- a/packages/share_plus/share_plus/README.md +++ b/packages/share_plus/share_plus/README.md @@ -138,6 +138,26 @@ All three methods return a `ShareResult` object which contains the following inf Note: `status` will be `ShareResultStatus.unavailable` if the platform does not support identifying the user action. +## Platform-Specific Features + +Use `PlatformOptions` to configure platform features. + +### Android + +Supports fine-grained configuration of intent in Android. You can use `AndroidIntentOptions` to specify the app to share. + +```dart + Share.share('check out my website https://example.com', platformOptions:PlatformOptions( + androidIntentOptions: AndroidIntentOptions( + packageName: 'com.example.app', + componentName: "com.example.app.ShareActivity", + flags: [ + AndroidIntentFlag.FLAG_ACTIVITY_CLEAR_TOP, + AndroidIntentFlag.FLAG_ACTIVITY_NEW_TASK, + ] + )); +``` + ## Known Issues ### Sharing data created with XFile.fromData diff --git a/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/MethodCallHandler.kt b/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/MethodCallHandler.kt index d4488922e5..b02a619048 100644 --- a/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/MethodCallHandler.kt +++ b/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/MethodCallHandler.kt @@ -1,5 +1,7 @@ package dev.fluttercommunity.plus.share +import android.content.ComponentName +import android.content.Intent import android.os.Build import io.flutter.BuildConfig import io.flutter.plugin.common.MethodCall @@ -29,6 +31,7 @@ internal class MethodCallHandler( call.argument("uri") as String, subject = null, withResult = isWithResult, + shareIntent = obtainIntent(call) ) success(isWithResult, result) } @@ -38,6 +41,7 @@ internal class MethodCallHandler( call.argument("text") as String, call.argument("subject") as String?, isWithResult, + shareIntent = obtainIntent(call) ) success(isWithResult, result) } @@ -49,6 +53,7 @@ internal class MethodCallHandler( call.argument("text"), call.argument("subject"), isWithResult, + shareIntent = obtainIntent(call) ) success(isWithResult, result) } @@ -61,6 +66,27 @@ internal class MethodCallHandler( } } + private fun obtainIntent(call: MethodCall): Intent { + val flags = call.argument("flags") + val packageName = call.argument("packageName") + val componentName = call.argument("componentName") + + return Intent().apply { + if (flags != null) { + addFlags(flags); + } + when { + !componentName.isNullOrBlank() && !packageName.isNullOrBlank() -> { + setComponent(ComponentName(packageName, componentName)) + } + + !packageName.isNullOrBlank() -> { + setPackage(packageName) + } + } + } + } + private fun success( isWithResult: Boolean, result: MethodChannel.Result diff --git a/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/Share.kt b/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/Share.kt index 59bdb1e914..defa95e8a9 100644 --- a/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/Share.kt +++ b/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/Share.kt @@ -55,8 +55,8 @@ internal class Share( this.activity = activity } - fun share(text: String, subject: String?, withResult: Boolean) { - val shareIntent = Intent().apply { + fun share(text: String, subject: String?, withResult: Boolean,shareIntent: Intent) { + shareIntent.apply { action = Intent.ACTION_SEND type = "text/plain" putExtra(Intent.EXTRA_TEXT, text) @@ -90,14 +90,13 @@ internal class Share( mimeTypes: List?, text: String?, subject: String?, - withResult: Boolean + withResult: Boolean,shareIntent: Intent ) { clearShareCacheFolder() val fileUris = getUrisForPaths(paths) - val shareIntent = Intent() when { (fileUris.isEmpty() && !text.isNullOrBlank()) -> { - share(text, subject, withResult) + share(text, subject, withResult, shareIntent) return } diff --git a/packages/share_plus/share_plus/example/lib/main.dart b/packages/share_plus/share_plus/example/lib/main.dart index 913a6e33ce..4c91713d26 100644 --- a/packages/share_plus/share_plus/example/lib/main.dart +++ b/packages/share_plus/share_plus/example/lib/main.dart @@ -38,6 +38,8 @@ class DemoAppState extends State { String subject = ''; String uri = ''; String fileName = ''; + String packageName = ''; + String componentName = ''; List imageNames = []; List imagePaths = []; @@ -107,6 +109,7 @@ class DemoAppState extends State { setState(() => fileName = value); }, ), + if(Platform.isAndroid) ...buildAndroidOptions(), const SizedBox(height: 16), ImagePreviews(imagePaths, onDelete: _onDeleteImage), ElevatedButton.icon( @@ -197,6 +200,37 @@ class DemoAppState extends State { ); } + List buildAndroidOptions() { + return [ + const SizedBox(height: 16), + Text("Android Platform(Optional)"), + const SizedBox(height: 16), + TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Target package name', + hintText: 'The target package name is available on Android.', + ), + maxLines: null, + onChanged: (String value) { + setState(() => packageName = value); + }, + ), + const SizedBox(height: 16), + TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Target component name', + hintText: + 'The target component name is available on Android.', + ), + maxLines: null, + onChanged: (String value) { + setState(() => componentName = value); + }, + )]; + } + void _onDeleteImage(int position) { setState(() { imagePaths.removeAt(position); @@ -216,28 +250,30 @@ class DemoAppState extends State { final scaffoldMessenger = ScaffoldMessenger.of(context); ShareResult shareResult; + final platformOptions = PlatformOptions( + androidIntentOptions: AndroidIntentOptions( + packageName: packageName, + componentName: componentName, + )); if (imagePaths.isNotEmpty) { final files = []; for (var i = 0; i < imagePaths.length; i++) { files.add(XFile(imagePaths[i], name: imageNames[i])); } - shareResult = await Share.shareXFiles( - files, - text: text, - subject: subject, - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ); + shareResult = await Share.shareXFiles(files, + text: text, + subject: subject, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + platformOptions: platformOptions); } else if (uri.isNotEmpty) { - shareResult = await Share.shareUri( - Uri.parse(uri), - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ); + shareResult = await Share.shareUri(Uri.parse(uri), + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + platformOptions: platformOptions); } else { - shareResult = await Share.share( - text, - subject: subject, - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ); + shareResult = await Share.share(text, + subject: subject, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + platformOptions: platformOptions); } scaffoldMessenger.showSnackBar(getResultSnackBar(shareResult)); } diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index e6e2f64a8b..439224068a 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -8,7 +8,13 @@ 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, + PlatformOptions, + AndroidIntentOptions, + AndroidIntentFlag; export 'src/share_plus_linux.dart'; export 'src/share_plus_windows.dart' @@ -37,14 +43,11 @@ class Share { /// from [MethodChannel]. /// /// See documentation about [ShareResult] on [share] method. - static Future shareUri( - Uri uri, { - Rect? sharePositionOrigin, - }) async { - return _platform.shareUri( - uri, - sharePositionOrigin: sharePositionOrigin, - ); + static Future shareUri(Uri uri, + {Rect? sharePositionOrigin, PlatformOptions? platformOptions}) async { + return _platform.shareUri(uri, + sharePositionOrigin: sharePositionOrigin, + platformOptions: platformOptions); } /// Summons the platform's share sheet to share text. @@ -82,17 +85,15 @@ class Share { /// /// Will gracefully fall back to the non result variant if not implemented /// for the current environment and return [ShareResult.unavailable]. - static Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) async { + static Future share(String text, + {String? subject, + Rect? sharePositionOrigin, + PlatformOptions? platformOptions}) async { assert(text.isNotEmpty); - return _platform.share( - text, - subject: subject, - sharePositionOrigin: sharePositionOrigin, - ); + return _platform.share(text, + subject: subject, + sharePositionOrigin: sharePositionOrigin, + platformOptions: platformOptions); } /// Summons the platform's share sheet to share multiple files. @@ -123,20 +124,18 @@ class Share { /// from [MethodChannel]. /// /// See documentation about [ShareResult] on [share] method. - static Future shareXFiles( - List files, { - String? subject, - String? text, - Rect? sharePositionOrigin, - List? fileNameOverrides, - }) async { + static Future shareXFiles(List files, + {String? subject, + String? text, + Rect? sharePositionOrigin, + List? fileNameOverrides, + PlatformOptions? platformOptions}) async { assert(files.isNotEmpty); - return _platform.shareXFiles( - files, - subject: subject, - text: text, - sharePositionOrigin: sharePositionOrigin, - fileNameOverrides: fileNameOverrides, - ); + return _platform.shareXFiles(files, + subject: subject, + text: text, + sharePositionOrigin: sharePositionOrigin, + fileNameOverrides: fileNameOverrides, + platformOptions: platformOptions); } } 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..4fccd126a3 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 @@ -1,5 +1,5 @@ /// The Linux implementation of `share_plus`. -library share_plus_linux; +library; import 'dart:ui'; @@ -19,23 +19,21 @@ class SharePlusLinuxPlugin extends SharePlatform { } @override - Future shareUri( - Uri uri, { - String? subject, - String? text, - Rect? sharePositionOrigin, - }) async { + Future shareUri(Uri uri, + {String? subject, + String? text, + Rect? sharePositionOrigin, + PlatformOptions? platformOptions}) async { throw UnimplementedError( 'shareUri() has not been implemented on Linux. Use share().'); } /// Share text. @override - Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) async { + Future share(String text, + {String? subject, + Rect? sharePositionOrigin, + PlatformOptions? platformOptions}) async { final queryParameters = { if (subject != null) 'subject': subject, 'body': text, @@ -63,13 +61,12 @@ class SharePlusLinuxPlugin extends SharePlatform { /// Share [XFile] objects with Result. @override - Future shareXFiles( - List files, { - String? subject, - String? text, - Rect? sharePositionOrigin, - List? fileNameOverrides, - }) { + Future shareXFiles(List files, + {String? subject, + String? text, + Rect? sharePositionOrigin, + List? fileNameOverrides, + PlatformOptions? platformOptions}) { throw UnimplementedError( 'shareXFiles() has not been implemented on Linux.', ); 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 d0456d327c..630c1d8d45 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 @@ -30,10 +30,8 @@ class SharePlusWebPlugin extends SharePlatform { }) : _navigator = debugNavigator ?? window.navigator; @override - Future shareUri( - Uri uri, { - Rect? sharePositionOrigin, - }) async { + Future shareUri(Uri uri, + {Rect? sharePositionOrigin, PlatformOptions? platformOptions}) async { final data = ShareData( url: uri.toString(), ); @@ -73,11 +71,10 @@ class SharePlusWebPlugin extends SharePlatform { } @override - Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) async { + Future share(String text, + {String? subject, + Rect? sharePositionOrigin, + PlatformOptions? platformOptions}) async { final ShareData data; if (subject != null && subject.isNotEmpty) { data = ShareData( @@ -155,13 +152,12 @@ class SharePlusWebPlugin extends SharePlatform { /// available. This builds on the /// [`cross_file`](https://pub.dev/packages/cross_file) package. @override - Future shareXFiles( - List files, { - String? subject, - String? text, - Rect? sharePositionOrigin, - List? fileNameOverrides, - }) async { + Future shareXFiles(List files, + {String? subject, + String? text, + Rect? sharePositionOrigin, + List? fileNameOverrides, + PlatformOptions? platformOptions}) async { assert( fileNameOverrides == null || files.length == fileNameOverrides.length); final webFiles = []; 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..2b320f600c 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 @@ -1,5 +1,5 @@ /// The Windows implementation of `share_plus`. -library share_plus_windows; +library; import 'dart:ui'; @@ -24,23 +24,21 @@ class SharePlusWindowsPlugin extends SharePlatform { } @override - Future shareUri( - Uri uri, { - String? subject, - String? text, - Rect? sharePositionOrigin, - }) async { + Future shareUri(Uri uri, + {String? subject, + String? text, + Rect? sharePositionOrigin, + PlatformOptions? platformOptions}) async { throw UnimplementedError( 'shareUri() has not been implemented on Windows. Use share().'); } /// Share text. @override - Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) async { + Future share(String text, + {String? subject, + Rect? sharePositionOrigin, + PlatformOptions? platformOptions}) async { final queryParameters = { if (subject != null) 'subject': subject, 'body': text, @@ -68,13 +66,12 @@ class SharePlusWindowsPlugin extends SharePlatform { /// Share [XFile] objects with Result. @override - Future shareXFiles( - List files, { - String? subject, - String? text, - Rect? sharePositionOrigin, - List? fileNameOverrides, - }) { + Future shareXFiles(List files, + {String? subject, + String? text, + Rect? sharePositionOrigin, + List? fileNameOverrides, + PlatformOptions? platformOptions}) { throw UnimplementedError( 'shareXFiles() is only available for Windows versions higher than 10.0.${VersionHelper.kWindows10RS5BuildNumber}.', ); 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..324aac72f3 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/foundation.dart' show defaultTargetPlatform; import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show visibleForTesting; import 'package:mime/mime.dart' show extensionFromMime, lookupMimeType; @@ -27,15 +28,12 @@ class MethodChannelShare extends SharePlatform { Future shareUri( Uri uri, { Rect? sharePositionOrigin, + PlatformOptions? platformOptions, }) async { final params = {'uri': uri.toString()}; - if (sharePositionOrigin != null) { - params['originX'] = sharePositionOrigin.left; - params['originY'] = sharePositionOrigin.top; - params['originWidth'] = sharePositionOrigin.width; - params['originHeight'] = sharePositionOrigin.height; - } + _addSharePositionOriginParams(params, sharePositionOrigin); + _addAndroidPlatformParams(params, platformOptions); final result = await channel.invokeMethod('shareUri', params) ?? 'dev.fluttercommunity.plus/share/unavailable'; @@ -49,6 +47,7 @@ class MethodChannelShare extends SharePlatform { String text, { String? subject, Rect? sharePositionOrigin, + PlatformOptions? platformOptions, }) async { assert(text.isNotEmpty); final params = { @@ -56,12 +55,8 @@ class MethodChannelShare extends SharePlatform { 'subject': subject, }; - if (sharePositionOrigin != null) { - params['originX'] = sharePositionOrigin.left; - params['originY'] = sharePositionOrigin.top; - params['originWidth'] = sharePositionOrigin.width; - params['originHeight'] = sharePositionOrigin.height; - } + _addSharePositionOriginParams(params, sharePositionOrigin); + _addAndroidPlatformParams(params, platformOptions); final result = await channel.invokeMethod('share', params) ?? 'dev.fluttercommunity.plus/share/unavailable'; @@ -77,6 +72,7 @@ class MethodChannelShare extends SharePlatform { String? text, Rect? sharePositionOrigin, List? fileNameOverrides, + PlatformOptions? platformOptions, }) async { assert(files.isNotEmpty); assert( @@ -102,12 +98,8 @@ class MethodChannelShare extends SharePlatform { 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; - } + _addSharePositionOriginParams(params, sharePositionOrigin); + _addAndroidPlatformParams(params, platformOptions); final result = await channel.invokeMethod('shareFiles', params) ?? 'dev.fluttercommunity.plus/share/unavailable'; @@ -192,4 +184,27 @@ class MethodChannelShare extends SharePlatform { return ShareResultStatus.success; } } + + void _addAndroidPlatformParams( + Map params, PlatformOptions? platformOptions) { + if (defaultTargetPlatform != TargetPlatform.android) { + return; + } + final androidIntentOptions = platformOptions?.androidIntentOptions; + if (androidIntentOptions != null) { + params['componentName'] = androidIntentOptions.componentName; + params['flags'] = androidIntentOptions.flags; + params['packageName'] = androidIntentOptions.packageName; + } + } + + void _addSharePositionOriginParams( + Map params, Rect? sharePositionOrigin) { + if (sharePositionOrigin != null) { + params['originX'] = sharePositionOrigin.left; + params['originY'] = sharePositionOrigin.top; + params['originWidth'] = sharePositionOrigin.width; + params['originHeight'] = sharePositionOrigin.height; + } + } } diff --git a/packages/share_plus/share_plus_platform_interface/lib/options/android_intent_flag.dart b/packages/share_plus/share_plus_platform_interface/lib/options/android_intent_flag.dart new file mode 100644 index 0000000000..d806881d2a --- /dev/null +++ b/packages/share_plus/share_plus_platform_interface/lib/options/android_intent_flag.dart @@ -0,0 +1,196 @@ +// ignore_for_file: constant_identifier_names + +/// Special flags that can be set on an intent to control how it is handled. +/// +/// See +/// https://developer.android.com/reference/android/content/Intent.html#setFlags(int) +/// for the official documentation on Intent flags. The constants here mirror +/// the existing [android.content.Intent] ones. +abstract class AndroidIntentFlag { + /// Specifies how an activity should be launched. Generally set by the system + /// in conjunction with SINGLE_TASK. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_BROUGHT_TO_FRONT. + static const int FLAG_ACTIVITY_BROUGHT_TO_FRONT = 4194304; + + /// Causes any existing tasks associated with the activity to be cleared. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_TASK + static const int FLAG_ACTIVITY_CLEAR_TASK = 32768; + + /// Closes any activities on top of this activity and brings it to the front, + /// if it's currently running. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_TOP + static const int FLAG_ACTIVITY_CLEAR_TOP = 67108864; + + /// @deprecated Use [FLAG_ACTIVITY_NEW_DOCUMENT] instead when on API 21 or above. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET + // ignore: provide_deprecation_message + @deprecated + static const int FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET = 524288; + + /// Keeps the activity from being listed with other recently launched + /// activities. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + static const int FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS = 8388608; + + /// Forwards the result from this activity to the existing one. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_FORWARD_RESULT + static const int FLAG_ACTIVITY_FORWARD_RESULT = 33554432; + + /// Generally set by the system if the activity is being launched from + /// history. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY + static const int FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY = 1048576; + + /// Used in split-screen mode to set the launched activity adjacent to the + /// launcher. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_LAUNCH_ADJACENT + static const int FLAG_ACTIVITY_LAUNCH_ADJACENT = 4096; + + /// Used in split-screen mode to set the launched activity adjacent to the + /// launcher. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_MATCH_EXTERNAL + static const int FLAG_ACTIVITY_MATCH_EXTERNAL = 2048; + + /// Creates and launches the activity into a new task. Should always be + /// combined with [FLAG_ACTIVITY_NEW_DOCUMENT] or [FLAG_ACTIVITY_NEW_TASK]. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_MULTIPLE_TASK. + static const int FLAG_ACTIVITY_MULTIPLE_TASK = 134217728; + + /// Opens a document into a new task rooted in this activity. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_DOCUMENT. + static const int FLAG_ACTIVITY_NEW_DOCUMENT = 524288; + + /// The launched activity starts a new task on the activity stack. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_TASK. + static const int FLAG_ACTIVITY_NEW_TASK = 268435456; + + /// Prevents the system from playing an activity transition animation when + /// launching this. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NO_ANIMATION. + static const int FLAG_ACTIVITY_NO_ANIMATION = 65536; + + /// Does not keep the launched activity in history. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NO_HISTORY. + static const int FLAG_ACTIVITY_NO_HISTORY = 1073741824; + + /// Prevents a typical callback from occuring when the activity is paused. + /// + /// https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NO_USER_ACTION + static const int FLAG_ACTIVITY_NO_USER_ACTION = 262144; + + /// Uses the previous activity as top when applicable. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_PREVIOUS_IS_TOP. + static const int FLAG_ACTIVITY_PREVIOUS_IS_TOP = 16777216; + + /// Brings any already instances of this activity to the front. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_REORDER_TO_FRONT. + static const int FLAG_ACTIVITY_REORDER_TO_FRONT = 131072; + + /// Launches the activity in a way that resets the task in some cases. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_RESET_TASK_IF_NEEDED. + static const int FLAG_ACTIVITY_RESET_TASK_IF_NEEDED = 2097152; + + /// Keeps an entry in recent tasks. Used with [FLAG_ACTIVITY_NEW_DOCUMENT]. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_RETAIN_IN_RECENTS. + static const int FLAG_ACTIVITY_RETAIN_IN_RECENTS = 8192; + + /// Will not re-launch the activity if it is already at the top of the history + /// stack. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_SINGLE_TOP. + static const int FLAG_ACTIVITY_SINGLE_TOP = 536870912; + + /// Places the activity on top of the home task. Must be used with + /// [FLAG_ACTIVITY_NEW_TASK]. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_TASK_ON_HOME. + static const int FLAG_ACTIVITY_TASK_ON_HOME = 16384; + + /// Prints debug logs while the intent is resolving. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_DEBUG_LOG_RESOLUTION. + static const int FLAG_DEBUG_LOG_RESOLUTION = 8; + + /// Does not match to any stopped components. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_EXCLUDE_STOPPED_PACKAGES. + static const int FLAG_EXCLUDE_STOPPED_PACKAGES = 16; + + /// Can be set by the caller to flag the intent as not being launched directly + /// by the user. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_FROM_BACKGROUND. + static const int FLAG_FROM_BACKGROUND = 4; + + /// Will persist the URI permision across device reboots. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_PERSISTABLE_URI_PERMISSION. + static const int FLAG_GRANT_PERSISTABLE_URI_PERMISSION = 64; + + /// Applies the URI permission grant based on prefix matching. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_PREFIX_URI_PERMISSION. + static const int FLAG_GRANT_PREFIX_URI_PERMISSION = 128; + + /// Grants the intent listener permission to read extra data from the Intent's + /// URI. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_READ_URI_PERMISSION. + static const int FLAG_GRANT_READ_URI_PERMISSION = 1; + + /// Grants the intent listener permission to write extra data from the + /// Intent's URI. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_WRITE_URI_PERMISSION. + static const int FLAG_GRANT_WRITE_URI_PERMISSION = 2; + + /// Always matches stopped components. This is the default behavior. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_INCLUDE_STOPPED_PACKAGES. + static const int FLAG_INCLUDE_STOPPED_PACKAGES = 32; + + /// Allows the listener to run at a high priority. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_FOREGROUND. + static const int FLAG_RECEIVER_FOREGROUND = 268435456; + + /// Doesn't allow listeners to cancel the broadcast. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_NO_ABORT. + static const int FLAG_RECEIVER_NO_ABORT = 134217728; + + /// Only allows registered receivers to listen for the intent. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_REGISTERED_ONLY. + static const int FLAG_RECEIVER_REGISTERED_ONLY = 1073741824; + + /// Will drop any pending broadcasts of this intent in favor of the newest + /// one. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_REPLACE_PENDING. + static const int FLAG_RECEIVER_REPLACE_PENDING = 536870912; + + /// Instant Apps will be able to listen for the intent (not the default + /// behavior). + /// + /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS. + static const int FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS = 2097152; +} diff --git a/packages/share_plus/share_plus_platform_interface/lib/options/android_intent_options.dart b/packages/share_plus/share_plus_platform_interface/lib/options/android_intent_options.dart new file mode 100644 index 0000000000..4c46254f27 --- /dev/null +++ b/packages/share_plus/share_plus_platform_interface/lib/options/android_intent_options.dart @@ -0,0 +1,43 @@ +class AndroidIntentOptions { + /// Constants that can be set on an intent to tweak how it is finally handled. + /// Some of the constants are mirrored to Dart via [Flag]. + /// + /// See https://developer.android.com/reference/android/content/Intent.html#setFlags(int). + final int? flags; + + final String? componentName; + + /// See https://developer.android.com/reference/android/content/Intent.html#setPackage(java.lang.String). + final String? packageName; + + AndroidIntentOptions._({this.flags, this.componentName, this.packageName}); + + factory AndroidIntentOptions({ + List? flags, + String? componentName, + String? packageName, + }) { + int convertFlags(List flags) { + bool isPowerOfTwo(int x) { + /* First x in the below expression is for the case when x is 0 */ + return x != 0 && ((x & (x - 1)) == 0); + } + + var finalValue = 0; + for (var i = 0; i < flags.length; i++) { + if (!isPowerOfTwo(flags[i])) { + throw ArgumentError.value( + flags[i], 'flag\'s value must be power of 2'); + } + finalValue |= flags[i]; + } + return finalValue; + } + + return AndroidIntentOptions._( + flags: flags?.isNotEmpty == true ? convertFlags(flags!) : null, + componentName: componentName, + packageName: packageName, + ); + } +} diff --git a/packages/share_plus/share_plus_platform_interface/lib/options/platform_options.dart b/packages/share_plus/share_plus_platform_interface/lib/options/platform_options.dart new file mode 100644 index 0000000000..d79c908a68 --- /dev/null +++ b/packages/share_plus/share_plus_platform_interface/lib/options/platform_options.dart @@ -0,0 +1,7 @@ +import 'android_intent_options.dart'; + +class PlatformOptions { + AndroidIntentOptions? androidIntentOptions; + + PlatformOptions({this.androidIntentOptions}); +} 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..119e3c81b7 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 @@ -7,6 +7,7 @@ import 'dart:ui'; import 'package:cross_file/cross_file.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:share_plus_platform_interface/options/platform_options.dart'; import '../method_channel/method_channel_share.dart'; @@ -35,11 +36,11 @@ class SharePlatform extends PlatformInterface { Future shareUri( Uri uri, { Rect? sharePositionOrigin, + PlatformOptions? platformOptions, }) { - return _instance.shareUri( - uri, - sharePositionOrigin: sharePositionOrigin, - ); + return _instance.shareUri(uri, + sharePositionOrigin: sharePositionOrigin, + platformOptions: platformOptions); } /// Share text with Result. @@ -47,12 +48,12 @@ class SharePlatform extends PlatformInterface { String text, { String? subject, Rect? sharePositionOrigin, + PlatformOptions? platformOptions, }) async { - return await _instance.share( - text, - subject: subject, - sharePositionOrigin: sharePositionOrigin, - ); + return await _instance.share(text, + subject: subject, + sharePositionOrigin: sharePositionOrigin, + platformOptions: platformOptions); } /// Share [XFile] objects with Result. @@ -62,14 +63,14 @@ class SharePlatform extends PlatformInterface { String? text, Rect? sharePositionOrigin, List? fileNameOverrides, + PlatformOptions? platformOptions, }) async { - return _instance.shareXFiles( - files, - subject: subject, - text: text, - sharePositionOrigin: sharePositionOrigin, - fileNameOverrides: fileNameOverrides, - ); + return _instance.shareXFiles(files, + subject: subject, + text: text, + sharePositionOrigin: sharePositionOrigin, + fileNameOverrides: fileNameOverrides, + platformOptions: platformOptions); } } diff --git a/packages/share_plus/share_plus_platform_interface/lib/share_plus_platform_interface.dart b/packages/share_plus/share_plus_platform_interface/lib/share_plus_platform_interface.dart index f1e38b83bc..28899d5cda 100644 --- a/packages/share_plus/share_plus_platform_interface/lib/share_plus_platform_interface.dart +++ b/packages/share_plus/share_plus_platform_interface/lib/share_plus_platform_interface.dart @@ -4,3 +4,6 @@ export 'package:cross_file/cross_file.dart'; export 'platform_interface/share_plus_platform.dart'; +export 'package:share_plus_platform_interface/options/platform_options.dart'; +export 'package:share_plus_platform_interface/options/android_intent_options.dart'; +export 'package:share_plus_platform_interface/options/android_intent_flag.dart'; 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..4d6919b08a 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 @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart' show TestDefaultBinaryMessengerBinding, TestWidgetsFlutterBinding; @@ -61,53 +62,17 @@ void main() { verifyZeroInteractions(mockChannel); }); - test('sharing origin sets the right params', () async { - await sharePlatform.shareUri( - Uri.parse('https://pub.dev/packages/share_plus'), - sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), - ); - verify(mockChannel.invokeMethod('shareUri', { - 'uri': 'https://pub.dev/packages/share_plus', - 'originX': 1.0, - 'originY': 2.0, - 'originWidth': 3.0, - 'originHeight': 4.0, - })); - - await sharePlatform.share( - 'some text to share', - subject: 'some subject to share', - sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), - ); - verify(mockChannel.invokeMethod('share', { - 'text': 'some text to share', - 'subject': 'some subject to share', - 'originX': 1.0, - 'originY': 2.0, - 'originWidth': 3.0, - 'originHeight': 4.0, - })); - - await withFile('tempfile-83649a.png', (File fd) async { - await sharePlatform.shareXFiles( - [XFile(fd.path)], - subject: 'some subject to share', - text: 'some text to share', - sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), - ); - verify(mockChannel.invokeMethod( - 'shareFiles', - { - 'paths': [fd.path], - 'mimeTypes': ['image/png'], - 'subject': 'some subject to share', - 'text': 'some text to share', - 'originX': 1.0, - 'originY': 2.0, - 'originWidth': 3.0, - 'originHeight': 4.0, - }, - )); + group('sharing sets the right params', () { + test('origin params', () { + return verifyParams(sharePlatform, mockChannel); + }); + test('android options params', () { + return verifyParams(sharePlatform, mockChannel, + targetPlatform: TargetPlatform.android); + }); + test('android options params on other platform(iOS)', () { + return verifyParams(sharePlatform, mockChannel, + targetPlatform: TargetPlatform.iOS); }); }); @@ -175,6 +140,80 @@ void main() { }); } +verifyParams(SharePlatform sharePlatform, MockMethodChannel mockChannel, + {TargetPlatform? targetPlatform}) async { + debugDefaultTargetPlatformOverride = targetPlatform; + + final platformOptions = PlatformOptions( + androidIntentOptions: AndroidIntentOptions( + packageName: 'com.example.app', + componentName: "com.example.app.ShareActivity", + flags: [ + AndroidIntentFlag.FLAG_ACTIVITY_CLEAR_TOP, + AndroidIntentFlag.FLAG_ACTIVITY_NEW_TASK, + ], + )); + + final Map androidOptionsVerifyArgs = { + 'componentName': "com.example.app.ShareActivity", + 'packageName': "com.example.app", + 'flags': AndroidIntentFlag.FLAG_ACTIVITY_CLEAR_TOP | + AndroidIntentFlag.FLAG_ACTIVITY_NEW_TASK, + }; + + await sharePlatform.shareUri(Uri.parse('https://pub.dev/packages/share_plus'), + sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + platformOptions: platformOptions); + verify(mockChannel.invokeMethod('shareUri', { + 'uri': 'https://pub.dev/packages/share_plus', + 'originX': 1.0, + 'originY': 2.0, + 'originWidth': 3.0, + 'originHeight': 4.0, + if (defaultTargetPlatform == TargetPlatform.android) + ...androidOptionsVerifyArgs + })); + + await sharePlatform.share('some text to share', + subject: 'some subject to share', + sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + platformOptions: platformOptions); + verify(mockChannel.invokeMethod('share', { + 'text': 'some text to share', + 'subject': 'some subject to share', + 'originX': 1.0, + 'originY': 2.0, + 'originWidth': 3.0, + 'originHeight': 4.0, + if (defaultTargetPlatform == TargetPlatform.android) + ...androidOptionsVerifyArgs + })); + + await withFile('tempfile-83649a.png', (File fd) async { + await sharePlatform.shareXFiles([XFile(fd.path)], + subject: 'some subject to share', + text: 'some text to share', + sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + platformOptions: platformOptions); + verify(mockChannel.invokeMethod( + 'shareFiles', + { + 'paths': [fd.path], + 'mimeTypes': ['image/png'], + 'subject': 'some subject to share', + 'text': 'some text to share', + 'originX': 1.0, + 'originY': 2.0, + 'originWidth': 3.0, + 'originHeight': 4.0, + if (defaultTargetPlatform == TargetPlatform.android) + ...androidOptionsVerifyArgs + }, + )); + }); + debugDefaultTargetPlatformOverride = null; +} + /// Execute a block within a context that handles creation and deletion of a helper file Future withFile(String filename, Future Function(File fd) func) async { final file = File(filename); From b269c56743d99648b1db77b68b2556289ad81f00 Mon Sep 17 00:00:00 2001 From: Yongle Date: Sat, 18 Jan 2025 10:11:19 +0800 Subject: [PATCH 2/2] chore(share_plus): melos run format --- .../share_plus/example/lib/main.dart | 60 +++++++++---------- .../share_plus_platform_interface_test.dart | 4 +- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/share_plus/share_plus/example/lib/main.dart b/packages/share_plus/share_plus/example/lib/main.dart index 4c91713d26..eda514884f 100644 --- a/packages/share_plus/share_plus/example/lib/main.dart +++ b/packages/share_plus/share_plus/example/lib/main.dart @@ -109,7 +109,7 @@ class DemoAppState extends State { setState(() => fileName = value); }, ), - if(Platform.isAndroid) ...buildAndroidOptions(), + if (Platform.isAndroid) ...buildAndroidOptions(), const SizedBox(height: 16), ImagePreviews(imagePaths, onDelete: _onDeleteImage), ElevatedButton.icon( @@ -202,33 +202,33 @@ class DemoAppState extends State { List buildAndroidOptions() { return [ - const SizedBox(height: 16), - Text("Android Platform(Optional)"), - const SizedBox(height: 16), - TextField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Target package name', - hintText: 'The target package name is available on Android.', - ), - maxLines: null, - onChanged: (String value) { - setState(() => packageName = value); - }, - ), - const SizedBox(height: 16), - TextField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Target component name', - hintText: - 'The target component name is available on Android.', - ), - maxLines: null, - onChanged: (String value) { - setState(() => componentName = value); - }, - )]; + const SizedBox(height: 16), + Text("Android Platform(Optional)"), + const SizedBox(height: 16), + TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Target package name', + hintText: 'The target package name is available on Android.', + ), + maxLines: null, + onChanged: (String value) { + setState(() => packageName = value); + }, + ), + const SizedBox(height: 16), + TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Target component name', + hintText: 'The target component name is available on Android.', + ), + maxLines: null, + onChanged: (String value) { + setState(() => componentName = value); + }, + ) + ]; } void _onDeleteImage(int position) { @@ -250,8 +250,8 @@ class DemoAppState extends State { final scaffoldMessenger = ScaffoldMessenger.of(context); ShareResult shareResult; - final platformOptions = PlatformOptions( - androidIntentOptions: AndroidIntentOptions( + final platformOptions = PlatformOptions( + androidIntentOptions: AndroidIntentOptions( packageName: packageName, componentName: componentName, )); 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 4d6919b08a..9fa7ac6591 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 @@ -145,7 +145,7 @@ verifyParams(SharePlatform sharePlatform, MockMethodChannel mockChannel, debugDefaultTargetPlatformOverride = targetPlatform; final platformOptions = PlatformOptions( - androidIntentOptions: AndroidIntentOptions( + androidIntentOptions: AndroidIntentOptions( packageName: 'com.example.app', componentName: "com.example.app.ShareActivity", flags: [ @@ -158,7 +158,7 @@ verifyParams(SharePlatform sharePlatform, MockMethodChannel mockChannel, 'componentName': "com.example.app.ShareActivity", 'packageName': "com.example.app", 'flags': AndroidIntentFlag.FLAG_ACTIVITY_CLEAR_TOP | - AndroidIntentFlag.FLAG_ACTIVITY_NEW_TASK, + AndroidIntentFlag.FLAG_ACTIVITY_NEW_TASK, }; await sharePlatform.shareUri(Uri.parse('https://pub.dev/packages/share_plus'),