Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions shell/platform/android/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ action("robolectric_tests") {
"test/io/flutter/embedding/android/FlutterActivityTest.java",
"test/io/flutter/embedding/android/FlutterFragmentTest.java",
"test/io/flutter/embedding/engine/FlutterEngineCacheTest.java",
"test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java",
"test/io/flutter/util/PreconditionsTest.java",
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.json.JSONObject;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;

import io.flutter.Log;
Expand Down Expand Up @@ -222,6 +223,17 @@ public void unspecifiedAction(int inputClientId) {
);
}

/**
* Instructs Flutter to clear the current input client, which ends the text
* input interaction with the given input control.
*/
public void onConnectionClosed(int inputClientId) {
channel.invokeMethod(
"TextInputClient.onConnectionClosed",
Collections.singletonList(inputClientId)
);
}

/**
* Sets the {@link TextInputMethodHandler} which receives all events and requests
* that are parsed from the underlying platform channel.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
package io.flutter.plugin.editing;

import android.content.Context;
import android.support.annotation.NonNull;
import android.text.DynamicLayout;
import android.text.Editable;
import android.text.Layout;
import android.text.Layout.Directions;
import android.text.Selection;
import android.text.TextPaint;
import android.view.KeyEvent;
Expand All @@ -19,37 +19,51 @@

import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.Log;
import io.flutter.plugin.common.ErrorLogResult;
import io.flutter.plugin.common.MethodChannel;

class InputConnectionAdaptor extends BaseInputConnection {
@NonNull
private final View mFlutterView;
private final int mClient;
@NonNull
private final TextInputChannel textInputChannel;
@NonNull
private final Editable mEditable;
@NonNull
private final Runnable onConnectionClosed;
private int mBatchCount;
@NonNull
private InputMethodManager mImm;
@NonNull
private final Layout mLayout;

@SuppressWarnings("deprecation")
public InputConnectionAdaptor(
View view,
@NonNull View view,
int client,
TextInputChannel textInputChannel,
Editable editable
@NonNull TextInputChannel textInputChannel,
@NonNull Editable editable,
@NonNull Runnable onConnectionClosed
) {
super(view, true);
mFlutterView = view;
mClient = client;
this.textInputChannel = textInputChannel;
mEditable = editable;
this.onConnectionClosed = onConnectionClosed;
mBatchCount = 0;
// We create a dummy Layout with max width so that the selection
// shifting acts as if all text were in one line.
mLayout = new DynamicLayout(mEditable, new TextPaint(), Integer.MAX_VALUE, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
}

@Override
public void closeConnection() {
super.closeConnection();
textInputChannel.onConnectionClosed(mClient);
onConnectionClosed.run();
}

// Send the current state of the editable to Flutter.
private void updateEditingState() {
// If the IME is in the middle of a batch edit, then wait until it completes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ public class TextInputPlugin {
// target is a platform view. See the comments on lockPlatformViewInputConnection for more details.
private boolean isInputConnectionLocked;

private final Runnable onInputConnectionClosed = new Runnable() {
@Override
public void run() {
clearTextInputClient();
}
};

public TextInputPlugin(View view, @NonNull DartExecutor dartExecutor, @NonNull PlatformViewsController platformViewsController) {
mView = view;
mImm = (InputMethodManager) view.getContext().getSystemService(
Expand Down Expand Up @@ -219,7 +226,8 @@ public InputConnection createInputConnection(View view, EditorInfo outAttrs) {
view,
inputTarget.id,
textInputChannel,
mEditable
mEditable,
onInputConnectionClosed
);
outAttrs.initialSelStart = Selection.getSelectionStart(mEditable);
outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable);
Expand Down
4 changes: 3 additions & 1 deletion shell/platform/android/test/io/flutter/FlutterTestSuite.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io.flutter.embedding.android.FlutterActivityTest;
import io.flutter.embedding.android.FlutterFragmentTest;
import io.flutter.embedding.engine.FlutterEngineCacheTest;
import io.flutter.embedding.engine.systemchannels.TextInputChannelTest;
import io.flutter.util.PreconditionsTest;

@RunWith(Suite.class)
Expand All @@ -21,7 +22,8 @@
FlutterActivityTest.class,
FlutterFragmentTest.class,
// FlutterActivityAndFragmentDelegateTest.class, TODO(mklim): Fix and re-enable this
FlutterEngineCacheTest.class
FlutterEngineCacheTest.class,
TextInputChannelTest.class
})
/** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */
public class FlutterTestSuite {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.flutter.embedding.engine.systemchannels;

import org.hamcrest.Description;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import java.nio.ByteBuffer;
import java.util.Collections;

import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.plugin.common.JSONMethodCodec;
import io.flutter.plugin.common.MethodCall;

import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@Config(manifest=Config.NONE)
@RunWith(RobolectricTestRunner.class)
public class TextInputChannelTest {
@Test
public void itNotifiesFrameworkWhenPlatformClosesInputConnection() {
// Setup test.
final int INPUT_CLIENT_ID = 9; // Arbitrary integer.
DartExecutor dartExecutor = mock(DartExecutor.class);

TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);

// Execute behavior under test.
textInputChannel.onConnectionClosed(INPUT_CLIENT_ID);

// Verify results.
verify(dartExecutor, times(1)).send(
eq("flutter/textinput"),
ByteBufferMatcher.eqByteBuffer(JSONMethodCodec.INSTANCE.encodeMethodCall(
new MethodCall(
"TextInputClient.onConnectionClosed",
Collections.singletonList(INPUT_CLIENT_ID)
)
)),
eq(null)
);
}

/**
* Mockito matcher that compares two {@link ByteBuffer}s by resetting both buffers and then
* utilizing their standard {@code equals()} method.
* <p>
* This matcher will change the state of the expected and actual buffers. The exact change in
* state depends on where the comparison fails or succeeds.
*/
static class ByteBufferMatcher extends ArgumentMatcher<ByteBuffer> {

static ByteBuffer eqByteBuffer(ByteBuffer expected) {
return argThat(new ByteBufferMatcher(expected));
}

private ByteBuffer expected;

ByteBufferMatcher(ByteBuffer expected) {
this.expected = expected;
}

@Override
public boolean matches(Object argument) {
if (!(argument instanceof ByteBuffer)) {
return false;
}

// Reset the buffers for content comparison.
((ByteBuffer) argument).position(0);
expected.position(0);

return expected.equals(argument);
}

// Implemented so that during a failure the expected value is
// shown in logs, rather than the name of this class.
@Override
public void describeTo(Description description) {
description.appendText(expected.toString());
}
}
}