Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit baf8e4c

Browse files
committed
Add tests and more documentation
1 parent 752a870 commit baf8e4c

File tree

3 files changed

+298
-69
lines changed

3 files changed

+298
-69
lines changed

shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@
2020
*
2121
* <p>When there is new a text context menu to display, the framework will send to the embedding the
2222
* message {@code ProcessText.queryTextActions}. In response, the {@link
23-
* io.flutter.plugin.text.ProcessTextPlugin} will return a map of all activities that can be performed
24-
* to process text. The map keys are generated IDs and the values are the activities labels.
25-
* On the first request, the {@link io.flutter.plugin.text.ProcessTextPlugin} will make a call to
26-
* Android's package manager to query all activities that can be performed for the
27-
* {@code Intent.ACTION_PROCESS_TEXT} intent.
23+
* io.flutter.plugin.text.ProcessTextPlugin} will return a map of all activities that can be
24+
* performed to process text. The map keys are generated IDs and the values are the activities
25+
* labels. On the first request, the {@link io.flutter.plugin.text.ProcessTextPlugin} will make a
26+
* call to Android's package manager to query all activities that can be performed for the {@code
27+
* Intent.ACTION_PROCESS_TEXT} intent.
2828
*
29-
* <p>When an text processing action has to be executed, the framework will send to the embedding the
30-
* message {@code ProcessText.processTextAction} with the {@code int id} of the choosen text action and the
31-
* {@code String} of text to process as arguments. In response, the {@link
29+
* <p>When an text processing action has to be executed, the framework will send to the embedding
30+
* the message {@code ProcessText.processTextAction} with the {@code int id} of the choosen text
31+
* action and the {@code String} of text to process as arguments. In response, the {@link
3232
* io.flutter.plugin.text.ProcessTextPlugin} will make a call to the Android application activity to
3333
* start the activity exposing the text action and it will return the processed text, or null if the
3434
* activity did not return a value.
@@ -107,7 +107,12 @@ public interface ProcessTextMethodHandler {
107107
/**
108108
* Requests to run a text action on a given input text.
109109
*
110-
* <p>TODO(bleroux): add documentation for parameters.
110+
* @param id The ID of the text action returned by queryTextActions.
111+
* @param input The text to be processed.
112+
* @param readOnly Indicates to the activity if the processed text will be used as read-only.
113+
* see
114+
* https://developer.android.com/reference/android/content/Intent#EXTRA_PROCESS_TEXT_READONLY
115+
* @param result The method channel result instance used to reply.
111116
*/
112117
void processTextAction(
113118
@NonNull int id,

shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java

Lines changed: 62 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
import java.util.List;
2222
import java.util.Map;
2323

24-
public class ProcessTextPlugin implements FlutterPlugin, ActivityAware, ActivityResultListener {
24+
public class ProcessTextPlugin
25+
implements FlutterPlugin,
26+
ActivityAware,
27+
ActivityResultListener,
28+
ProcessTextChannel.ProcessTextMethodHandler {
2529
private static final String TAG = "ProcessTextPlugin";
2630

2731
@NonNull private final ProcessTextChannel processTextChannel;
@@ -37,65 +41,63 @@ public ProcessTextPlugin(@NonNull ProcessTextChannel processTextChannel) {
3741
this.processTextChannel = processTextChannel;
3842
this.packageManager = processTextChannel.packageManager;
3943

40-
processTextChannel.setMethodHandler(
41-
new ProcessTextChannel.ProcessTextMethodHandler() {
42-
@Override
43-
public Map<Integer, String> queryTextActions() {
44-
if (resolveInfosById == null) {
45-
resolveInfosById = new HashMap<Integer, ResolveInfo>();
46-
cacheResolveInfos();
47-
}
48-
Map<Integer, String> result = new HashMap<Integer, String>();
49-
for (Integer id : resolveInfosById.keySet()) {
50-
final ResolveInfo info = resolveInfosById.get(id);
51-
result.put(id, info.loadLabel(packageManager).toString());
52-
}
53-
return result;
54-
}
55-
56-
@Override
57-
public void processTextAction(
58-
@NonNull int id,
59-
@NonNull String text,
60-
@NonNull boolean readOnly,
61-
@NonNull MethodChannel.Result result) {
62-
if (activityBinding == null) {
63-
result.error("error", "Plugin not bound to an Activity", null);
64-
return;
65-
}
66-
67-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
68-
result.error("error", "Android version not supported", null);
69-
return;
70-
}
71-
72-
if (resolveInfosById == null) {
73-
result.error("error", "Can not process text actions before calling queryTextActions", null);
74-
return;
75-
}
76-
77-
final ResolveInfo info = resolveInfosById.get(id);
78-
if (info == null) {
79-
result.error("error", "Text processing activity not found", null);
80-
return;
81-
}
82-
83-
Integer requestCode = result.hashCode();
84-
requestsByCode.put(requestCode, result);
85-
86-
Intent intent =
87-
new Intent()
88-
.setClassName(info.activityInfo.packageName, info.activityInfo.name)
89-
.setAction(Intent.ACTION_PROCESS_TEXT)
90-
.setType("text/plain")
91-
.putExtra(Intent.EXTRA_PROCESS_TEXT, text)
92-
.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, readOnly);
93-
94-
// Start the text processing activity. onActivityResult callback is called
95-
// when the activity completes.
96-
activityBinding.getActivity().startActivityForResult(intent, requestCode);
97-
}
98-
});
44+
processTextChannel.setMethodHandler(this);
45+
}
46+
47+
@Override
48+
public Map<Integer, String> queryTextActions() {
49+
if (resolveInfosById == null) {
50+
resolveInfosById = new HashMap<Integer, ResolveInfo>();
51+
cacheResolveInfos();
52+
}
53+
Map<Integer, String> result = new HashMap<Integer, String>();
54+
for (Integer id : resolveInfosById.keySet()) {
55+
final ResolveInfo info = resolveInfosById.get(id);
56+
result.put(id, info.loadLabel(packageManager).toString());
57+
}
58+
return result;
59+
}
60+
61+
@Override
62+
public void processTextAction(
63+
@NonNull int id,
64+
@NonNull String text,
65+
@NonNull boolean readOnly,
66+
@NonNull MethodChannel.Result result) {
67+
if (activityBinding == null) {
68+
result.error("error", "Plugin not bound to an Activity", null);
69+
return;
70+
}
71+
72+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
73+
result.error("error", "Android version not supported", null);
74+
return;
75+
}
76+
77+
if (resolveInfosById == null) {
78+
result.error("error", "Can not process text actions before calling queryTextActions", null);
79+
return;
80+
}
81+
82+
final ResolveInfo info = resolveInfosById.get(id);
83+
if (info == null) {
84+
result.error("error", "Text processing activity not found", null);
85+
return;
86+
}
87+
88+
Integer requestCode = result.hashCode();
89+
requestsByCode.put(requestCode, result);
90+
91+
Intent intent = new Intent();
92+
intent.setClassName(info.activityInfo.packageName, info.activityInfo.name);
93+
intent.setAction(Intent.ACTION_PROCESS_TEXT);
94+
intent.setType("text/plain");
95+
intent.putExtra(Intent.EXTRA_PROCESS_TEXT, text);
96+
intent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, readOnly);
97+
98+
// Start the text processing activity. onActivityResult callback is called
99+
// when the activity completes.
100+
activityBinding.getActivity().startActivityForResult(intent, requestCode);
99101
}
100102

101103
private void cacheResolveInfos() {
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package io.flutter.plugin.text;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.mockito.Mockito.any;
5+
import static org.mockito.Mockito.anyInt;
6+
import static org.mockito.Mockito.mock;
7+
import static org.mockito.Mockito.times;
8+
import static org.mockito.Mockito.verify;
9+
import static org.mockito.Mockito.when;
10+
11+
import android.app.Activity;
12+
import android.content.Intent;
13+
import android.content.pm.ActivityInfo;
14+
import android.content.pm.PackageItemInfo;
15+
import android.content.pm.PackageManager;
16+
import android.content.pm.ResolveInfo;
17+
import androidx.test.ext.junit.runners.AndroidJUnit4;
18+
import io.flutter.embedding.engine.dart.DartExecutor;
19+
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
20+
import io.flutter.embedding.engine.systemchannels.ProcessTextChannel;
21+
import io.flutter.plugin.common.BinaryMessenger;
22+
import io.flutter.plugin.common.MethodCall;
23+
import io.flutter.plugin.common.MethodChannel;
24+
import io.flutter.plugin.common.StandardMethodCodec;
25+
import java.lang.reflect.Field;
26+
import java.nio.ByteBuffer;
27+
import java.util.ArrayList;
28+
import java.util.Arrays;
29+
import java.util.List;
30+
import java.util.Map;
31+
import org.junit.Test;
32+
import org.junit.runner.RunWith;
33+
import org.mockito.ArgumentCaptor;
34+
35+
@RunWith(AndroidJUnit4.class)
36+
public class ProcessTextPluginTest {
37+
38+
private static void sendToBinaryMessageHandler(
39+
BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method, Object args) {
40+
MethodCall methodCall = new MethodCall(method, args);
41+
ByteBuffer encodedMethodCall = StandardMethodCodec.INSTANCE.encodeMethodCall(methodCall);
42+
binaryMessageHandler.onMessage(
43+
(ByteBuffer) encodedMethodCall.flip(), mock(BinaryMessenger.BinaryReply.class));
44+
}
45+
46+
@SuppressWarnings("deprecation")
47+
// setMessageHandler is deprecated.
48+
@Test
49+
public void respondsToProcessTextChannelMessage() {
50+
ArgumentCaptor<BinaryMessenger.BinaryMessageHandler> binaryMessageHandlerCaptor =
51+
ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
52+
DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
53+
ProcessTextChannel.ProcessTextMethodHandler mockHandler =
54+
mock(ProcessTextChannel.ProcessTextMethodHandler.class);
55+
PackageManager mockPackageManager = mock(PackageManager.class);
56+
ProcessTextChannel processTextChannel =
57+
new ProcessTextChannel(mockBinaryMessenger, mockPackageManager);
58+
59+
processTextChannel.setMethodHandler(mockHandler);
60+
61+
verify(mockBinaryMessenger, times(1))
62+
.setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture());
63+
64+
BinaryMessenger.BinaryMessageHandler binaryMessageHandler =
65+
binaryMessageHandlerCaptor.getValue();
66+
67+
sendToBinaryMessageHandler(binaryMessageHandler, "ProcessText.queryTextActions", null);
68+
69+
verify(mockHandler).queryTextActions();
70+
}
71+
72+
@SuppressWarnings("deprecation")
73+
// setMessageHandler is deprecated.
74+
@Test
75+
public void performQueryTextActions() {
76+
DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
77+
PackageManager mockPackageManager = mock(PackageManager.class);
78+
ProcessTextChannel processTextChannel =
79+
new ProcessTextChannel(mockBinaryMessenger, mockPackageManager);
80+
81+
// Set up mocked result for PackageManager.queryIntentActivities.
82+
ResolveInfo action1 = mock(ResolveInfo.class);
83+
when(action1.loadLabel(mockPackageManager)).thenReturn("Action1");
84+
ResolveInfo action2 = mock(ResolveInfo.class);
85+
when(action2.loadLabel(mockPackageManager)).thenReturn("Action2");
86+
List<ResolveInfo> infos = new ArrayList<ResolveInfo>(Arrays.asList(action1, action2));
87+
Intent intent = new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
88+
when(mockPackageManager.queryIntentActivities(
89+
any(Intent.class), any(PackageManager.ResolveInfoFlags.class)))
90+
.thenReturn(infos);
91+
92+
// ProcessTextPlugin should retrieve the mocked text actions.
93+
ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(processTextChannel);
94+
Map<Integer, String> textActions = processTextPlugin.queryTextActions();
95+
final int action1Id = 0;
96+
final int action2Id = 1;
97+
assertEquals(textActions, Map.of(action1Id, "Action1", action2Id, "Action2"));
98+
}
99+
100+
@SuppressWarnings("deprecation")
101+
// setMessageHandler is deprecated.
102+
@Test
103+
public void performProcessTextActionWithNoReturnedValue() {
104+
DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
105+
PackageManager mockPackageManager = mock(PackageManager.class);
106+
ProcessTextChannel processTextChannel =
107+
new ProcessTextChannel(mockBinaryMessenger, mockPackageManager);
108+
109+
// Set up mocked result for PackageManager.queryIntentActivities.
110+
ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager);
111+
ResolveInfo action2 = createFakeResolveInfo("Action2", mockPackageManager);
112+
List<ResolveInfo> infos = new ArrayList<ResolveInfo>(Arrays.asList(action1, action2));
113+
when(mockPackageManager.queryIntentActivities(
114+
any(Intent.class), any(PackageManager.ResolveInfoFlags.class)))
115+
.thenReturn(infos);
116+
117+
// ProcessTextPlugin should retrieve the mocked text actions.
118+
ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(processTextChannel);
119+
Map<Integer, String> textActions = processTextPlugin.queryTextActions();
120+
final int action1Id = 0;
121+
final int action2Id = 1;
122+
assertEquals(textActions, Map.of(action1Id, "Action1", action2Id, "Action2"));
123+
124+
// Set up activity binding.
125+
ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class);
126+
Activity mockActivity = mock(Activity.class);
127+
when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity);
128+
processTextPlugin.onAttachedToActivity(mockActivityPluginBinding);
129+
130+
// Execute first action.
131+
String textToBeProcessed = "Flutter!";
132+
MethodChannel.Result result = mock(MethodChannel.Result.class);
133+
processTextPlugin.processTextAction(action1Id, textToBeProcessed, false, result);
134+
135+
// Activity.startActivityForResult should have been called.
136+
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
137+
verify(mockActivity, times(1)).startActivityForResult(intentCaptor.capture(), anyInt());
138+
Intent intent = intentCaptor.getValue();
139+
assertEquals(intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT), textToBeProcessed);
140+
141+
// Simulate an Android activity answer which does not return any value.
142+
Intent resultIntent = new Intent();
143+
processTextPlugin.onActivityResult(result.hashCode(), Activity.RESULT_OK, resultIntent);
144+
145+
// Success with no returned value is expected.
146+
verify(result).success(null);
147+
}
148+
149+
@SuppressWarnings("deprecation")
150+
// setMessageHandler is deprecated.
151+
@Test
152+
public void performProcessTextActionWithReturnedValue() {
153+
DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
154+
PackageManager mockPackageManager = mock(PackageManager.class);
155+
ProcessTextChannel processTextChannel =
156+
new ProcessTextChannel(mockBinaryMessenger, mockPackageManager);
157+
158+
// Set up mocked result for PackageManager.queryIntentActivities.
159+
ResolveInfo action1 = createFakeResolveInfo("Action1", mockPackageManager);
160+
ResolveInfo action2 = createFakeResolveInfo("Action2", mockPackageManager);
161+
List<ResolveInfo> infos = new ArrayList<ResolveInfo>(Arrays.asList(action1, action2));
162+
when(mockPackageManager.queryIntentActivities(
163+
any(Intent.class), any(PackageManager.ResolveInfoFlags.class)))
164+
.thenReturn(infos);
165+
166+
// ProcessTextPlugin should retrieve the mocked text actions.
167+
ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(processTextChannel);
168+
Map<Integer, String> textActions = processTextPlugin.queryTextActions();
169+
final int action1Id = 0;
170+
final int action2Id = 1;
171+
assertEquals(textActions, Map.of(action1Id, "Action1", action2Id, "Action2"));
172+
173+
// Set up activity binding.
174+
ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class);
175+
Activity mockActivity = mock(Activity.class);
176+
when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity);
177+
processTextPlugin.onAttachedToActivity(mockActivityPluginBinding);
178+
179+
// Execute first action.
180+
String textToBeProcessed = "Flutter!";
181+
MethodChannel.Result result = mock(MethodChannel.Result.class);
182+
processTextPlugin.processTextAction(action1Id, textToBeProcessed, false, result);
183+
184+
// Activity.startActivityForResult should have been called.
185+
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
186+
verify(mockActivity, times(1)).startActivityForResult(intentCaptor.capture(), anyInt());
187+
Intent intent = intentCaptor.getValue();
188+
assertEquals(intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT), textToBeProcessed);
189+
190+
// Simulate an Android activity answer which returns a transformed text.
191+
String processedText = "Flutter!!!";
192+
Intent resultIntent = new Intent();
193+
resultIntent.putExtra(Intent.EXTRA_PROCESS_TEXT, processedText);
194+
processTextPlugin.onActivityResult(result.hashCode(), Activity.RESULT_OK, resultIntent);
195+
196+
// Success with the transformed text is expected.
197+
verify(result).success(processedText);
198+
}
199+
200+
private ResolveInfo createFakeResolveInfo(String label, PackageManager mockPackageManager) {
201+
ResolveInfo resolveInfo = mock(ResolveInfo.class);
202+
ActivityInfo activityInfo = new ActivityInfo();
203+
when(resolveInfo.loadLabel(mockPackageManager)).thenReturn(label);
204+
205+
// Use Java reflection to set required member variables.
206+
try {
207+
Field activityField = ResolveInfo.class.getDeclaredField("activityInfo");
208+
activityField.setAccessible(true);
209+
activityField.set(resolveInfo, activityInfo);
210+
Field packageNameField = PackageItemInfo.class.getDeclaredField("packageName");
211+
packageNameField.setAccessible(true);
212+
packageNameField.set(activityInfo, "mockActivityPackageName");
213+
Field nameField = PackageItemInfo.class.getDeclaredField("name");
214+
nameField.setAccessible(true);
215+
nameField.set(activityInfo, "mockActivityName");
216+
} catch (Exception ex) {
217+
// Test will failed if reflection APIs throw.
218+
}
219+
220+
return resolveInfo;
221+
}
222+
}

0 commit comments

Comments
 (0)