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 https://github.com/flutter/engine/pull/44579). ## Related Issue Non-UI framework side for https://github.com/flutter/flutter/issues/107603 ## Tests Adds 3 tests.
This commit is contained in:
parent
8ba52bc49b
commit
b2ec34fcb4
@ -33,6 +33,7 @@ export 'src/services/mouse_cursor.dart';
|
||||
export 'src/services/mouse_tracking.dart';
|
||||
export 'src/services/platform_channel.dart';
|
||||
export 'src/services/platform_views.dart';
|
||||
export 'src/services/process_text.dart';
|
||||
export 'src/services/raw_keyboard.dart';
|
||||
export 'src/services/raw_keyboard_android.dart';
|
||||
export 'src/services/raw_keyboard_fuchsia.dart';
|
||||
|
121
packages/flutter/lib/src/services/process_text.dart
Normal file
121
packages/flutter/lib/src/services/process_text.dart
Normal file
@ -0,0 +1,121 @@
|
||||
// Copyright 2014 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.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'system_channels.dart';
|
||||
|
||||
/// A data structure describing text processing actions.
|
||||
@immutable
|
||||
class ProcessTextAction {
|
||||
/// Creates text processing actions based on those returned by the engine.
|
||||
const ProcessTextAction(this.id, this.label);
|
||||
|
||||
/// The action unique id.
|
||||
final String id;
|
||||
|
||||
/// The action localized label.
|
||||
final String label;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return other is ProcessTextAction &&
|
||||
other.id == id &&
|
||||
other.label == label;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, label);
|
||||
}
|
||||
|
||||
/// Determines how to interact with the text processing feature.
|
||||
abstract class ProcessTextService {
|
||||
/// Returns a [Future] that resolves to a [List] of [ProcessTextAction]s
|
||||
/// containing all text processing actions available.
|
||||
///
|
||||
/// If there are no actions available, an empty list will be returned.
|
||||
Future<List<ProcessTextAction>> queryTextActions();
|
||||
|
||||
/// Returns a [Future] that resolves to a [String] when the text action
|
||||
/// returns a transformed text or null when the text action did not return
|
||||
/// a transformed text.
|
||||
///
|
||||
/// The `id` parameter is the text action unique identifier returned by
|
||||
/// [queryTextActions].
|
||||
///
|
||||
/// The `text` parameter is the text to be processed.
|
||||
///
|
||||
/// The `readOnly` parameter indicates that the transformed text, if it exists,
|
||||
/// will be used as read-only.
|
||||
Future<String?> processTextAction(String id, String text, bool readOnly);
|
||||
}
|
||||
|
||||
/// The service used by default for the text processing feature.
|
||||
///
|
||||
/// Any widget may use this service to get a list of text processing actions
|
||||
/// and send requests to activate these text actions.
|
||||
///
|
||||
/// This is currently only supported by Android.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ProcessTextService], the service that this implements.
|
||||
class DefaultProcessTextService implements ProcessTextService {
|
||||
/// Creates the default service to interact with the platform text processing
|
||||
/// feature via communication over the text processing [MethodChannel].
|
||||
DefaultProcessTextService() {
|
||||
_processTextChannel = SystemChannels.processText;
|
||||
}
|
||||
|
||||
/// The channel used to communicate with the engine side.
|
||||
late MethodChannel _processTextChannel;
|
||||
|
||||
/// Set the [MethodChannel] used to communicate with the engine text processing
|
||||
/// feature.
|
||||
///
|
||||
/// This is only meant for testing within the Flutter SDK.
|
||||
@visibleForTesting
|
||||
void setChannel(MethodChannel newChannel) {
|
||||
assert(() {
|
||||
_processTextChannel = newChannel;
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ProcessTextAction>> queryTextActions() async {
|
||||
final List<ProcessTextAction> textActions = <ProcessTextAction>[];
|
||||
final Map<Object?, Object?>? rawResults;
|
||||
|
||||
try {
|
||||
rawResults = await _processTextChannel.invokeMethod(
|
||||
'ProcessText.queryTextActions',
|
||||
) as Map<Object?, Object?>;
|
||||
} catch (e) {
|
||||
return textActions;
|
||||
}
|
||||
|
||||
for (final Object? id in rawResults.keys) {
|
||||
textActions.add(ProcessTextAction(id! as String, rawResults[id]! as String));
|
||||
}
|
||||
|
||||
return textActions;
|
||||
}
|
||||
|
||||
@override
|
||||
/// On Android, the readOnly parameter might be used by the targeted activity, see:
|
||||
/// https://developer.android.com/reference/android/content/Intent#EXTRA_PROCESS_TEXT_READONLY.
|
||||
Future<String?> processTextAction(String id, String text, bool readOnly) async {
|
||||
final String? processedText = await _processTextChannel.invokeMethod(
|
||||
'ProcessText.processTextAction',
|
||||
<dynamic>[id, text, readOnly],
|
||||
) as String?;
|
||||
|
||||
return processedText;
|
||||
}
|
||||
}
|
@ -144,6 +144,14 @@ abstract final class SystemChannels {
|
||||
JSONMethodCodec(),
|
||||
);
|
||||
|
||||
/// A [MethodChannel] for handling text processing actions.
|
||||
///
|
||||
/// This channel exposes the text processing feature for supported platforms.
|
||||
/// Currently supported on Android only.
|
||||
static const MethodChannel processText = OptionalMethodChannel(
|
||||
'flutter/processtext',
|
||||
);
|
||||
|
||||
/// A JSON [MethodChannel] for handling text input.
|
||||
///
|
||||
/// This channel exposes a system text input control for interacting with IMEs
|
||||
|
117
packages/flutter/test/services/process_text_test.dart
Normal file
117
packages/flutter/test/services/process_text_test.dart
Normal file
@ -0,0 +1,117 @@
|
||||
// Copyright 2014 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.
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('ProcessTextService.queryTextActions emits correct method call', () async {
|
||||
final List<MethodCall> log = <MethodCall>[];
|
||||
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.processText, (MethodCall methodCall) async {
|
||||
log.add(methodCall);
|
||||
return null;
|
||||
});
|
||||
|
||||
final ProcessTextService processTextService = DefaultProcessTextService();
|
||||
await processTextService.queryTextActions();
|
||||
|
||||
expect(log, hasLength(1));
|
||||
expect(log.single, isMethodCall('ProcessText.queryTextActions', arguments: null));
|
||||
});
|
||||
|
||||
test('ProcessTextService.processTextAction emits correct method call', () async {
|
||||
final List<MethodCall> log = <MethodCall>[];
|
||||
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.processText, (MethodCall methodCall) async {
|
||||
log.add(methodCall);
|
||||
return null;
|
||||
});
|
||||
|
||||
final ProcessTextService processTextService = DefaultProcessTextService();
|
||||
const String fakeActionId = 'fakeActivity.fakeAction';
|
||||
const String textToProcess = 'Flutter';
|
||||
await processTextService.processTextAction(fakeActionId, textToProcess, false);
|
||||
|
||||
expect(log, hasLength(1));
|
||||
expect(log.single, isMethodCall('ProcessText.processTextAction', arguments: <Object>[fakeActionId, textToProcess, false]));
|
||||
});
|
||||
|
||||
test('ProcessTextService handles engine answers over the channel', () async {
|
||||
const String action1Id = 'fakeActivity.fakeAction1';
|
||||
const String action2Id = 'fakeActivity.fakeAction2';
|
||||
|
||||
// Fake channel that simulates responses returned from the engine.
|
||||
final MethodChannel fakeChannel = FakeProcessTextChannel((MethodCall call) async {
|
||||
if (call.method == 'ProcessText.queryTextActions') {
|
||||
return <String, String>{
|
||||
action1Id: 'Action1',
|
||||
action2Id: 'Action2',
|
||||
};
|
||||
}
|
||||
if (call.method == 'ProcessText.processTextAction') {
|
||||
final List<dynamic> args = call.arguments as List<dynamic>;
|
||||
final String actionId = args[0] as String;
|
||||
final String testToProcess = args[1] as String;
|
||||
if (actionId == action1Id) {
|
||||
// Simulates an action that returns a transformed text.
|
||||
return '$testToProcess!!!';
|
||||
}
|
||||
// Simulates an action that failed or does not transform text.
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
final DefaultProcessTextService processTextService = DefaultProcessTextService();
|
||||
processTextService.setChannel(fakeChannel);
|
||||
|
||||
final List<ProcessTextAction> actions = await processTextService.queryTextActions();
|
||||
expect(actions, hasLength(2));
|
||||
|
||||
const String textToProcess = 'Flutter';
|
||||
String? processedText;
|
||||
|
||||
processedText = await processTextService.processTextAction(action1Id, textToProcess, false);
|
||||
expect(processedText, 'Flutter!!!');
|
||||
|
||||
processedText = await processTextService.processTextAction(action2Id, textToProcess, false);
|
||||
expect(processedText, null);
|
||||
});
|
||||
}
|
||||
|
||||
class FakeProcessTextChannel implements MethodChannel {
|
||||
FakeProcessTextChannel(this.outgoing);
|
||||
|
||||
Future<dynamic> Function(MethodCall) outgoing;
|
||||
Future<void> Function(MethodCall)? incoming;
|
||||
|
||||
List<MethodCall> outgoingCalls = <MethodCall>[];
|
||||
|
||||
@override
|
||||
BinaryMessenger get binaryMessenger => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
MethodCodec get codec => const StandardMethodCodec();
|
||||
|
||||
@override
|
||||
Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
|
||||
final MethodCall call = MethodCall(method, arguments);
|
||||
outgoingCalls.add(call);
|
||||
return await outgoing(call) as T;
|
||||
}
|
||||
|
||||
@override
|
||||
String get name => 'flutter/processtext';
|
||||
|
||||
@override
|
||||
void setMethodCallHandler(Future<void> Function(MethodCall call)? handler) => incoming = handler;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user