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
diff --git a/examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart b/examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart
new file mode 100644
index 0000000..7334621
--- /dev/null
+++ b/examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart
@@ -0,0 +1,73 @@
+// 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!),
+ ],
+ ),
+ );
+ }
+}
diff --git a/examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart b/examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart
new file mode 100644
index 0000000..f81f338
--- /dev/null
+++ b/examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart
@@ -0,0 +1,57 @@
+// 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);
+ });
+}
diff --git a/packages/flutter/lib/services.dart b/packages/flutter/lib/services.dart
index fb8420f..681c918 100644
--- a/packages/flutter/lib/services.dart
+++ b/packages/flutter/lib/services.dart
@@ -21,6 +21,7 @@
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';
diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart
index 98ab4cc..8215d38 100644
--- a/packages/flutter/lib/src/cupertino/text_field.dart
+++ b/packages/flutter/lib/src/cupertino/text_field.dart
@@ -274,6 +274,7 @@
this.scrollController,
this.scrollPhysics,
this.autofillHints = const <String>[],
+ this.contentInsertionConfiguration,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scribbleEnabled = true,
@@ -403,6 +404,7 @@
this.scrollController,
this.scrollPhysics,
this.autofillHints = const <String>[],
+ this.contentInsertionConfiguration,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scribbleEnabled = true,
@@ -723,6 +725,9 @@
/// {@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 @@
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 @@
restorationId: 'editable',
scribbleEnabled: widget.scribbleEnabled,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
+ contentInsertionConfiguration: widget.contentInsertionConfiguration,
contextMenuBuilder: widget.contextMenuBuilder,
spellCheckConfiguration: spellCheckConfiguration,
),
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index ac527f2..49ac280 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -307,6 +307,7 @@
this.scrollController,
this.scrollPhysics,
this.autofillHints = const <String>[],
+ this.contentInsertionConfiguration,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scribbleEnabled = true,
@@ -754,6 +755,9 @@
/// {@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 @@
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 @@
restorationId: 'editable',
scribbleEnabled: widget.scribbleEnabled,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
+ contentInsertionConfiguration: widget.contentInsertionConfiguration,
contextMenuBuilder: widget.contextMenuBuilder,
spellCheckConfiguration: spellCheckConfiguration,
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
diff --git a/packages/flutter/lib/src/services/keyboard_inserted_content.dart b/packages/flutter/lib/src/services/keyboard_inserted_content.dart
new file mode 100644
index 0000000..47553ac
--- /dev/null
+++ b/packages/flutter/lib/src/services/keyboard_inserted_content.dart
@@ -0,0 +1,58 @@
+// 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);
+}
diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart
index b346660..8cb89e9 100644
--- a/packages/flutter/lib/src/services/text_input.dart
+++ b/packages/flutter/lib/src/services/text_input.dart
@@ -17,6 +17,7 @@
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 @@
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 @@
/// {@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 @@
Brightness? keyboardAppearance,
TextCapitalization? textCapitalization,
bool? enableIMEPersonalizedLearning,
+ List<String>? allowedMimeTypes,
AutofillConfiguration? autofillConfiguration,
bool? enableDeltaModel,
}) {
@@ -650,6 +656,7 @@
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 @@
'textCapitalization': textCapitalization.toString(),
'keyboardAppearance': keyboardAppearance.toString(),
'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning,
+ 'contentCommitMimeTypes': allowedMimeTypes,
if (autofill != null) 'autofill': autofill,
'enableDeltaModel' : enableDeltaModel,
};
@@ -1105,6 +1113,9 @@
/// 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 @@
(_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>();
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index 56633e4..0bac5f4 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -46,7 +46,7 @@
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 @@
// 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 @@
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 @@
this.scrollBehavior,
this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true,
+ this.contentInsertionConfiguration,
this.contextMenuBuilder,
this.spellCheckConfiguration,
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
@@ -1632,6 +1712,37 @@
/// {@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 @@
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 @@
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 @@
keyboardAppearance: widget.keyboardAppearance,
autofillConfiguration: autofillConfiguration,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
+ allowedMimeTypes: widget.contentInsertionConfiguration == null
+ ? const <String>[]
+ : widget.contentInsertionConfiguration!.allowedMimeTypes,
);
}
diff --git a/packages/flutter/test/services/autofill_test.dart b/packages/flutter/test/services/autofill_test.dart
index dba33c4..dbb6554 100644
--- a/packages/flutter/test/services/autofill_test.dart
+++ b/packages/flutter/test/services/autofill_test.dart
@@ -125,6 +125,11 @@
}
@override
+ void insertContent(KeyboardInsertedContent content) {
+ latestMethodCall = 'commitContent';
+ }
+
+ @override
void updateFloatingCursor(RawFloatingCursorPoint point) {
latestMethodCall = 'updateFloatingCursor';
}
diff --git a/packages/flutter/test/services/delta_text_input_test.dart b/packages/flutter/test/services/delta_text_input_test.dart
index 4e98c5c..662b397 100644
--- a/packages/flutter/test/services/delta_text_input_test.dart
+++ b/packages/flutter/test/services/delta_text_input_test.dart
@@ -247,6 +247,11 @@
}
@override
+ void insertContent(KeyboardInsertedContent content) {
+ latestMethodCall = 'commitContent';
+ }
+
+ @override
void updateEditingValue(TextEditingValue value) {
latestMethodCall = 'updateEditingValue';
}
diff --git a/packages/flutter/test/services/text_input_test.dart b/packages/flutter/test/services/text_input_test.dart
index 221491f..8c8ba8e 100644
--- a/packages/flutter/test/services/text_input_test.dart
+++ b/packages/flutter/test/services/text_input_test.dart
@@ -405,6 +405,31 @@
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();
@@ -988,6 +1013,11 @@
}
@override
+ void insertContent(KeyboardInsertedContent content) {
+ latestMethodCall = 'commitContent';
+ }
+
+ @override
void updateEditingValue(TextEditingValue value) {
latestMethodCall = 'updateEditingValue';
}
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
index 992df3c..3ea6092 100644
--- a/packages/flutter/test/widgets/editable_text_test.dart
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -422,6 +422,62 @@
);
});
+ 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(