Skip to content

Conversation

@dnfield
Copy link
Contributor

@dnfield dnfield commented Nov 1, 2019

Description

Companion to flutter/engine#13474

When the new Android embedding detaches from the engine (e.g. because it goes to the background), it loses state related to text input client(s).

It's being updated to send this message to the framework so that the framework can reattach the text input client, and the keyboard can continue to work.

Related Issues

This and the related engine PR will combine to resolve #35054

Tests

I need to figure out how to test this. Testing will require some refactoring, I'm thinking at the very lease making _TextInputClientHandler @visibleForTesting, or figure out a way to have the test binding simulate a message coming from the engine for this. For now I just want it up so reviewers of the engine side can see.

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require Flutter developers to manually update their apps to accommodate your change?

  • Yes, this is a breaking change (Please read Handling breaking changes). Replace this with a link to the e-mail where you asked for input on this proposed change.
  • No, this is not a breaking change.

@fluttergithubbot fluttergithubbot added framework flutter/packages/flutter repository. See also f: labels. work in progress; do not review labels Nov 1, 2019
@dnfield dnfield marked this pull request as ready for review November 1, 2019 08:33
@dnfield
Copy link
Contributor Author

dnfield commented Nov 1, 2019

This test is .. very ugly. But it's a test.

final _TextInputClientHandler _clientHandler = _TextInputClientHandler();
final TextInputClientHandler _clientHandler = TextInputClientHandler(
SystemChannels.textInput,
null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this an optional parameter?

/// should call [TextInputConnection.close] on the returned
/// [TextInputConnection].
static TextInputConnection attach(TextInputClient client, TextInputConfiguration configuration) {
static TextInputConnection attach(TextInputClient client) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change, no?

return connection;
}

static void _attach(TextInputClientHandler handler, TextInputConnection connection) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should probably be some documentation on what the difference between attach and _attach is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

///
/// This configuration is requested when establishing or re-establishing the
/// connection between the [TextInput] and the engine.
TextInputConfiguration createConfiguration();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also looks like a breaking change? Implementors/Subclasses of this class will have to implement a new method?

class TextInputClientHandler {
/// Creates a [TextInputClientHandler].
@visibleForTesting
TextInputClientHandler(this._channel, this._currentConnection) : assert(_channel != null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The arguments are only there for testing, right? I wonder if we should default them to what we need them in production.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm refactoring the test.

final String method = methodCall.method;

// Reattach request needs to be handled regardless of the client ID.
if (method == 'TextInputClient.reattach') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here as the engine side PR. Can we call this something like requestExistingInputState or requestInputConfiguration so that we can avoid implying a previous attachment and express the goal of the message?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some other notes:

  • Do Android and Framework agree on the state of the text field after re-attach, i.e. with regards to the text that's present in the text field?
  • Can you also double check that this doesn't break textfields that switch the keyboard (as we have seen with the previous attempt)?
  • We need separate API for this and #43466? Or can we consolidate? (Maybe at least only do one breaking change announcement for both)

final String method = methodCall.method;

// Reattach request needs to be handled regardless of the client ID.
if (method == 'TextInputClient.reattach') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add the new message to the documentation in system_channels.dart

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor

@matthew-carroll matthew-carroll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll defer to @goderbauer for the LGTM on this because I'm less familiar with the Dart side of input connections. But nothing jumps out at me.

/// second argument is a [String] consisting of the stringification of one
/// of the values of the [TextInputAction] enum.
///
/// * `TextInputClient.requestExistingInputState`: The embedding may have
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's document what should happen if there's nothing to send.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (method == 'TextInputClient.requestExistingInputState') {
assert(_currentConnection._client != null);
_attach(_currentConnection);
if (_lastTextEditingValue != null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it seem odd that we cache the lastTextEditingValue, but don't cache the configuration?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just caching them all now. I think this is probably cleaner and it's no longer breaking.

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe revert this and clean it up in a separate PR, potentially by adding something to gitignore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to file a separate PR, this was unintentional. Cleaned it up for now here.

@dnfield dnfield force-pushed the reattach_text_input branch from 9686425 to ed02d70 Compare November 1, 2019 22:11
@dnfield dnfield force-pushed the reattach_text_input branch from ed02d70 to 629c3d9 Compare November 1, 2019 22:12
@dnfield dnfield requested a review from goderbauer November 1, 2019 23:29
Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

I like that all the channel calls are now encapsulated in one object and not all over the place.

// Reattach request needs to be handled regardless of the client ID.
if (method == 'TextInputClient.requestExistingInputState') {
assert(_currentConnection._client != null);
_attach(_currentConnection, _currentConfiguration);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering here if _currentConfiguration could ever be null at this point. Maybe add an assert(_currentConfiguration != null) above this one, possibly with a comment saying that we always have a configuration if we have a connection?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have an early return at line 926 if _currentConfiguration == null.

return;
final String method = methodCall.method;

// Reattach request needs to be handled regardless of the client ID.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update this comment? Since the method is no longer called "reattach"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


setUp(() {
fakeTextChannel = FakeTextChannel((MethodCall call) async {
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do you actually need the return? Just an empty body is not enough?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that would make the analyzer unhappy, I was wrong. Done


test('text input client handler responds to reattach with setClient', () async {
TextInput.attach(client, client.configuration);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe expect here that there has been one method call to ensure that requestExistingInputState only calls setClient once and not twice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

final TextInputConnection connection = TextInput.attach(client, client.configuration);
const TextEditingValue editingState = TextEditingValue(text: 'foo');
connection.setEditingState(editingState);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


@override
void updateEditingValue(TextEditingValue value) => throw UnimplementedError();
@override
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uber-nit: add an empty line above this one for constancy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

final String expectedString = utf8.decode(expectedData.buffer.asUint8List());

if (outgoingString != expectedString) {
print('''Index $i did not match:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would format a little nicer as:

print(
    'Index $i did not match:\n'
    '  actual: ${outgoingCalls[i]}\n'
    '  expected: ${calls[i]}'
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, done.

@dnfield
Copy link
Contributor Author

dnfield commented Nov 4, 2019

/cc @nturgut for review of merge

Copy link
Contributor

@nturgut nturgut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the merge Dan!

@dnfield dnfield merged commit 7957c56 into flutter:master Nov 5, 2019
@dnfield dnfield deleted the reattach_text_input branch November 5, 2019 00:28
Inconnu08 pushed a commit to Inconnu08/flutter that referenced this pull request Nov 26, 2019
Comment on lines +949 to +951
if (_currentTextEditingValue != null) {
_setEditingState(_currentTextEditingValue);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out these lines never get called because of #47137 - see #47177

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick question: in Android or in general?

In Flutter for Web, this is the main part that we are syncing the state from Framework to Platform. It works for the web engine so far.

Let's debug and try to see why it is not the case for Android.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android is the only platform that would go down this codepath today. It shouldn't be a problem for web.

See the linked PR for more details - I don't think this is a problem for web, but it is a problem for Android where the engine side loses state.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 2, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Add2App] It's possible to lose the ability to input characters using the keyboard on Android

6 participants