diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java index 06244b5b32f68..d3fd6774fa6f7 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java @@ -187,6 +187,11 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result.success(response); break; } + case "Share.invoke": + String text = (String) arguments; + platformMessageHandler.share(text); + result.success(null); + break; default: result.notImplemented(); break; @@ -549,6 +554,13 @@ default void setFrameworkHandlesBack(boolean frameworkHandlesBack) {} * can be pasted. */ boolean clipboardHasStrings(); + + /** + * The Flutter application would like to share the given {@code text} using the Android standard + * intent action named {@code Intent.ACTION_SEND}. See: + * https://developer.android.com/reference/android/content/Intent.html#ACTION_SEND + */ + void share(@NonNull String text); } /** Types of sounds the Android OS can play on behalf of an application. */ diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index c3c14dff25213..58618b2b83247 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -11,6 +11,7 @@ import android.content.ClipDescription; import android.content.ClipboardManager; import android.content.Context; +import android.content.Intent; import android.content.res.AssetFileDescriptor; import android.os.Build; import android.view.HapticFeedbackConstants; @@ -145,6 +146,11 @@ public void setClipboardData(@NonNull String text) { public boolean clipboardHasStrings() { return PlatformPlugin.this.clipboardHasStrings(); } + + @Override + public void share(@NonNull String text) { + PlatformPlugin.this.share(text); + } }; public PlatformPlugin(@NonNull Activity activity, @NonNull PlatformChannel platformChannel) { @@ -570,4 +576,13 @@ private boolean clipboardHasStrings() { } return description.hasMimeType("text/*"); } + + private void share(@NonNull String text) { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TEXT, text); + + activity.startActivity(Intent.createChooser(intent, null)); + } } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java index 85ebafd471ff8..a8c203d917ffe 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java @@ -1,6 +1,8 @@ package io.flutter.embedding.engine.systemchannels; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -15,6 +17,7 @@ import org.json.JSONObject; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.robolectric.annotation.Config; @Config(manifest = Config.NONE) @@ -42,4 +45,26 @@ public void platformChannel_hasStringsMessage() { } verify(mockResult).success(refEq(expected)); } + + @Test + public void platformChannel_shareInvokeMessage() { + MethodChannel rawChannel = mock(MethodChannel.class); + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = new DartExecutor(mockFlutterJNI, mock(AssetManager.class)); + PlatformChannel fakePlatformChannel = new PlatformChannel(dartExecutor); + PlatformChannel.PlatformMessageHandler mockMessageHandler = + mock(PlatformChannel.PlatformMessageHandler.class); + fakePlatformChannel.setPlatformMessageHandler(mockMessageHandler); + + ArgumentCaptor valueCapture = ArgumentCaptor.forClass(String.class); + doNothing().when(mockMessageHandler).share(valueCapture.capture()); + + final String expectedContent = "Flutter"; + MethodCall methodCall = new MethodCall("Share.invoke", expectedContent); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + fakePlatformChannel.parsingMethodCallHandler.onMethodCall(methodCall, mockResult); + + assertEquals(valueCapture.getValue(), expectedContent); + verify(mockResult).success(null); + } } diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java index 51e66c91db30b..a9ced69401eee 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -6,16 +6,19 @@ import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -28,6 +31,7 @@ import android.content.ClipboardManager; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.os.Build; @@ -47,6 +51,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.android.controller.ActivityController; @@ -629,4 +635,35 @@ public void performsDefaultBehaviorWhenNoDelegateProvided() { verify(mockActivity, times(1)).finish(); } + + @Test + public void startChoosenActivityWhenSharingText() { + Activity mockActivity = mock(Activity.class); + PlatformChannel mockPlatformChannel = mock(PlatformChannel.class); + PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class); + PlatformPlugin platformPlugin = + new PlatformPlugin(mockActivity, mockPlatformChannel, mockPlatformPluginDelegate); + + // Mock Intent.createChooser (in real application it opens a chooser where the user can + // select which application will be used to share the selected text). + Intent choosenIntent = new Intent(); + MockedStatic intentClass = mockStatic(Intent.class); + ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + intentClass + .when(() -> Intent.createChooser(intentCaptor.capture(), any())) + .thenReturn(choosenIntent); + + final String expectedContent = "Flutter"; + platformPlugin.mPlatformMessageHandler.share(expectedContent); + + // Activity.startActivity should have been called. + verify(mockActivity, times(1)).startActivity(choosenIntent); + + // The intent action created by the plugin and passed to Intent.createChooser should be + // 'Intent.ACTION_SEND'. + Intent sendToIntent = intentCaptor.getValue(); + assertEquals(sendToIntent.getAction(), Intent.ACTION_SEND); + assertEquals(sendToIntent.getType(), "text/plain"); + assertEquals(sendToIntent.getStringExtra(Intent.EXTRA_TEXT), expectedContent); + } }