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

Commit 0bab9c3

Browse files
committed
[Android] Add support for text processing actions
1 parent 4304d61 commit 0bab9c3

File tree

5 files changed

+296
-0
lines changed

5 files changed

+296
-0
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2416,6 +2416,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/syst
24162416
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java + ../../../flutter/LICENSE
24172417
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java + ../../../flutter/LICENSE
24182418
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java + ../../../flutter/LICENSE
2419+
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java + ../../../flutter/LICENSE
24192420
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java + ../../../flutter/LICENSE
24202421
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java + ../../../flutter/LICENSE
24212422
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java + ../../../flutter/LICENSE
@@ -2458,6 +2459,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platf
24582459
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java + ../../../flutter/LICENSE
24592460
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java + ../../../flutter/LICENSE
24602461
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java + ../../../flutter/LICENSE
2462+
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java + ../../../flutter/LICENSE
24612463
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/util/HandlerCompat.java + ../../../flutter/LICENSE
24622464
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java + ../../../flutter/LICENSE
24632465
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java + ../../../flutter/LICENSE
@@ -5141,6 +5143,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/system
51415143
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java
51425144
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java
51435145
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java
5146+
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java
51445147
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java
51455148
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
51465149
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java
@@ -5188,6 +5191,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platfor
51885191
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java
51895192
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java
51905193
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java
5194+
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/text/ProcessTextPlugin.java
51915195
FILE: ../../../flutter/shell/platform/android/io/flutter/util/HandlerCompat.java
51925196
FILE: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java
51935197
FILE: ../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java

shell/platform/android/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ android_java_sources = [
259259
"io/flutter/embedding/engine/systemchannels/NavigationChannel.java",
260260
"io/flutter/embedding/engine/systemchannels/PlatformChannel.java",
261261
"io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java",
262+
"io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java",
262263
"io/flutter/embedding/engine/systemchannels/RestorationChannel.java",
263264
"io/flutter/embedding/engine/systemchannels/SettingsChannel.java",
264265
"io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java",
@@ -306,6 +307,7 @@ android_java_sources = [
306307
"io/flutter/plugin/platform/SingleViewPresentation.java",
307308
"io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java",
308309
"io/flutter/plugin/platform/VirtualDisplayController.java",
310+
"io/flutter/plugin/text/ProcessTextPlugin.java",
309311
"io/flutter/util/HandlerCompat.java",
310312
"io/flutter/util/PathUtils.java",
311313
"io/flutter/util/Preconditions.java",

shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@
3131
import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
3232
import io.flutter.embedding.engine.systemchannels.NavigationChannel;
3333
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
34+
import io.flutter.embedding.engine.systemchannels.ProcessTextChannel;
3435
import io.flutter.embedding.engine.systemchannels.RestorationChannel;
3536
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
3637
import io.flutter.embedding.engine.systemchannels.SpellCheckChannel;
3738
import io.flutter.embedding.engine.systemchannels.SystemChannel;
3839
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
3940
import io.flutter.plugin.localization.LocalizationPlugin;
4041
import io.flutter.plugin.platform.PlatformViewsController;
42+
import io.flutter.plugin.text.ProcessTextPlugin;
4143
import io.flutter.util.ViewUtils;
4244
import java.util.HashSet;
4345
import java.util.List;
@@ -95,6 +97,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
9597
@NonNull private final NavigationChannel navigationChannel;
9698
@NonNull private final RestorationChannel restorationChannel;
9799
@NonNull private final PlatformChannel platformChannel;
100+
@NonNull private final ProcessTextChannel processTextChannel;
98101
@NonNull private final SettingsChannel settingsChannel;
99102
@NonNull private final SpellCheckChannel spellCheckChannel;
100103
@NonNull private final SystemChannel systemChannel;
@@ -329,6 +332,7 @@ public FlutterEngine(
329332
mouseCursorChannel = new MouseCursorChannel(dartExecutor);
330333
navigationChannel = new NavigationChannel(dartExecutor);
331334
platformChannel = new PlatformChannel(dartExecutor);
335+
processTextChannel = new ProcessTextChannel(dartExecutor, context.getPackageManager());
332336
restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData);
333337
settingsChannel = new SettingsChannel(dartExecutor);
334338
spellCheckChannel = new SpellCheckChannel(dartExecutor);
@@ -382,6 +386,9 @@ public FlutterEngine(
382386
}
383387

384388
ViewUtils.calculateMaximumDisplayMetrics(context, this);
389+
390+
ProcessTextPlugin processTextPlugin = new ProcessTextPlugin(this.getProcessTextChannel());
391+
this.pluginRegistry.add(processTextPlugin);
385392
}
386393

387394
private void attachToJni() {
@@ -543,6 +550,12 @@ public PlatformChannel getPlatformChannel() {
543550
return platformChannel;
544551
}
545552

553+
/** System channel that sends text processing requests from Flutter to Android. */
554+
@NonNull
555+
public ProcessTextChannel getProcessTextChannel() {
556+
return processTextChannel;
557+
}
558+
546559
/**
547560
* System channel to exchange restoration data between framework and engine.
548561
*
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.embedding.engine.systemchannels;
6+
7+
import android.content.pm.PackageManager;
8+
import androidx.annotation.NonNull;
9+
import androidx.annotation.Nullable;
10+
import io.flutter.embedding.engine.dart.DartExecutor;
11+
import io.flutter.plugin.common.MethodCall;
12+
import io.flutter.plugin.common.MethodChannel;
13+
import io.flutter.plugin.common.StandardMethodCodec;
14+
import java.util.ArrayList;
15+
import java.util.Map;
16+
17+
/**
18+
* {@link ProcessTextChannel} is a platform channel that is used by the framework to initiate text
19+
* processing feature in the embedding and for the embedding to send back the results.
20+
*
21+
* <p>TODO(bleroux): add more documentation.
22+
*/
23+
public class ProcessTextChannel {
24+
private static final String TAG = "ProcessTextChannel";
25+
private static final String CHANNEL_NAME = "flutter/processtext";
26+
private static final String METHOD_QUERY_TEXT_ACTIONS = "ProcessText.queryTextActions";
27+
private static final String METHOD_PROCESS_TEXT_ACTION = "ProcessText.processTextAction";
28+
29+
public final MethodChannel channel;
30+
public final PackageManager packageManager;
31+
private ProcessTextMethodHandler processTextMethodHandler;
32+
33+
@NonNull
34+
public final MethodChannel.MethodCallHandler parsingMethodHandler =
35+
new MethodChannel.MethodCallHandler() {
36+
@Override
37+
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
38+
if (processTextMethodHandler == null) {
39+
return;
40+
}
41+
String method = call.method;
42+
Object args = call.arguments;
43+
switch (method) {
44+
case METHOD_QUERY_TEXT_ACTIONS:
45+
try {
46+
Map<Integer, String> actions = processTextMethodHandler.queryTextActions();
47+
result.success(actions);
48+
} catch (IllegalStateException exception) {
49+
result.error("error", exception.getMessage(), null);
50+
}
51+
break;
52+
case METHOD_PROCESS_TEXT_ACTION:
53+
try {
54+
final ArrayList<Object> argumentList = (ArrayList<Object>) args;
55+
int id = (int) (argumentList.get(0));
56+
String text = (String) (argumentList.get(1));
57+
boolean readOnly = (boolean) (argumentList.get(2));
58+
processTextMethodHandler.processTextAction(id, text, readOnly, result);
59+
} catch (IllegalStateException exception) {
60+
result.error("error", exception.getMessage(), null);
61+
}
62+
break;
63+
default:
64+
result.notImplemented();
65+
break;
66+
}
67+
}
68+
};
69+
70+
public ProcessTextChannel(
71+
@NonNull DartExecutor dartExecutor, @NonNull PackageManager packageManager) {
72+
this.packageManager = packageManager;
73+
channel = new MethodChannel(dartExecutor, CHANNEL_NAME, StandardMethodCodec.INSTANCE);
74+
channel.setMethodCallHandler(parsingMethodHandler);
75+
}
76+
77+
/**
78+
* Sets the {@link ProcessTextMethodHandler} which receives all requests to the text processing
79+
* feature sent through this channel.
80+
*/
81+
public void setMethodHandler(@Nullable ProcessTextMethodHandler processTextMethodHandler) {
82+
this.processTextMethodHandler = processTextMethodHandler;
83+
}
84+
85+
public interface ProcessTextMethodHandler {
86+
/**
87+
* Requests the list of text actions. Each text action has a unique id and a localized label.
88+
*/
89+
Map<Integer, String> queryTextActions();
90+
91+
/**
92+
* Requests to run a text action on a given input text.
93+
*
94+
* <p>TODO(bleroux): add documentation for parameters.
95+
*/
96+
void processTextAction(
97+
@NonNull int id,
98+
@NonNull String input,
99+
@NonNull boolean readOnly,
100+
@NonNull MethodChannel.Result result);
101+
}
102+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugin.text;
6+
7+
import android.app.Activity;
8+
import android.content.Intent;
9+
import android.content.pm.PackageManager;
10+
import android.content.pm.ResolveInfo;
11+
import android.os.Build;
12+
import androidx.annotation.NonNull;
13+
import androidx.annotation.Nullable;
14+
import io.flutter.embedding.engine.plugins.FlutterPlugin;
15+
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
16+
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
17+
import io.flutter.embedding.engine.systemchannels.ProcessTextChannel;
18+
import io.flutter.plugin.common.MethodChannel;
19+
import io.flutter.plugin.common.PluginRegistry.ActivityResultListener;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
public class ProcessTextPlugin implements FlutterPlugin, ActivityAware, ActivityResultListener {
25+
private static final String TAG = "ProcessTextPlugin";
26+
27+
@NonNull private final ProcessTextChannel processTextChannel;
28+
@NonNull private final PackageManager packageManager;
29+
@Nullable private ActivityPluginBinding activityBinding;
30+
@NonNull private Map<Integer, ResolveInfo> resolveInfosById = new HashMap<Integer, ResolveInfo>();
31+
32+
@NonNull
33+
private Map<Integer, MethodChannel.Result> requestsByCode =
34+
new HashMap<Integer, MethodChannel.Result>();
35+
36+
public ProcessTextPlugin(@NonNull ProcessTextChannel processTextChannel) {
37+
this.processTextChannel = processTextChannel;
38+
this.packageManager = processTextChannel.packageManager;
39+
40+
this.cacheResolveInfos();
41+
42+
processTextChannel.setMethodHandler(
43+
new ProcessTextChannel.ProcessTextMethodHandler() {
44+
@Override
45+
public Map<Integer, String> queryTextActions() {
46+
Map<Integer, String> result = new HashMap<Integer, String>();
47+
for (Integer id : resolveInfosById.keySet()) {
48+
final ResolveInfo info = resolveInfosById.get(id);
49+
result.put(id, info.loadLabel(packageManager).toString());
50+
}
51+
return result;
52+
}
53+
54+
@Override
55+
public void processTextAction(
56+
@NonNull int id,
57+
@NonNull String text,
58+
@NonNull boolean readOnly,
59+
@NonNull MethodChannel.Result result) {
60+
if (activityBinding == null) {
61+
result.error("error", "Plugin not bound to an Activity", null);
62+
return;
63+
}
64+
65+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
66+
result.error("error", "Android version not supported", null);
67+
return;
68+
}
69+
70+
final ResolveInfo info = resolveInfosById.get(id);
71+
if (info == null) {
72+
result.error("error", "Text processing activity not found", null);
73+
return;
74+
}
75+
76+
Integer requestCode = result.hashCode();
77+
requestsByCode.put(requestCode, result);
78+
79+
Intent intent =
80+
new Intent()
81+
.setClassName(info.activityInfo.packageName, info.activityInfo.name)
82+
.setAction(Intent.ACTION_PROCESS_TEXT)
83+
.setType("text/plain")
84+
.putExtra(Intent.EXTRA_PROCESS_TEXT, text)
85+
.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, readOnly);
86+
87+
// Start the text processing activity. onActivityResult callback is called
88+
// when the activity completes.
89+
activityBinding.getActivity().startActivityForResult(intent, requestCode);
90+
}
91+
});
92+
}
93+
94+
private void cacheResolveInfos() {
95+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
96+
return;
97+
}
98+
99+
Intent intent = new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
100+
101+
List<ResolveInfo> infos;
102+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
103+
infos = packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0));
104+
} else {
105+
infos = packageManager.queryIntentActivities(intent, 0);
106+
}
107+
108+
// Assign an internal id for communication between the engine and the framework.
109+
int index = 0;
110+
resolveInfosById.clear();
111+
for (ResolveInfo info : infos) {
112+
final String label = info.loadLabel(packageManager).toString();
113+
resolveInfosById.put(index++, info);
114+
}
115+
}
116+
117+
/**
118+
* Executed when a text processing activity terminates.
119+
*
120+
* <p>The result is null when an activity does not return an updated text.
121+
*/
122+
public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent intent) {
123+
String result = null;
124+
if (resultCode == Activity.RESULT_OK) {
125+
result = intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT);
126+
}
127+
requestsByCode.remove(requestCode).success(result);
128+
return true;
129+
}
130+
131+
/**
132+
* Unregisters this {@code ProcessTextPlugin} as the {@code
133+
* ProcessTextChannel.ProcessTextMethodHandler}, for the {@link
134+
* io.flutter.embedding.engine.systemchannels.ProcessTextChannel}.
135+
*
136+
* <p>Do not invoke any methods on a {@code ProcessTextPlugin} after invoking this method.
137+
*/
138+
public void destroy() {
139+
processTextChannel.setMethodHandler(null);
140+
}
141+
142+
// FlutterPlugin interface implementation.
143+
144+
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
145+
// Nothing to do because this plugin is instantiated by the engine.
146+
}
147+
148+
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
149+
// Nothing to do because this plugin is instantiated by the engine.
150+
}
151+
152+
// ActivityAware interface implementation.
153+
//
154+
// Store the binding and manage the activity result listerner.
155+
156+
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
157+
this.activityBinding = binding;
158+
this.activityBinding.addActivityResultListener(this);
159+
};
160+
161+
public void onDetachedFromActivityForConfigChanges() {
162+
this.activityBinding.removeActivityResultListener(this);
163+
this.activityBinding = null;
164+
}
165+
166+
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
167+
this.activityBinding = binding;
168+
this.activityBinding.addActivityResultListener(this);
169+
}
170+
171+
public void onDetachedFromActivity() {
172+
this.activityBinding.removeActivityResultListener(this);
173+
this.activityBinding = null;
174+
}
175+
}

0 commit comments

Comments
 (0)