Skip to content

Commit

Permalink
Reland "Keyboard support for embedded Android views. (flutter#9203) (f…
Browse files Browse the repository at this point in the history
…lutter#9257)

flutter#9203 broke the keyboard_resize integration test(see more details in flutter/flutter#34085 (comment)).

This re-lands @9203 and fixes the issue the integration test uncovered by always allowing to hide the keyboard.

The difference from the original change is 07d2598
  • Loading branch information
amirh authored Jun 10, 2019
1 parent 36b7123 commit 259d334
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
// in a way that Flutter understands.
textInputPlugin = new TextInputPlugin(
this,
this.flutterEngine.getDartExecutor()
this.flutterEngine.getDartExecutor(),
null
);
androidKeyProcessor = new AndroidKeyProcessor(
this.flutterEngine.getKeyEventChannel(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ public class PlatformViewsChannel {
private final MethodChannel channel;
private PlatformViewsHandler handler;

public void invokeViewFocused(int viewId) {
if (channel == null) {
return;
}
channel.invokeMethod("viewFocused", viewId);
}

private final MethodChannel.MethodCallHandler parsingHandler = new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
Expand All @@ -51,6 +58,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
case "setDirection":
setDirection(call, result);
break;
case "clearFocus":
clearFocus(call, result);
break;
default:
result.notImplemented();
}
Expand Down Expand Up @@ -172,6 +182,20 @@ private void setDirection(@NonNull MethodCall call, @NonNull MethodChannel.Resul
);
}
}

private void clearFocus(MethodCall call, MethodChannel.Result result) {
int viewId = call.arguments();
try {
handler.clearFocus(viewId);
result.success(null);
} catch (IllegalStateException exception) {
result.error(
"error",
exception.getMessage(),
null
);
}
}
};

/**
Expand Down Expand Up @@ -241,6 +265,11 @@ void resizePlatformView(
*/
// TODO(mattcarroll): Introduce an annotation for @TextureId
void setDirection(int viewId, int direction);

/**
* Clears the focus from the platform view with a give id if it is currently focused.
*/
void clearFocus(int viewId);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
result.error("error", exception.getMessage(), null);
}
break;
case "TextInput.setPlatformViewClient":
final int id = (int) args;
textInputMethodHandler.setPlatformViewClient(id);
break;
case "TextInput.setEditingState":
try {
final JSONObject editingState = (JSONObject) args;
Expand Down Expand Up @@ -218,6 +222,16 @@ public interface TextInputMethodHandler {
// TODO(mattcarroll): javadoc
void setClient(int textInputClientId, @NonNull Configuration configuration);

/**
* Sets a platform view as the text input client.
*
* Subsequent calls to createInputConnection will be delegated to the platform view until a
* different client is set.
*
* @param id the ID of the platform view to be set as a text input client.
*/
void setPlatformViewClient(int id);

// TODO(mattcarroll): javadoc
void setEditingState(@NonNull TextEditState editingState);

Expand Down
120 changes: 113 additions & 7 deletions shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.view.FlutterView;
import io.flutter.plugin.platform.PlatformViewsController;

/**
* Android implementation of the text input plugin.
Expand All @@ -30,7 +30,8 @@ public class TextInputPlugin {
private final InputMethodManager mImm;
@NonNull
private final TextInputChannel textInputChannel;
private int mClient = 0;
@NonNull
private InputTarget inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
@Nullable
private TextInputChannel.Configuration configuration;
@Nullable
Expand All @@ -39,7 +40,13 @@ public class TextInputPlugin {
@Nullable
private InputConnection lastInputConnection;

public TextInputPlugin(View view, @NonNull DartExecutor dartExecutor) {
private PlatformViewsController platformViewsController;

// When true following calls to createInputConnection will return the cached lastInputConnection if the input
// target is a platform view. See the comments on lockPlatformViewInputConnection for more details.
private boolean isInputConnectionLocked;

public TextInputPlugin(View view, @NonNull DartExecutor dartExecutor, PlatformViewsController platformViewsController) {
mView = view;
mImm = (InputMethodManager) view.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE);
Expand All @@ -61,6 +68,11 @@ public void setClient(int textInputClientId, TextInputChannel.Configuration conf
setTextInputClient(textInputClientId, configuration);
}

@Override
public void setPlatformViewClient(int platformViewId) {
setPlatformViewTextInputClient(platformViewId);
}

@Override
public void setEditingState(TextInputChannel.TextEditState editingState) {
setTextInputEditingState(mView, editingState);
Expand All @@ -71,13 +83,49 @@ public void clearClient() {
clearTextInputClient();
}
});
this.platformViewsController = platformViewsController;
platformViewsController.attachTextInputPlugin(this);
}

@NonNull
public InputMethodManager getInputMethodManager() {
return mImm;
}

/***
* Use the current platform view input connection until unlockPlatformViewInputConnection is called.
*
* The current input connection instance is cached and any following call to @{link createInputConnection} returns
* the cached connection until unlockPlatformViewInputConnection is called.
*
* This is a no-op if the current input target isn't a platform view.
*
* This is used to preserve an input connection when moving a platform view from one virtual display to another.
*/
public void lockPlatformViewInputConnection() {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
isInputConnectionLocked = true;
}
}

/**
* Unlocks the input connection.
*
* See also: @{link lockPlatformViewInputConnection}.
*/
public void unlockPlatformViewInputConnection() {
isInputConnectionLocked = false;
}

/**
* Detaches the text input plugin from the platform views controller.
*
* The TextInputPlugin instance should not be used after calling this.
*/
public void destroy() {
platformViewsController.detachTextInputPlugin();
}

private static int inputTypeFromTextInputType(
TextInputChannel.InputType type,
boolean obscureText,
Expand Down Expand Up @@ -128,8 +176,16 @@ private static int inputTypeFromTextInputType(
}

public InputConnection createInputConnection(View view, EditorInfo outAttrs) {
if (mClient == 0) {
if (inputTarget.type == InputTarget.Type.NO_TARGET) {
lastInputConnection = null;
return null;
}

if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
if (isInputConnectionLocked) {
return lastInputConnection;
}
lastInputConnection = platformViewsController.getPlatformViewById(inputTarget.id).onCreateInputConnection(outAttrs);
return lastInputConnection;
}

Expand Down Expand Up @@ -158,7 +214,7 @@ public InputConnection createInputConnection(View view, EditorInfo outAttrs) {

InputConnectionAdaptor connection = new InputConnectionAdaptor(
view,
mClient,
inputTarget.id,
textInputChannel,
mEditable
);
Expand All @@ -180,17 +236,28 @@ private void showTextInput(View view) {
}

private void hideTextInput(View view) {
// Note: a race condition may lead to us hiding the keyboard here just after a platform view has shown it.
// This can only potentially happen when switching focus from a Flutter text field to a platform view's text
// field(by text field here I mean anything that keeps the keyboard open).
// See: https://github.com/flutter/flutter/issues/34169
mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
}

private void setTextInputClient(int client, TextInputChannel.Configuration configuration) {
mClient = client;
inputTarget = new InputTarget(InputTarget.Type.FRAMEWORK_CLIENT, client);
this.configuration = configuration;
mEditable = Editable.Factory.getInstance().newEditable("");

// setTextInputClient will be followed by a call to setTextInputEditingState.
// Do a restartInput at that time.
mRestartInputPending = true;
unlockPlatformViewInputConnection();
}

private void setPlatformViewTextInputClient(int platformViewId) {
inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId);
mImm.restartInput(mView);
mRestartInputPending = false;
}

private void applyStateToSelection(TextInputChannel.TextEditState state) {
Expand Down Expand Up @@ -220,6 +287,45 @@ private void setTextInputEditingState(View view, TextInputChannel.TextEditState
}

private void clearTextInputClient() {
mClient = 0;
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
// Focus changes in the framework tree have no guarantees on the order focus nodes are notified. A node
// that lost focus may be notified before or after a node that gained focus.
// When moving the focus from a Flutter text field to an AndroidView, it is possible that the Flutter text
// field's focus node will be notified that it lost focus after the AndroidView was notified that it gained
// focus. When this happens the text field will send a clearTextInput command which we ignore.
// By doing this we prevent the framework from clearing a platform view input client(the only way to do so
// is to set a new framework text client). I don't see an obvious use case for "clearing" a platform views
// text input client, and it may be error prone as we don't know how the platform view manages the input
// connection and we probably shouldn't interfere.
// If we ever want to allow the framework to clear a platform view text client we should probably consider
// changing the focus manager such that focus nodes that lost focus are notified before focus nodes that
// gained focus as part of the same focus event.
return;
}
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
unlockPlatformViewInputConnection();
}

static private class InputTarget {
enum Type {
NO_TARGET,
// InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter framework.
FRAMEWORK_CLIENT,
// InputConnection is managed by an embedded platform view.
PLATFORM_VIEW
}

public InputTarget(@NonNull Type type, int id) {
this.type = type;
this.id = id;
}

@NonNull
Type type;
// The ID of the input target.
//
// For framework clients this is the framework input connection client ID.
// For platform views this is the platform view's ID.
int id;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
* Facilitates interaction between the accessibility bridge and embedded platform views.
*/
public interface PlatformViewsAccessibilityDelegate {

/**
* Returns the root of the view hierarchy for the platform view with the requested id, or null if there is no
* corresponding view.
Expand Down
Loading

0 comments on commit 259d334

Please sign in to comment.