mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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:
parent
f263e6a49f
commit
d003a7f2fb
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user