Unverified Commit 0e22aca7 authored by Tanay Neotia's avatar Tanay Neotia Committed by GitHub

Add support for image insertion on Android (#110052)

* Add support for image insertion on Android

* Fix checks

* Use proper Dart syntax on snippet

* Specify type annotation on list

* Fix nits, add some asserts, and improve example code

* Add missing import

* Fix nullsafety error

* Fix nullsafety error

* Remove reference to contentCommitMimeTypes in docs

* Fix nits

* Fix warnings and import

* Add test for content commit in editable_text_test.dart

* Check that URIs are equal in test

* Fix nits and rename functions / classes to be more self-explanatory

* Fix failing debugFillProperties tests

* Add empty implementation to `insertContent` in TextInputClient

* Tweak documentation slightly

* Improve docs for contentInsertionMimeTypes and fix assert

* Rework contentInsertionMimeType asserts

* Add test for onContentInserted example

* Switch implementation to a configuration class for more granularity in setting mime types

* Fix nits

* Improve docs and fix doc tests

* Fix more nits (LongCatIsLooong)

* Fix failing tests

* Make parameters (guaranteed by platform to be non-nullable) non-nullable

* Fix analysis issues
parent 7bf95f41
// 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.
// Flutter code sample for EditableText.onContentInserted
import 'dart:typed_data';
import 'package:flutter/material.dart';
void main() => runApp(const KeyboardInsertedContentApp());
class KeyboardInsertedContentApp extends StatelessWidget {
const KeyboardInsertedContentApp({super.key});
static const String _title = 'Keyboard Inserted Content Sample';
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: KeyboardInsertedContentDemo(),
);
}
}
class KeyboardInsertedContentDemo extends StatefulWidget {
const KeyboardInsertedContentDemo({super.key});
@override
State<KeyboardInsertedContentDemo> createState() => _KeyboardInsertedContentDemoState();
}
class _KeyboardInsertedContentDemoState extends State<KeyboardInsertedContentDemo> {
final TextEditingController _controller = TextEditingController();
Uint8List? bytes;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Keyboard Inserted Content Sample')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("Here's a text field that supports inserting only png or gif content:"),
TextField(
controller: _controller,
contentInsertionConfiguration: ContentInsertionConfiguration(
allowedMimeTypes: const <String>['image/png', 'image/gif'],
onContentInserted: (KeyboardInsertedContent data) async {
if (data.data != null) {
setState(() {
bytes = data.data;
});
}
},
),
),
if (bytes != null)
const Text("Here's the most recently inserted content:"),
if (bytes != null)
Image.memory(bytes!),
],
),
);
}
}
// 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 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_api_samples/widgets/editable_text/editable_text.on_content_inserted.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Image.memory displays inserted content', (WidgetTester tester) async {
await tester.pumpWidget(
const example.KeyboardInsertedContentApp(),
);
expect(find.text('Keyboard Inserted Content Sample'), findsOneWidget);
await tester.tap(find.byType(EditableText));
await tester.enterText(find.byType(EditableText), 'test');
await tester.idle();
const String uri = 'content://com.google.android.inputmethod.latin.fileprovider/test.png';
const List<int> kBlueSquarePng = <int>[
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x08, 0x06,
0x00, 0x00, 0x00, 0x1e, 0x3f, 0x88, 0xb1, 0x00, 0x00, 0x00, 0x48, 0x49, 0x44,
0x41, 0x54, 0x78, 0xda, 0xed, 0xcf, 0x31, 0x0d, 0x00, 0x30, 0x08, 0x00, 0xb0,
0x61, 0x63, 0x2f, 0xfe, 0x2d, 0x61, 0x05, 0x34, 0xf0, 0x92, 0xd6, 0x41, 0x23,
0x7f, 0xf5, 0x3b, 0x20, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
0x44, 0x44, 0x44, 0x36, 0x06, 0x03, 0x6e, 0x69, 0x47, 0x12, 0x8e, 0xea, 0xaa,
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
];
final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[
-1,
'TextInputAction.commitContent',
jsonDecode('{"mimeType": "image/png", "data": $kBlueSquarePng, "uri": "$uri"}'),
],
'method': 'TextInputClient.performAction',
});
try {
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? _) {},
);
} catch (_) {}
await tester.pumpAndSettle();
expect(find.byType(Image), findsOneWidget);
});
}
......@@ -21,6 +21,7 @@ export 'src/services/deferred_component.dart';
export 'src/services/font_loader.dart';
export 'src/services/haptic_feedback.dart';
export 'src/services/hardware_keyboard.dart';
export 'src/services/keyboard_inserted_content.dart';
export 'src/services/keyboard_key.g.dart';
export 'src/services/keyboard_maps.g.dart';
export 'src/services/message_codec.dart';
......
......@@ -274,6 +274,7 @@ class CupertinoTextField extends StatefulWidget {
this.scrollController,
this.scrollPhysics,
this.autofillHints = const <String>[],
this.contentInsertionConfiguration,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scribbleEnabled = true,
......@@ -403,6 +404,7 @@ class CupertinoTextField extends StatefulWidget {
this.scrollController,
this.scrollPhysics,
this.autofillHints = const <String>[],
this.contentInsertionConfiguration,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scribbleEnabled = true,
......@@ -723,6 +725,9 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning;
/// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
final ContentInsertionConfiguration? contentInsertionConfiguration;
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
///
/// If not provided, will build a default menu based on the platform.
......@@ -819,6 +824,7 @@ class CupertinoTextField extends StatefulWidget {
properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
properties.add(DiagnosticsProperty<SpellCheckConfiguration>('spellCheckConfiguration', spellCheckConfiguration, defaultValue: null));
properties.add(DiagnosticsProperty<List<String>>('contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[], defaultValue: contentInsertionConfiguration == null ? const <String>[] : kDefaultContentInsertionMimeTypes));
}
static final TextMagnifierConfiguration _iosMagnifierConfiguration = TextMagnifierConfiguration(
......@@ -1328,6 +1334,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
restorationId: 'editable',
scribbleEnabled: widget.scribbleEnabled,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
contentInsertionConfiguration: widget.contentInsertionConfiguration,
contextMenuBuilder: widget.contextMenuBuilder,
spellCheckConfiguration: spellCheckConfiguration,
),
......
......@@ -307,6 +307,7 @@ class TextField extends StatefulWidget {
this.scrollController,
this.scrollPhysics,
this.autofillHints = const <String>[],
this.contentInsertionConfiguration,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scribbleEnabled = true,
......@@ -754,6 +755,9 @@ class TextField extends StatefulWidget {
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning;
/// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
final ContentInsertionConfiguration? contentInsertionConfiguration;
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
///
/// If not provided, will build a default menu based on the platform.
......@@ -865,6 +869,7 @@ class TextField extends StatefulWidget {
properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
properties.add(DiagnosticsProperty<SpellCheckConfiguration>('spellCheckConfiguration', spellCheckConfiguration, defaultValue: null));
properties.add(DiagnosticsProperty<List<String>>('contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[], defaultValue: contentInsertionConfiguration == null ? const <String>[] : kDefaultContentInsertionMimeTypes));
}
}
......@@ -1361,6 +1366,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
restorationId: 'editable',
scribbleEnabled: widget.scribbleEnabled,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
contentInsertionConfiguration: widget.contentInsertionConfiguration,
contextMenuBuilder: widget.contextMenuBuilder,
spellCheckConfiguration: spellCheckConfiguration,
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
......
// 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';
/// A class representing rich content (such as a PNG image) inserted via the
/// system input method.
///
/// The following data is represented in this class:
/// - MIME Type
/// - Bytes
/// - URI
@immutable
class KeyboardInsertedContent {
/// Creates an object to represent content that is inserted from the virtual
/// keyboard.
///
/// The mime type and URI will always be provided, but the bytedata may be null.
const KeyboardInsertedContent({required this.mimeType, required this.uri, this.data});
/// Converts JSON received from the Flutter Engine into the Dart class.
KeyboardInsertedContent.fromJson(Map<String, dynamic> metadata):
mimeType = metadata['mimeType'] as String,
uri = metadata['uri'] as String,
data = metadata['data'] != null
? Uint8List.fromList(List<int>.from(metadata['data'] as Iterable<dynamic>))
: null;
/// The mime type of the inserted content.
final String mimeType;
/// The URI (location) of the inserted content, usually a "content://" URI.
final String uri;
/// The bytedata of the inserted content.
final Uint8List? data;
/// Convenience getter to check if bytedata is available for the inserted content.
bool get hasData => data?.isNotEmpty ?? false;
@override
String toString() => '${objectRuntimeType(this, 'KeyboardInsertedContent')}($mimeType, $uri, $data)';
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is KeyboardInsertedContent
&& other.mimeType == mimeType
&& other.uri == uri
&& other.data == data;
}
@override
int get hashCode => Object.hash(mimeType, uri, data);
}
......@@ -17,6 +17,7 @@ import 'package:vector_math/vector_math_64.dart' show Matrix4;
import 'autofill.dart';
import 'clipboard.dart' show Clipboard;
import 'keyboard_inserted_content.dart';
import 'message_codec.dart';
import 'platform_channel.dart';
import 'system_channels.dart';
......@@ -477,6 +478,7 @@ class TextInputConfiguration {
this.textCapitalization = TextCapitalization.none,
this.autofillConfiguration = AutofillConfiguration.disabled,
this.enableIMEPersonalizedLearning = true,
this.allowedMimeTypes = const <String>[],
this.enableDeltaModel = false,
}) : smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled);
......@@ -618,6 +620,9 @@ class TextInputConfiguration {
/// {@endtemplate}
final bool enableIMEPersonalizedLearning;
/// {@macro flutter.widgets.contentInsertionConfiguration.allowedMimeTypes}
final List<String> allowedMimeTypes;
/// Creates a copy of this [TextInputConfiguration] with the given fields
/// replaced with new values.
TextInputConfiguration copyWith({
......@@ -634,6 +639,7 @@ class TextInputConfiguration {
Brightness? keyboardAppearance,
TextCapitalization? textCapitalization,
bool? enableIMEPersonalizedLearning,
List<String>? allowedMimeTypes,
AutofillConfiguration? autofillConfiguration,
bool? enableDeltaModel,
}) {
......@@ -650,6 +656,7 @@ class TextInputConfiguration {
textCapitalization: textCapitalization ?? this.textCapitalization,
keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance,
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning?? this.enableIMEPersonalizedLearning,
allowedMimeTypes: allowedMimeTypes ?? this.allowedMimeTypes,
autofillConfiguration: autofillConfiguration ?? this.autofillConfiguration,
enableDeltaModel: enableDeltaModel ?? this.enableDeltaModel,
);
......@@ -697,6 +704,7 @@ class TextInputConfiguration {
'textCapitalization': textCapitalization.toString(),
'keyboardAppearance': keyboardAppearance.toString(),
'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning,
'contentCommitMimeTypes': allowedMimeTypes,
if (autofill != null) 'autofill': autofill,
'enableDeltaModel' : enableDeltaModel,
};
......@@ -1105,6 +1113,9 @@ mixin TextInputClient {
/// Requests that this client perform the given action.
void performAction(TextInputAction action);
/// Notify client about new content insertion from Android keyboard.
void insertContent(KeyboardInsertedContent content) {}
/// Request from the input method that this client perform the given private
/// command.
///
......@@ -1847,7 +1858,12 @@ class TextInput {
(_currentConnection!._client as DeltaTextInputClient).updateEditingValueWithDeltas(deltas);
break;
case 'TextInputClient.performAction':
_currentConnection!._client.performAction(_toTextInputAction(args[1] as String));
if (args[1] as String == 'TextInputAction.commitContent') {
final KeyboardInsertedContent content = KeyboardInsertedContent.fromJson(args[2] as Map<String, dynamic>);
_currentConnection!._client.insertContent(content);
} else {
_currentConnection!._client.performAction(_toTextInputAction(args[1] as String));
}
break;
case 'TextInputClient.performSelectors':
final List<String> selectors = (args[1] as List<dynamic>).cast<String>();
......
......@@ -46,7 +46,7 @@ import 'ticker_provider.dart';
import 'view.dart';
import 'widget_span.dart';
export 'package:flutter/services.dart' show SelectionChangedCause, SmartDashesType, SmartQuotesType, TextEditingValue, TextInputType, TextSelection;
export 'package:flutter/services.dart' show KeyboardInsertedContent, SelectionChangedCause, SmartDashesType, SmartQuotesType, TextEditingValue, TextInputType, TextSelection;
// Examples can assume:
// late BuildContext context;
......@@ -84,6 +84,19 @@ const Duration _kCursorBlinkHalfPeriod = Duration(milliseconds: 500);
// is shown in an obscured text field.
const int _kObscureShowLatestCharCursorTicks = 3;
/// The default mime types to be used when allowedMimeTypes is not provided.
///
/// The default value supports inserting images of any supported format.
const List<String> kDefaultContentInsertionMimeTypes = <String>[
'image/png',
'image/bmp',
'image/jpg',
'image/tiff',
'image/gif',
'image/jpeg',
'image/webp'
];
class _CompositionCallback extends SingleChildRenderObjectWidget {
const _CompositionCallback({ required this.compositeCallback, required this.enabled, super.child });
final CompositionCallback compositeCallback;
......@@ -377,6 +390,72 @@ class ToolbarOptions {
final bool selectAll;
}
/// Configures the ability to insert media content through the soft keyboard.
///
/// The configuration provides a handler for any rich content inserted through
/// the system input method, and also provides the ability to limit the mime
/// types of the inserted content.
///
/// See also:
///
/// * [EditableText.contentInsertionConfiguration]
class ContentInsertionConfiguration {
/// Creates a content insertion configuration with the specified options.
///
/// A handler for inserted content, in the form of [onContentInserted], must
/// be supplied.
///
/// The allowable mime types of inserted content may also
/// be provided via [allowedMimeTypes], which cannot be an empty list.
ContentInsertionConfiguration({
required this.onContentInserted,
this.allowedMimeTypes = kDefaultContentInsertionMimeTypes,
}) : assert(allowedMimeTypes.isNotEmpty);
/// Called when a user inserts content through the virtual / on-screen keyboard,
/// currently only used on Android.
///
/// [KeyboardInsertedContent] holds the data representing the inserted content.
///
/// {@tool dartpad}
///
/// This example shows how to access the data for inserted content in your
/// `TextField`.
///
/// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * <https://developer.android.com/guide/topics/text/image-keyboard>
final ValueChanged<KeyboardInsertedContent> onContentInserted;
/// {@template flutter.widgets.contentInsertionConfiguration.allowedMimeTypes}
/// Used when a user inserts image-based content through the device keyboard,
/// currently only used on Android.
///
/// The passed list of strings will determine which MIME types are allowed to
/// be inserted via the device keyboard.
///
/// The default mime types are given by [kDefaultContentInsertionMimeTypes].
/// These are all the mime types that are able to be handled and inserted
/// from keyboards.
///
/// This field cannot be an empty list.
///
/// {@tool dartpad}
/// This example shows how to limit image insertion to specific file types.
///
/// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * <https://developer.android.com/guide/topics/text/image-keyboard>
/// {@endtemplate}
final List<String> allowedMimeTypes;
}
// A time-value pair that represents a key frame in an animation.
class _KeyFrame {
const _KeyFrame(this.time, this.value);
......@@ -723,6 +802,7 @@ class EditableText extends StatefulWidget {
this.scrollBehavior,
this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true,
this.contentInsertionConfiguration,
this.contextMenuBuilder,
this.spellCheckConfiguration,
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
......@@ -1632,6 +1712,37 @@ class EditableText extends StatefulWidget {
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning;
/// {@template flutter.widgets.editableText.contentInsertionConfiguration}
/// Configuration of handler for media content inserted via the system input
/// method.
///
/// Defaults to null in which case media content insertion will be disabled,
/// and the system will display a message informing the user that the text field
/// does not support inserting media content.
///
/// Set [ContentInsertionConfiguration.onContentInserted] to provide a handler.
/// Additionally, set [ContentInsertionConfiguration.allowedMimeTypes]
/// to limit the allowable mime types for inserted content.
///
/// {@tool dartpad}
///
/// This example shows how to access the data for inserted content in your
/// `TextField`.
///
/// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart **
/// {@end-tool}
///
/// If [contentInsertionConfiguration] is not provided, by default
/// an empty list of mime types will be sent to the Flutter Engine.
/// A handler function must be provided in order to customize the allowable
/// mime types for inserted content.
///
/// If rich content is inserted without a handler, the system will display
/// a message informing the user that the current text input does not support
/// inserting rich content.
/// {@endtemplate}
final ContentInsertionConfiguration? contentInsertionConfiguration;
/// {@template flutter.widgets.EditableText.contextMenuBuilder}
/// Builds the text selection toolbar when requested by the user.
///
......@@ -1920,6 +2031,7 @@ class EditableText extends StatefulWidget {
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableInteractiveSelection', enableInteractiveSelection, defaultValue: true));
properties.add(DiagnosticsProperty<SpellCheckConfiguration>('spellCheckConfiguration', spellCheckConfiguration, defaultValue: null));
properties.add(DiagnosticsProperty<List<String>>('contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[], defaultValue: contentInsertionConfiguration == null ? const <String>[] : kDefaultContentInsertionMimeTypes));
}
}
......@@ -2730,6 +2842,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
widget.onAppPrivateCommand?.call(action, data);
}
@override
void insertContent(KeyboardInsertedContent content) {
assert(widget.contentInsertionConfiguration?.allowedMimeTypes.contains(content.mimeType) ?? false);
widget.contentInsertionConfiguration?.onContentInserted.call(content);
}
// The original position of the caret on FloatingCursorDragState.start.
Rect? _startCaretRect;
......@@ -3916,6 +4034,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
keyboardAppearance: widget.keyboardAppearance,
autofillConfiguration: autofillConfiguration,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
allowedMimeTypes: widget.contentInsertionConfiguration == null
? const <String>[]
: widget.contentInsertionConfiguration!.allowedMimeTypes,
);
}
......
......@@ -124,6 +124,11 @@ class FakeAutofillClient implements TextInputClient, AutofillClient {
latestMethodCall = 'performPrivateCommand';
}
@override
void insertContent(KeyboardInsertedContent content) {
latestMethodCall = 'commitContent';
}
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {
latestMethodCall = 'updateFloatingCursor';
......
......@@ -246,6 +246,11 @@ class FakeDeltaTextInputClient implements DeltaTextInputClient {
latestMethodCall = 'performPrivateCommand';
}
@override
void insertContent(KeyboardInsertedContent content) {
latestMethodCall = 'commitContent';
}
@override
void updateEditingValue(TextEditingValue value) {
latestMethodCall = 'updateEditingValue';
......
......@@ -405,6 +405,31 @@ void main() {
expect(client.latestMethodCall, 'connectionClosed');
});
test('TextInputClient insertContent method is called', () async {
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
const TextInputConfiguration configuration = TextInputConfiguration();
TextInput.attach(client, configuration);
expect(client.latestMethodCall, isEmpty);
// Send commitContent message with fake GIF data.
final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[
1,
'TextInputAction.commitContent',
jsonDecode('{"mimeType": "image/gif", "data": [0,1,0,1,0,1,0,0,0], "uri": "content://com.google.android.inputmethod.latin.fileprovider/test.gif"}'),
],
'method': 'TextInputClient.performAction',
});
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? _) {},
);
expect(client.latestMethodCall, 'commitContent');
});
test('TextInputClient performSelectors method is called', () async {
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
const TextInputConfiguration configuration = TextInputConfiguration();
......@@ -987,6 +1012,11 @@ class FakeTextInputClient with TextInputClient {
latestPrivateCommandData = data;
}
@override
void insertContent(KeyboardInsertedContent content) {
latestMethodCall = 'commitContent';
}
@override
void updateEditingValue(TextEditingValue value) {
latestMethodCall = 'updateEditingValue';
......
......@@ -422,6 +422,62 @@ void main() {
);
});
testWidgets('insertContent does not throw and parses data correctly', (WidgetTester tester) async {
String? latestUri;
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
contentInsertionConfiguration: ContentInsertionConfiguration(
onContentInserted: (KeyboardInsertedContent content) {
latestUri = content.uri;
},
allowedMimeTypes: const <String>['image/gif'],
),
),
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.enterText(find.byType(EditableText), 'test');
await tester.idle();
const String uri = 'content://com.google.android.inputmethod.latin.fileprovider/test.gif';
final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[
-1,
'TextInputAction.commitContent',
jsonDecode('{"mimeType": "image/gif", "data": [0,1,0,1,0,1,0,0,0], "uri": "$uri"}'),
],
'method': 'TextInputClient.performAction',
});
Object? error;
try {
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? _) {},
);
} catch (e) {
error = e;
}
expect(error, isNull);
expect(latestUri, equals(uri));
});
testWidgets('onAppPrivateCommand does not throw', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment