Reland - [Android] Add support for text processing actions (flutter/engine#46817)

## Description

This is a reland of https://github.com/flutter/engine/pull/44579 which was reverted in https://github.com/flutter/engine/pull/46788.

This reland adds a check into `onActivityResult` in order to return early if the result is related to an unknown request code (aka the result is related to a request sent by another plugin).
It also adds one test that simulates receiving such an unknown request code.

## Related Issue

Android engine side for https://github.com/flutter/flutter/issues/107603

## Tests

Adds 4 tests.
This commit is contained in:
Bruno Leroux 2023-10-16 07:43:15 +02:00 committed by GitHub
parent f263e6a49f
commit d003a7f2fb
7 changed files with 626 additions and 0 deletions

View File

@ -3094,6 +3094,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/syst
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java + ../../../flutter/LICENSE
@ -3136,6 +3137,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platf
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/util/HandlerCompat.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java + ../../../flutter/LICENSE
@ -5868,6 +5870,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/system
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java
@ -5915,6 +5918,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platfor
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java
FILE: ../../../flutter/shell/platform/android/io/flutter/util/HandlerCompat.java
FILE: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java
FILE: ../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java

View File

@ -24,4 +24,12 @@
</intent-filter>
</activity>
</application>
<!-- Required for io.flutter.plugin.text.ProcessTextPlugin to query activities that can process text. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>

View File

@ -275,6 +275,7 @@ android_java_sources = [
"io/flutter/embedding/engine/systemchannels/NavigationChannel.java",
"io/flutter/embedding/engine/systemchannels/PlatformChannel.java",
"io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java",
"io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java",
"io/flutter/embedding/engine/systemchannels/RestorationChannel.java",
"io/flutter/embedding/engine/systemchannels/SettingsChannel.java",
"io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java",
@ -322,6 +323,7 @@ android_java_sources = [
"io/flutter/plugin/platform/SingleViewPresentation.java",
"io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java",
"io/flutter/plugin/platform/VirtualDisplayController.java",
"io/flutter/plugin/text/ProcessTextPlugin.java",
"io/flutter/util/HandlerCompat.java",
"io/flutter/util/PathUtils.java",
"io/flutter/util/Preconditions.java",

View File

@ -31,6 +31,7 @@ import io.flutter.embedding.engine.systemchannels.LocalizationChannel;
import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
import io.flutter.embedding.engine.systemchannels.NavigationChannel;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.embedding.engine.systemchannels.ProcessTextChannel;
import io.flutter.embedding.engine.systemchannels.RestorationChannel;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.embedding.engine.systemchannels.SpellCheckChannel;
@ -38,6 +39,7 @@ import io.flutter.embedding.engine.systemchannels.SystemChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.localization.LocalizationPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.plugin.text.ProcessTextPlugin;
import io.flutter.util.ViewUtils;
import java.util.HashSet;
import java.util.List;
@ -95,6 +97,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
@NonNull private final NavigationChannel navigationChannel;
@NonNull private final RestorationChannel restorationChannel;
@NonNull private final PlatformChannel platformChannel;
@NonNull private final ProcessTextChannel processTextChannel;
@NonNull private final SettingsChannel settingsChannel;
@NonNull private final SpellCheckChannel spellCheckChannel;
@NonNull private final SystemChannel systemChannel;
@ -329,6 +332,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
mouseCursorChannel = new MouseCursorChannel(dartExecutor);
navigationChannel = new NavigationChannel(dartExecutor);
platformChannel = new PlatformChannel(dartExecutor);
processTextChannel = new ProcessTextChannel(dartExecutor, context.getPackageManager());
restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData);
settingsChannel = new SettingsChannel(dartExecutor);
spellCheckChannel = new SpellCheckChannel(dartExecutor);
@ -384,6 +388,9 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
}
ViewUtils.calculateMaximumDisplayMetrics(context, this);
ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(this.getProcessTextChannel());
this.pluginRegistry.add(processTextPlugin);
}
private void attachToJni() {
@ -545,6 +552,12 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
return platformChannel;
}
/** System channel that sends text processing requests from Flutter to Android. */
@NonNull
public ProcessTextChannel getProcessTextChannel() {
return processTextChannel;
}
/**
* System channel to exchange restoration data between framework and engine.
*

View File

@ -0,0 +1,122 @@
// Copyright 2013 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.embedding.engine.systemchannels;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.StandardMethodCodec;
import java.util.ArrayList;
import java.util.Map;
/**
* {@link ProcessTextChannel} is a platform channel that is used by the framework to initiate text
* processing feature in the embedding and for the embedding to send back the results.
*
* <p>When the framework needs to query the list of text processing actions (for instance to expose
* them in the selected text context menu), it will send to the embedding the message {@code
* ProcessText.queryTextActions}. In response, the {@link io.flutter.plugin.text.ProcessTextPlugin}
* will return a map of all activities that can process text. The map keys are generated IDs and the
* values are the activities labels. On the first request, the {@link
* io.flutter.plugin.text.ProcessTextPlugin} will make a call to Android's package manager to query
* all activities that can be performed for the {@code Intent.ACTION_PROCESS_TEXT} intent.
*
* <p>When a text processing action has to be executed, the framework will send to the embedding the
* message {@code ProcessText.processTextAction} with the {@code int id} of the choosen text action
* and the {@code String} of text to process as arguments. In response, the {@link
* io.flutter.plugin.text.ProcessTextPlugin} will make a call to the Android application activity to
* start the activity exposing the text action. The {@link io.flutter.plugin.text.ProcessTextPlugin}
* will return the processed text if there is one, or null if the activity did not return a
* transformed text.
*
* <p>{@link io.flutter.plugin.text.ProcessTextPlugin} implements {@link ProcessTextMethodHandler}
* that parses incoming messages from Flutter.
*/
public class ProcessTextChannel {
private static final String TAG = "ProcessTextChannel";
private static final String CHANNEL_NAME = "flutter/processtext";
private static final String METHOD_QUERY_TEXT_ACTIONS = "ProcessText.queryTextActions";
private static final String METHOD_PROCESS_TEXT_ACTION = "ProcessText.processTextAction";
public final MethodChannel channel;
public final PackageManager packageManager;
private ProcessTextMethodHandler processTextMethodHandler;
@NonNull
public final MethodChannel.MethodCallHandler parsingMethodHandler =
new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
if (processTextMethodHandler == null) {
return;
}
String method = call.method;
Object args = call.arguments;
switch (method) {
case METHOD_QUERY_TEXT_ACTIONS:
try {
Map<String, String> actions = processTextMethodHandler.queryTextActions();
result.success(actions);
} catch (IllegalStateException exception) {
result.error("error", exception.getMessage(), null);
}
break;
case METHOD_PROCESS_TEXT_ACTION:
try {
final ArrayList<Object> argumentList = (ArrayList<Object>) args;
String id = (String) (argumentList.get(0));
String text = (String) (argumentList.get(1));
boolean readOnly = (boolean) (argumentList.get(2));
processTextMethodHandler.processTextAction(id, text, readOnly, result);
} catch (IllegalStateException exception) {
result.error("error", exception.getMessage(), null);
}
break;
default:
result.notImplemented();
break;
}
}
};
public ProcessTextChannel(
@NonNull DartExecutor dartExecutor, @NonNull PackageManager packageManager) {
this.packageManager = packageManager;
channel = new MethodChannel(dartExecutor, CHANNEL_NAME, StandardMethodCodec.INSTANCE);
channel.setMethodCallHandler(parsingMethodHandler);
}
/**
* Sets the {@link ProcessTextMethodHandler} which receives all requests to the text processing
* feature sent through this channel.
*/
public void setMethodHandler(@Nullable ProcessTextMethodHandler processTextMethodHandler) {
this.processTextMethodHandler = processTextMethodHandler;
}
public interface ProcessTextMethodHandler {
/** Requests the map of text actions. Each text action has a unique id and a localized label. */
Map<String, String> queryTextActions();
/**
* Requests to run a text action on a given input text.
*
* @param id The ID of the text action returned by {@code ProcessText.queryTextActions}.
* @param input The text to be processed.
* @param readOnly Indicates to the activity if the processed text will be used as read-only.
* see
* https://developer.android.com/reference/android/content/Intent#EXTRA_PROCESS_TEXT_READONLY
* @param result The method channel result instance used to reply.
*/
void processTextAction(
@NonNull String id,
@NonNull String input,
@NonNull boolean readOnly,
@NonNull MethodChannel.Result result);
}
}

View File

@ -0,0 +1,197 @@
// Copyright 2013 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.plugin.text;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.systemchannels.ProcessTextChannel;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.PluginRegistry.ActivityResultListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ProcessTextPlugin
implements FlutterPlugin,
ActivityAware,
ActivityResultListener,
ProcessTextChannel.ProcessTextMethodHandler {
private static final String TAG = "ProcessTextPlugin";
@NonNull private final ProcessTextChannel processTextChannel;
@NonNull private final PackageManager packageManager;
@Nullable private ActivityPluginBinding activityBinding;
private Map<String, ResolveInfo> resolveInfosById;
@NonNull
private Map<Integer, MethodChannel.Result> requestsByCode =
new HashMap<Integer, MethodChannel.Result>();
public ProcessTextPlugin(@NonNull ProcessTextChannel processTextChannel) {
this.processTextChannel = processTextChannel;
this.packageManager = processTextChannel.packageManager;
processTextChannel.setMethodHandler(this);
}
@Override
public Map<String, String> queryTextActions() {
if (resolveInfosById == null) {
cacheResolveInfos();
}
Map<String, String> result = new HashMap<String, String>();
for (String id : resolveInfosById.keySet()) {
final ResolveInfo info = resolveInfosById.get(id);
result.put(id, info.loadLabel(packageManager).toString());
}
return result;
}
@Override
public void processTextAction(
@NonNull String id,
@NonNull String text,
@NonNull boolean readOnly,
@NonNull MethodChannel.Result result) {
if (activityBinding == null) {
result.error("error", "Plugin not bound to an Activity", null);
return;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
result.error("error", "Android version not supported", null);
return;
}
if (resolveInfosById == null) {
result.error("error", "Can not process text actions before calling queryTextActions", null);
return;
}
final ResolveInfo info = resolveInfosById.get(id);
if (info == null) {
result.error("error", "Text processing activity not found", null);
return;
}
Integer requestCode = result.hashCode();
requestsByCode.put(requestCode, result);
Intent intent = new Intent();
intent.setClassName(info.activityInfo.packageName, info.activityInfo.name);
intent.setAction(Intent.ACTION_PROCESS_TEXT);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_PROCESS_TEXT, text);
intent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, readOnly);
// Start the text processing activity. When the activity completes, the onActivityResult
// callback
// is called.
activityBinding.getActivity().startActivityForResult(intent, requestCode);
}
private void cacheResolveInfos() {
resolveInfosById = new HashMap<String, ResolveInfo>();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
Intent intent = new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
List<ResolveInfo> infos;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
infos = packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0));
} else {
infos = packageManager.queryIntentActivities(intent, 0);
}
for (ResolveInfo info : infos) {
final String id = info.activityInfo.name;
final String label = info.loadLabel(packageManager).toString();
resolveInfosById.put(id, info);
}
}
/**
* Executed when a text processing activity terminates.
*
* <p>When an activity returns a value, the request is completed successfully and returns the
* processed text.
*
* <p>When an activity does not return a value. the request is completed successfully and returns
* null.
*/
@TargetApi(Build.VERSION_CODES.M)
@RequiresApi(Build.VERSION_CODES.M)
public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent intent) {
// Return early if the result is not related to a request sent by this plugin.
if (!requestsByCode.containsKey(requestCode)) {
return false;
}
String result = null;
if (resultCode == Activity.RESULT_OK) {
result = intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT);
}
requestsByCode.remove(requestCode).success(result);
return true;
}
/**
* Unregisters this {@code ProcessTextPlugin} as the {@code
* ProcessTextChannel.ProcessTextMethodHandler}, for the {@link
* io.flutter.embedding.engine.systemchannels.ProcessTextChannel}.
*
* <p>Do not invoke any methods on a {@code ProcessTextPlugin} after invoking this method.
*/
public void destroy() {
processTextChannel.setMethodHandler(null);
}
// FlutterPlugin interface implementation.
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
// Nothing to do because this plugin is instantiated by the engine.
}
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
// Nothing to do because this plugin is instantiated by the engine.
}
// ActivityAware interface implementation.
//
// Store the binding and manage the activity result listener.
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
this.activityBinding = binding;
this.activityBinding.addActivityResultListener(this);
};
public void onDetachedFromActivityForConfigChanges() {
this.activityBinding.removeActivityResultListener(this);
this.activityBinding = null;
}
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
this.activityBinding = binding;
this.activityBinding.addActivityResultListener(this);
}
public void onDetachedFromActivity() {
this.activityBinding.removeActivityResultListener(this);
this.activityBinding = null;
}
}

View File

@ -0,0 +1,280 @@
package io.flutter.plugin.text;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import androidx.annotation.RequiresApi;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.systemchannels.ProcessTextChannel;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.StandardMethodCodec;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@RunWith(AndroidJUnit4.class)
@TargetApi(Build.VERSION_CODES.N)
@RequiresApi(Build.VERSION_CODES.N)
public class ProcessTextPluginTest {
private static void sendToBinaryMessageHandler(
BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method, Object args) {
MethodCall methodCall = new MethodCall(method, args);
ByteBuffer encodedMethodCall = StandardMethodCodec.INSTANCE.encodeMethodCall(methodCall);
binaryMessageHandler.onMessage(
(ByteBuffer) encodedMethodCall.flip(), mock(BinaryMessenger.BinaryReply.class));
}
@SuppressWarnings("deprecation")
// setMessageHandler is deprecated.
@Test
public void respondsToProcessTextChannelMessage() {
ArgumentCaptor<BinaryMessenger.BinaryMessageHandler> binaryMessageHandlerCaptor =
ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
ProcessTextChannel.ProcessTextMethodHandler mockHandler =
mock(ProcessTextChannel.ProcessTextMethodHandler.class);
PackageManager mockPackageManager = mock(PackageManager.class);
ProcessTextChannel processTextChannel =
new ProcessTextChannel(mockBinaryMessenger, mockPackageManager);
processTextChannel.setMethodHandler(mockHandler);
verify(mockBinaryMessenger, times(1))
.setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture());
BinaryMessenger.BinaryMessageHandler binaryMessageHandler =
binaryMessageHandlerCaptor.getValue();
sendToBinaryMessageHandler(binaryMessageHandler, "ProcessText.queryTextActions", null);
verify(mockHandler).queryTextActions();
}
@SuppressWarnings("deprecation")
// setMessageHandler is deprecated.
@Test
public void performQueryTextActions() {
DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
PackageManager mockPackageManager = mock(PackageManager.class);
ProcessTextChannel processTextChannel =
new ProcessTextChannel(mockBinaryMessenger, mockPackageManager);
// Set up mocked result for PackageManager.queryIntentActivities.
ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager);
ResolveInfo action2 = createFakeResolveInfo("Action2", mockPackageManager);
List<ResolveInfo> infos = new ArrayList<ResolveInfo>(Arrays.asList(action1, action2));
Intent intent = new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
when(mockPackageManager.queryIntentActivities(
any(Intent.class), any(PackageManager.ResolveInfoFlags.class)))
.thenReturn(infos);
// ProcessTextPlugin should retrieve the mocked text actions.
ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(processTextChannel);
Map<String, String> textActions = processTextPlugin.queryTextActions();
final String action1Id = "mockActivityName.Action1";
final String action2Id = "mockActivityName.Action2";
assertEquals(textActions, Map.of(action1Id, "Action1", action2Id, "Action2"));
}
@SuppressWarnings("deprecation")
// setMessageHandler is deprecated.
@Test
public void performProcessTextActionWithNoReturnedValue() {
DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
PackageManager mockPackageManager = mock(PackageManager.class);
ProcessTextChannel processTextChannel =
new ProcessTextChannel(mockBinaryMessenger, mockPackageManager);
// Set up mocked result for PackageManager.queryIntentActivities.
ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager);
ResolveInfo action2 = createFakeResolveInfo("Action2", mockPackageManager);
List<ResolveInfo> infos = new ArrayList<ResolveInfo>(Arrays.asList(action1, action2));
when(mockPackageManager.queryIntentActivities(
any(Intent.class), any(PackageManager.ResolveInfoFlags.class)))
.thenReturn(infos);
// ProcessTextPlugin should retrieve the mocked text actions.
ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(processTextChannel);
Map<String, String> textActions = processTextPlugin.queryTextActions();
final String action1Id = "mockActivityName.Action1";
final String action2Id = "mockActivityName.Action2";
assertEquals(textActions, Map.of(action1Id, "Action1", action2Id, "Action2"));
// Set up the activity binding.
ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class);
Activity mockActivity = mock(Activity.class);
when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity);
processTextPlugin.onAttachedToActivity(mockActivityPluginBinding);
// Execute th first action.
String textToBeProcessed = "Flutter!";
MethodChannel.Result result = mock(MethodChannel.Result.class);
processTextPlugin.processTextAction(action1Id, textToBeProcessed, false, result);
// Activity.startActivityForResult should have been called.
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mockActivity, times(1)).startActivityForResult(intentCaptor.capture(), anyInt());
Intent intent = intentCaptor.getValue();
assertEquals(intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT), textToBeProcessed);
// Simulate an Android activity answer which does not return a value.
Intent resultIntent = new Intent();
processTextPlugin.onActivityResult(result.hashCode(), Activity.RESULT_OK, resultIntent);
// Success with no returned value is expected.
verify(result).success(null);
}
@SuppressWarnings("deprecation")
// setMessageHandler is deprecated.
@Test
public void performProcessTextActionWithReturnedValue() {
DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
PackageManager mockPackageManager = mock(PackageManager.class);
ProcessTextChannel processTextChannel =
new ProcessTextChannel(mockBinaryMessenger, mockPackageManager);
// Set up mocked result for PackageManager.queryIntentActivities.
ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager);
ResolveInfo action2 = createFakeResolveInfo("Action2", mockPackageManager);
List<ResolveInfo> infos = new ArrayList<ResolveInfo>(Arrays.asList(action1, action2));
when(mockPackageManager.queryIntentActivities(
any(Intent.class), any(PackageManager.ResolveInfoFlags.class)))
.thenReturn(infos);
// ProcessTextPlugin should retrieve the mocked text actions.
ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(processTextChannel);
Map<String, String> textActions = processTextPlugin.queryTextActions();
final String action1Id = "mockActivityName.Action1";
final String action2Id = "mockActivityName.Action2";
assertEquals(textActions, Map.of(action1Id, "Action1", action2Id, "Action2"));
// Set up the activity binding.
ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class);
Activity mockActivity = mock(Activity.class);
when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity);
processTextPlugin.onAttachedToActivity(mockActivityPluginBinding);
// Execute the first action.
String textToBeProcessed = "Flutter!";
MethodChannel.Result result = mock(MethodChannel.Result.class);
processTextPlugin.processTextAction(action1Id, textToBeProcessed, false, result);
// Activity.startActivityForResult should have been called.
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mockActivity, times(1)).startActivityForResult(intentCaptor.capture(), anyInt());
Intent intent = intentCaptor.getValue();
assertEquals(intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT), textToBeProcessed);
// Simulate an Android activity answer which returns a transformed text.
String processedText = "Flutter!!!";
Intent resultIntent = new Intent();
resultIntent.putExtra(Intent.EXTRA_PROCESS_TEXT, processedText);
processTextPlugin.onActivityResult(result.hashCode(), Activity.RESULT_OK, resultIntent);
// Success with the transformed text is expected.
verify(result).success(processedText);
}
@SuppressWarnings("deprecation")
// setMessageHandler is deprecated.
@Test
public void doNotCrashOnNonRelatedActivityResult() {
DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
PackageManager mockPackageManager = mock(PackageManager.class);
ProcessTextChannel processTextChannel =
new ProcessTextChannel(mockBinaryMessenger, mockPackageManager);
// Set up mocked result for PackageManager.queryIntentActivities.
ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager);
ResolveInfo action2 = createFakeResolveInfo("Action2", mockPackageManager);
List<ResolveInfo> infos = new ArrayList<ResolveInfo>(Arrays.asList(action1, action2));
when(mockPackageManager.queryIntentActivities(
any(Intent.class), any(PackageManager.ResolveInfoFlags.class)))
.thenReturn(infos);
// ProcessTextPlugin should retrieve the mocked text actions.
ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(processTextChannel);
Map<String, String> textActions = processTextPlugin.queryTextActions();
final String action1Id = "mockActivityName.Action1";
final String action2Id = "mockActivityName.Action2";
assertEquals(textActions, Map.of(action1Id, "Action1", action2Id, "Action2"));
// Set up the activity binding.
ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class);
Activity mockActivity = mock(Activity.class);
when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity);
processTextPlugin.onAttachedToActivity(mockActivityPluginBinding);
// Execute the first action.
String textToBeProcessed = "Flutter!";
MethodChannel.Result result = mock(MethodChannel.Result.class);
processTextPlugin.processTextAction(action1Id, textToBeProcessed, false, result);
// Activity.startActivityForResult should have been called.
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mockActivity, times(1)).startActivityForResult(intentCaptor.capture(), anyInt());
Intent intent = intentCaptor.getValue();
assertEquals(intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT), textToBeProcessed);
// Result to a request not sent by this plugin should be ignored.
final int externalRequestCode = 42;
processTextPlugin.onActivityResult(externalRequestCode, Activity.RESULT_OK, new Intent());
// Simulate an Android activity answer which returns a transformed text.
String processedText = "Flutter!!!";
Intent resultIntent = new Intent();
resultIntent.putExtra(Intent.EXTRA_PROCESS_TEXT, processedText);
processTextPlugin.onActivityResult(result.hashCode(), Activity.RESULT_OK, resultIntent);
// Success with the transformed text is expected.
verify(result).success(processedText);
}
private ResolveInfo createFakeResolveInfo(String label, PackageManager mockPackageManager) {
ResolveInfo resolveInfo = mock(ResolveInfo.class);
ActivityInfo activityInfo = new ActivityInfo();
when(resolveInfo.loadLabel(mockPackageManager)).thenReturn(label);
// Use Java reflection to set required member variables.
try {
Field activityField = ResolveInfo.class.getDeclaredField("activityInfo");
activityField.setAccessible(true);
activityField.set(resolveInfo, activityInfo);
Field packageNameField = PackageItemInfo.class.getDeclaredField("packageName");
packageNameField.setAccessible(true);
packageNameField.set(activityInfo, "mockActivityPackageName");
Field nameField = PackageItemInfo.class.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(activityInfo, "mockActivityName." + label);
} catch (Exception ex) {
// Test will failed if reflection APIs throw.
}
return resolveInfo;
}
}