Skip to content

Commit b2ec34f

Browse files
authored
Add ProcessTextService (#137145)
## Description This PR adds `ProcessTextService` on the framework side to communicate with the engine to query and run text processing actions (on the engine side, only Android is supported currently, see flutter/engine#44579). ## Related Issue Non-UI framework side for flutter/flutter#107603 ## Tests Adds 3 tests.
1 parent 8ba52bc commit b2ec34f

File tree

4 files changed

+247
-0
lines changed

4 files changed

+247
-0
lines changed

packages/flutter/lib/services.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export 'src/services/mouse_cursor.dart';
3333
export 'src/services/mouse_tracking.dart';
3434
export 'src/services/platform_channel.dart';
3535
export 'src/services/platform_views.dart';
36+
export 'src/services/process_text.dart';
3637
export 'src/services/raw_keyboard.dart';
3738
export 'src/services/raw_keyboard_android.dart';
3839
export 'src/services/raw_keyboard_fuchsia.dart';
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright 2014 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+
import 'package:flutter/foundation.dart';
6+
7+
import 'system_channels.dart';
8+
9+
/// A data structure describing text processing actions.
10+
@immutable
11+
class ProcessTextAction {
12+
/// Creates text processing actions based on those returned by the engine.
13+
const ProcessTextAction(this.id, this.label);
14+
15+
/// The action unique id.
16+
final String id;
17+
18+
/// The action localized label.
19+
final String label;
20+
21+
@override
22+
bool operator ==(Object other) {
23+
if (identical(this, other)) {
24+
return true;
25+
}
26+
27+
return other is ProcessTextAction &&
28+
other.id == id &&
29+
other.label == label;
30+
}
31+
32+
@override
33+
int get hashCode => Object.hash(id, label);
34+
}
35+
36+
/// Determines how to interact with the text processing feature.
37+
abstract class ProcessTextService {
38+
/// Returns a [Future] that resolves to a [List] of [ProcessTextAction]s
39+
/// containing all text processing actions available.
40+
///
41+
/// If there are no actions available, an empty list will be returned.
42+
Future<List<ProcessTextAction>> queryTextActions();
43+
44+
/// Returns a [Future] that resolves to a [String] when the text action
45+
/// returns a transformed text or null when the text action did not return
46+
/// a transformed text.
47+
///
48+
/// The `id` parameter is the text action unique identifier returned by
49+
/// [queryTextActions].
50+
///
51+
/// The `text` parameter is the text to be processed.
52+
///
53+
/// The `readOnly` parameter indicates that the transformed text, if it exists,
54+
/// will be used as read-only.
55+
Future<String?> processTextAction(String id, String text, bool readOnly);
56+
}
57+
58+
/// The service used by default for the text processing feature.
59+
///
60+
/// Any widget may use this service to get a list of text processing actions
61+
/// and send requests to activate these text actions.
62+
///
63+
/// This is currently only supported by Android.
64+
///
65+
/// See also:
66+
///
67+
/// * [ProcessTextService], the service that this implements.
68+
class DefaultProcessTextService implements ProcessTextService {
69+
/// Creates the default service to interact with the platform text processing
70+
/// feature via communication over the text processing [MethodChannel].
71+
DefaultProcessTextService() {
72+
_processTextChannel = SystemChannels.processText;
73+
}
74+
75+
/// The channel used to communicate with the engine side.
76+
late MethodChannel _processTextChannel;
77+
78+
/// Set the [MethodChannel] used to communicate with the engine text processing
79+
/// feature.
80+
///
81+
/// This is only meant for testing within the Flutter SDK.
82+
@visibleForTesting
83+
void setChannel(MethodChannel newChannel) {
84+
assert(() {
85+
_processTextChannel = newChannel;
86+
return true;
87+
}());
88+
}
89+
90+
@override
91+
Future<List<ProcessTextAction>> queryTextActions() async {
92+
final List<ProcessTextAction> textActions = <ProcessTextAction>[];
93+
final Map<Object?, Object?>? rawResults;
94+
95+
try {
96+
rawResults = await _processTextChannel.invokeMethod(
97+
'ProcessText.queryTextActions',
98+
) as Map<Object?, Object?>;
99+
} catch (e) {
100+
return textActions;
101+
}
102+
103+
for (final Object? id in rawResults.keys) {
104+
textActions.add(ProcessTextAction(id! as String, rawResults[id]! as String));
105+
}
106+
107+
return textActions;
108+
}
109+
110+
@override
111+
/// On Android, the readOnly parameter might be used by the targeted activity, see:
112+
/// https://developer.android.com/reference/android/content/Intent#EXTRA_PROCESS_TEXT_READONLY.
113+
Future<String?> processTextAction(String id, String text, bool readOnly) async {
114+
final String? processedText = await _processTextChannel.invokeMethod(
115+
'ProcessText.processTextAction',
116+
<dynamic>[id, text, readOnly],
117+
) as String?;
118+
119+
return processedText;
120+
}
121+
}

packages/flutter/lib/src/services/system_channels.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ abstract final class SystemChannels {
144144
JSONMethodCodec(),
145145
);
146146

147+
/// A [MethodChannel] for handling text processing actions.
148+
///
149+
/// This channel exposes the text processing feature for supported platforms.
150+
/// Currently supported on Android only.
151+
static const MethodChannel processText = OptionalMethodChannel(
152+
'flutter/processtext',
153+
);
154+
147155
/// A JSON [MethodChannel] for handling text input.
148156
///
149157
/// This channel exposes a system text input control for interacting with IMEs
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2014 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+
import 'package:flutter/services.dart';
6+
import 'package:flutter_test/flutter_test.dart';
7+
8+
void main() {
9+
TestWidgetsFlutterBinding.ensureInitialized();
10+
11+
test('ProcessTextService.queryTextActions emits correct method call', () async {
12+
final List<MethodCall> log = <MethodCall>[];
13+
14+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.processText, (MethodCall methodCall) async {
15+
log.add(methodCall);
16+
return null;
17+
});
18+
19+
final ProcessTextService processTextService = DefaultProcessTextService();
20+
await processTextService.queryTextActions();
21+
22+
expect(log, hasLength(1));
23+
expect(log.single, isMethodCall('ProcessText.queryTextActions', arguments: null));
24+
});
25+
26+
test('ProcessTextService.processTextAction emits correct method call', () async {
27+
final List<MethodCall> log = <MethodCall>[];
28+
29+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.processText, (MethodCall methodCall) async {
30+
log.add(methodCall);
31+
return null;
32+
});
33+
34+
final ProcessTextService processTextService = DefaultProcessTextService();
35+
const String fakeActionId = 'fakeActivity.fakeAction';
36+
const String textToProcess = 'Flutter';
37+
await processTextService.processTextAction(fakeActionId, textToProcess, false);
38+
39+
expect(log, hasLength(1));
40+
expect(log.single, isMethodCall('ProcessText.processTextAction', arguments: <Object>[fakeActionId, textToProcess, false]));
41+
});
42+
43+
test('ProcessTextService handles engine answers over the channel', () async {
44+
const String action1Id = 'fakeActivity.fakeAction1';
45+
const String action2Id = 'fakeActivity.fakeAction2';
46+
47+
// Fake channel that simulates responses returned from the engine.
48+
final MethodChannel fakeChannel = FakeProcessTextChannel((MethodCall call) async {
49+
if (call.method == 'ProcessText.queryTextActions') {
50+
return <String, String>{
51+
action1Id: 'Action1',
52+
action2Id: 'Action2',
53+
};
54+
}
55+
if (call.method == 'ProcessText.processTextAction') {
56+
final List<dynamic> args = call.arguments as List<dynamic>;
57+
final String actionId = args[0] as String;
58+
final String testToProcess = args[1] as String;
59+
if (actionId == action1Id) {
60+
// Simulates an action that returns a transformed text.
61+
return '$testToProcess!!!';
62+
}
63+
// Simulates an action that failed or does not transform text.
64+
return null;
65+
}
66+
});
67+
68+
final DefaultProcessTextService processTextService = DefaultProcessTextService();
69+
processTextService.setChannel(fakeChannel);
70+
71+
final List<ProcessTextAction> actions = await processTextService.queryTextActions();
72+
expect(actions, hasLength(2));
73+
74+
const String textToProcess = 'Flutter';
75+
String? processedText;
76+
77+
processedText = await processTextService.processTextAction(action1Id, textToProcess, false);
78+
expect(processedText, 'Flutter!!!');
79+
80+
processedText = await processTextService.processTextAction(action2Id, textToProcess, false);
81+
expect(processedText, null);
82+
});
83+
}
84+
85+
class FakeProcessTextChannel implements MethodChannel {
86+
FakeProcessTextChannel(this.outgoing);
87+
88+
Future<dynamic> Function(MethodCall) outgoing;
89+
Future<void> Function(MethodCall)? incoming;
90+
91+
List<MethodCall> outgoingCalls = <MethodCall>[];
92+
93+
@override
94+
BinaryMessenger get binaryMessenger => throw UnimplementedError();
95+
96+
@override
97+
MethodCodec get codec => const StandardMethodCodec();
98+
99+
@override
100+
Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) => throw UnimplementedError();
101+
102+
@override
103+
Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) => throw UnimplementedError();
104+
105+
@override
106+
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
107+
final MethodCall call = MethodCall(method, arguments);
108+
outgoingCalls.add(call);
109+
return await outgoing(call) as T;
110+
}
111+
112+
@override
113+
String get name => 'flutter/processtext';
114+
115+
@override
116+
void setMethodCallHandler(Future<void> Function(MethodCall call)? handler) => incoming = handler;
117+
}

0 commit comments

Comments
 (0)