Unverified Commit 97cf355b authored by Nurhan Turgut's avatar Nurhan Turgut Committed by GitHub

Adding handling of TextInputClient.onConnectionClosed messages handli… (#43466)

* Adding handling of TextInputClient.onConnectionClosed messages handling to Framework

* Adding more test cases for closing connection to editable_text_test

* fixing analyze error.

* Fixing analyze error in the test file

* Fixing comments on the new method

* Adding more closing connection examples.

* Indentation change

* Remove auto-add white space

* Changing the oncloseconnection behaviour to stop editing. Updating the tests

* Addressing PR comments. Added explicit log for method channnels to the tests. Added comments to the interfaces.

* add more documentation
parent 3422540b
......@@ -159,6 +159,14 @@ class SystemChannels {
/// second argument is a [String] consisting of the stringification of one
/// of the values of the [TextInputAction] enum.
///
/// * `TextInputClient.onConnectionClosed`: The text input connection closed
/// on the platform side. For example the application is moved to
/// background or used closed the virtual keyboard. This method informs
/// [TextInputClient] to clear connection and finalize editing.
/// `TextInput.clearClient` and `TextInput.hide` is not called after
/// clearing the connection since on the platform side the connection is
/// already finalized.
///
/// Calls to methods that are not implemented on the shell side are ignored
/// (so it is safe to call methods when the relevant plugin might be missing).
static const MethodChannel textInput = OptionalMethodChannel(
......
......@@ -650,6 +650,11 @@ abstract class TextInputClient {
/// Updates the floating cursor position and state.
void updateFloatingCursor(RawFloatingCursorPoint point);
/// Platform notified framework of closed connection.
///
/// [TextInputClient] should cleanup its connection and finalize editing.
void connectionClosed();
}
/// An interface for interacting with a text input control.
......@@ -751,6 +756,14 @@ class TextInputConnection {
}
assert(!attached);
}
/// Platform sent a notification informing the connection is closed.
///
/// [TextInputConnection] should clean current client connection.
void connectionClosedReceived() {
_clientHandler._currentConnection = null;
assert(!attached);
}
}
TextInputAction _toTextInputAction(String action) {
......@@ -831,6 +844,9 @@ class _TextInputClientHandler {
case 'TextInputClient.updateFloatingCursor':
_currentConnection._client.updateFloatingCursor(_toTextPoint(_toTextCursorAction(args[1]), args[2]));
break;
case 'TextInputClient.onConnectionClosed':
_currentConnection._client.connectionClosed();
break;
default:
throw MissingPluginException();
}
......
......@@ -1444,6 +1444,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
@override
void connectionClosed() {
if (_hasInputConnection) {
_textInputConnection.connectionClosedReceived();
_textInputConnection = null;
_lastKnownRemoteTextEditingValue = null;
_finalizeEditing(true);
}
}
/// Express interest in interacting with the keyboard.
///
/// If this control is already attached to the keyboard, this function will
......
......@@ -88,5 +88,51 @@ void main() {
expect(signed.hashCode == signedDecimal.hashCode, false);
expect(decimal.hashCode == signedDecimal.hashCode, false);
});
test('TextInputClient onConnectionClosed method is called', () async {
// Assemble a TextInputConnection so we can verify its change in state.
final FakeTextInputClient client = FakeTextInputClient();
const TextInputConfiguration configuration = TextInputConfiguration();
TextInput.attach(client, configuration);
expect(client.latestMethodCall, isEmpty);
// Send onConnectionClosed message.
final ByteData messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[1],
'method': 'TextInputClient.onConnectionClosed',
});
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData _) {},
);
expect(client.latestMethodCall, 'connectionClosed');
});
});
}
class FakeTextInputClient extends TextInputClient {
String latestMethodCall = '';
@override
void performAction(TextInputAction action) {
latestMethodCall = 'performAction';
}
@override
void updateEditingValue(TextEditingValue value) {
latestMethodCall = 'updateEditingValue';
}
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {
latestMethodCall = 'updateFloatingCursor';
}
@override
void connectionClosed() {
latestMethodCall = 'connectionClosed';
}
}
......@@ -585,6 +585,175 @@ void main() {
equals('TextInputAction.done'));
});
testWidgets('connection is closed when TextInputClient.onConnectionClosed message received', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
maxLines: 1, // Sets text keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
),
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(state.wantKeepAlive, true);
tester.testTextInput.log.clear();
tester.testTextInput.closeConnection();
await tester.idle();
// Widget does not have focus anymore.
expect(state.wantKeepAlive, false);
// No method calls are sent from the framework.
// This makes sure hide/clearClient methods are not called after connection
// closed.
expect(tester.testTextInput.log, isEmpty);
});
testWidgets('closed connection reopened when user focused', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
maxLines: 1, // Sets text keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
),
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test3';
await tester.idle();
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
expect(tester.testTextInput.editingState['text'], equals('test3'));
expect(state.wantKeepAlive, true);
tester.testTextInput.log.clear();
tester.testTextInput.closeConnection();
await tester.pumpAndSettle();
// Widget does not have focus anymore.
expect(state.wantKeepAlive, false);
// No method calls are sent from the framework.
// This makes sure hide/clearClient methods are not called after connection
// closed.
expect(tester.testTextInput.log, isEmpty);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
await tester.pump();
controller.text = 'test2';
expect(tester.testTextInput.editingState['text'], equals('test2'));
// Widget regained the focus.
expect(state.wantKeepAlive, true);
});
testWidgets('closed connection reopened when user focused on another field', (WidgetTester tester) async {
final EditableText testNameField =
EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
maxLines: null,
keyboardType: TextInputType.text,
style: textStyle,
cursorColor: cursorColor,
);
final EditableText testPhoneField =
EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.phone,
maxLines: 3,
style: textStyle,
cursorColor: cursorColor,
);
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: ListView(
children: <Widget>[
testNameField,
testPhoneField,
],
),
),
),
),
);
// Tap, enter text.
await tester.tap(find.byWidget(testNameField));
await tester.showKeyboard(find.byWidget(testNameField));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
final EditableTextState state =
tester.state<EditableTextState>(find.byWidget(testNameField));
expect(state.wantKeepAlive, true);
tester.testTextInput.log.clear();
tester.testTextInput.closeConnection();
// Widget does not have focus anymore.
expect(state.wantKeepAlive, false);
// No method calls are sent from the framework.
// This makes sure hide/clearClient methods are not called after connection
// closed.
expect(tester.testTextInput.log, isEmpty);
// For the next fields, tap, enter text.
await tester.tap(find.byWidget(testPhoneField));
await tester.showKeyboard(find.byWidget(testPhoneField));
controller.text = '650123123';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('650123123'));
// Widget regained the focus.
expect(state.wantKeepAlive, true);
});
/// Toolbar is not used in Flutter Web. Skip this check.
///
/// Web is using native dom elements (it is also used as platform input)
......
......@@ -55,6 +55,12 @@ class TestTextInput {
_isRegistered = false;
}
/// Log for method calls.
///
/// For all registered channels, handled calls are added to the list. Can
/// be cleaned using [clearLog].
final List<MethodCall> log = <MethodCall>[];
/// Whether this [TestTextInput] is registered with [SystemChannels.textInput].
///
/// Use [register] and [unregister] methods to control this value.
......@@ -78,6 +84,7 @@ class TestTextInput {
Map<String, dynamic> editingState;
Future<dynamic> _handleTextInputCall(MethodCall methodCall) async {
log.add(methodCall);
switch (methodCall.method) {
case 'TextInput.setClient':
_client = methodCall.arguments[0];
......@@ -123,6 +130,28 @@ class TestTextInput {
);
}
/// Simulates the user closing the text input connection.
///
/// For example:
/// - User pressed the home button and sent the application to background.
/// - User closed the virtual keyboard.
void closeConnection() {
// Not using the `expect` function because in the case of a FlutterDriver
// test this code does not run in a package:test test zone.
if (_client == 0)
throw TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.');
_binaryMessenger.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
MethodCall(
'TextInputClient.onConnectionClosed',
<dynamic>[_client,]
),
),
(ByteData data) { /* response from framework is discarded */ },
);
}
/// Simulates the user typing the given text.
void enterText(String text) {
updateEditingValue(TextEditingValue(
......
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