未验证 提交 dfa94983 编写于 作者: M Michael Klimushyn 提交者: GitHub

Enable platform view keyboard input on Android Q (#12085)

Naively embedded platform views on Android were never able to receive
keyboard input, because they were never focusable. So far we've worked
around the limiation by hooking into InputMethodManager and proxying the
InputConnection from a focused window over to the embeded view.

Android Q changed InputMethodManager to be instanced per display instead
of a singleton. Because of this our proxy hook was never being called,
since it was being set up on a different instance of IMM than was being
used in the virtual display.

Update `SingleViewPresentation` to store the IMM from the focused window
and return it whenever there are any calls to `INPUT_METHOD_SERVICE`.
This hooks our proxy back into place for the embedded view in the
virtual display. This restores the functionality of our workaround from
previous versions.

Unfortunately there's still a lot of noisy error logs from IMM here. It
can tell that the IMM has a different displayId than what it's expecting
from the window.

This also updates the unit tests to support SDK=27. SDK 16 doesn't have
DisplayManager, so there were NPEs attempting to instantiate the class
under test.
上级 bf91a8d6
......@@ -483,7 +483,7 @@ deps = {
'packages': [
{
'package': 'flutter/android/robolectric_bundle',
'version': 'last_updated:2019-08-02T16:01:27-0700'
'version': 'last_updated:2019-09-09T16:47:38-0700'
}
],
'condition': 'download_android_deps',
......
......@@ -416,6 +416,7 @@ action("robolectric_tests") {
"test/io/flutter/embedding/engine/RenderingComponentTest.java",
"test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java",
"test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java",
"test/io/flutter/plugin/platform/SingleViewPresentationTest.java",
"test/io/flutter/util/PreconditionsTest.java",
]
......
......@@ -13,13 +13,24 @@ import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Keep;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.*;
import android.view.Display;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
import java.lang.reflect.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import static android.content.Context.INPUT_METHOD_SERVICE;
import static android.content.Context.WINDOW_SERVICE;
import static android.view.View.OnFocusChangeListener;
......@@ -99,7 +110,7 @@ class SingleViewPresentation extends Presentation {
Object createParams,
OnFocusChangeListener focusChangeListener
) {
super(outerContext, display);
super(new ImmContext(outerContext), display);
this.viewFactory = viewFactory;
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
this.viewId = viewId;
......@@ -128,7 +139,7 @@ class SingleViewPresentation extends Presentation {
OnFocusChangeListener focusChangeListener,
boolean startFocused
) {
super(outerContext, display);
super(new ImmContext(outerContext), display);
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
viewFactory = null;
this.state = state;
......@@ -154,7 +165,10 @@ class SingleViewPresentation extends Presentation {
}
container = new FrameLayout(getContext());
PresentationContext context = new PresentationContext(getContext(), state.windowManagerHandler);
// Our base mContext has already been wrapped with an IMM cache at instantiation time, but
// we want to wrap it again here to also return state.windowManagerHandler.
Context context = new PresentationContext(getContext(), state.windowManagerHandler);
if (state.platformView == null) {
state.platformView = viewFactory.create(context, viewId, createParams);
......@@ -235,14 +249,51 @@ class SingleViewPresentation extends Presentation {
}
}
/**
* Proxies a Context replacing the WindowManager with our custom instance.
*/
static class PresentationContext extends ContextWrapper {
private WindowManager windowManager;
private final WindowManagerHandler windowManagerHandler;
/** Answers calls for {@link InputMethodManager} with an instance cached at creation time. */
// TODO(mklim): This caches the IMM at construction time and won't pick up any changes. In rare
// cases where the FlutterView changes windows this will return an outdated instance. This
// should be fixed to instead defer returning the IMM to something that know's FlutterView's
// true Context.
private static class ImmContext extends ContextWrapper {
private @NonNull
final InputMethodManager inputMethodManager;
ImmContext(Context base) {
this(base, /*inputMethodManager=*/null);
}
private ImmContext(Context base, @Nullable InputMethodManager inputMethodManager) {
super(base);
this.inputMethodManager = inputMethodManager != null ? inputMethodManager : (InputMethodManager) base.getSystemService(INPUT_METHOD_SERVICE);
}
@Override
public Object getSystemService(String name) {
if (INPUT_METHOD_SERVICE.equals(name)) {
return inputMethodManager;
}
return super.getSystemService(name);
}
@Override
public Context createDisplayContext(Display display) {
Context displayContext = super.createDisplayContext(display);
return new ImmContext(displayContext, inputMethodManager);
}
}
PresentationContext(Context base, WindowManagerHandler windowManagerHandler) {
/** Proxies a Context replacing the WindowManager with our custom instance. */
// TODO(mklim): This caches the IMM at construction time and won't pick up any changes. In rare
// cases where the FlutterView changes windows this will return an outdated instance. This
// should be fixed to instead defer returning the IMM to something that know's FlutterView's
// true Context.
private static class PresentationContext extends ContextWrapper {
private @NonNull
final WindowManagerHandler windowManagerHandler;
private @Nullable
WindowManager windowManager;
PresentationContext(Context base, @NonNull WindowManagerHandler windowManagerHandler) {
super(base);
this.windowManagerHandler = windowManagerHandler;
}
......
......@@ -64,13 +64,13 @@ Once you've uploaded the new version, also make sure to tag it with the updated
timestamp and robolectric version (most likely still 3.8, unless you've migrated
all the packages to 4+).
$ cipd set-tag flutter/android/robolectric --version=<new_version_hash> -tag=last_updated:<timestamp>
$ cipd set-tag flutter/android/robolectric_bundle --version=<new_version_hash> -tag=last_updated:<timestamp>
Example of a last-updated timestamp: 2019-07-29T15:27:42-0700
You can generate the same date format with `date +%Y-%m-%dT%T%z`.
$ cipd set-tag flutter/android/robolectric --version=<new_version_hash> -tag=robolectric_version:<robolectric_version>
$ cipd set-tag flutter/android/robolectric_bundle --version=<new_version_hash> -tag=robolectric_version:<robolectric_version>
You can run `cipd describe flutter/android/robolectric_bundle
--version=<new_version_hash>` to verify. You should see:
......
......@@ -8,28 +8,29 @@ import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import io.flutter.embedding.android.FlutterActivityAndFragmentDelegateTest;
import io.flutter.embedding.android.FlutterActivityTest;
import io.flutter.embedding.android.FlutterFragmentTest;
import io.flutter.embedding.engine.FlutterEngineCacheTest;
import io.flutter.embedding.engine.systemchannels.PlatformChannelTest;
import io.flutter.embedding.engine.FlutterJNITest;
import io.flutter.embedding.engine.RenderingComponentTest;
import io.flutter.embedding.engine.renderer.FlutterRendererTest;
import io.flutter.embedding.engine.systemchannels.PlatformChannelTest;
import io.flutter.plugin.platform.SingleViewPresentationTest;
import io.flutter.util.PreconditionsTest;
import io.flutter.embedding.engine.FlutterJNITest;
@RunWith(Suite.class)
@SuiteClasses({
PreconditionsTest.class,
SmokeTest.class,
FlutterActivityTest.class,
FlutterFragmentTest.class,
// FlutterActivityAndFragmentDelegateTest.class, TODO(mklim): Fix and re-enable this
FlutterActivityTest.class,
FlutterEngineCacheTest.class,
FlutterFragmentTest.class,
FlutterJNITest.class,
RenderingComponentTest.class,
FlutterRendererTest.class,
PlatformChannelTest.class
PlatformChannelTest.class,
PreconditionsTest.class,
RenderingComponentTest.class,
SingleViewPresentationTest.class,
SmokeTest.class,
})
/** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */
public class FlutterTestSuite {}
public class FlutterTestSuite { }
package io.flutter.plugin.platform;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.view.Display;
import android.view.inputmethod.InputMethodManager;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowDisplay;
import org.robolectric.shadows.ShadowDisplayManager;
import org.robolectric.shadows.ShadowInputMethodManager;
import static junit.framework.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@Config(manifest = Config.NONE, shadows = {ShadowInputMethodManager.class, ShadowDisplayManager.class, ShadowDisplay.class}, sdk = 27)
@RunWith(RobolectricTestRunner.class)
@TargetApi(27)
public class SingleViewPresentationTest {
@Test
public void returnsOuterContextInputMethodManager() {
// There's a bug in Android Q caused by the IMM being instanced per display.
// https://github.com/flutter/flutter/issues/38375. We need the context returned by
// SingleViewPresentation to be consistent from its instantiation instead of defaulting to
// what the system would have returned at call time.
// It's not possible to set up the exact same conditions as the unit test in the bug here,
// but we can make sure that we're wrapping the Context passed in at instantiation time and
// returning the same InputMethodManager from it. This test passes in a Spy context instance
// that initially returns a mock. Without the bugfix this test falls back to Robolectric's
// system service instead of the spy's and fails.
// Create an SVP under test with a Context that returns a local IMM mock.
Context context = spy(RuntimeEnvironment.application);
InputMethodManager expected = mock(InputMethodManager.class);
when(context.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(expected);
DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
SingleViewPresentation svp = new SingleViewPresentation(context, dm.getDisplay(0), null, null, null, false);
// Get the IMM from the SVP's context.
InputMethodManager actual = (InputMethodManager) svp.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
// This should be the mocked instance from construction, not the IMM from the greater
// Android OS (or Robolectric's shadow, in this case).
assertEquals(expected, actual);
}
@Test
public void returnsOuterContextInputMethodManager_createDisplayContext() {
// The IMM should also persist across display contexts created from the base context.
// Create an SVP under test with a Context that returns a local IMM mock.
Context context = spy(RuntimeEnvironment.application);
InputMethodManager expected = mock(InputMethodManager.class);
when(context.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(expected);
Display display = ((DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE)).getDisplay(0);
SingleViewPresentation svp = new SingleViewPresentation(context, display, null, null, null, false);
// Get the IMM from the SVP's context.
InputMethodManager actual = (InputMethodManager) svp.getContext().createDisplayContext(display).getSystemService(Context.INPUT_METHOD_SERVICE);
// This should be the mocked instance from construction, not the IMM from the greater
// Android OS (or Robolectric's shadow, in this case).
assertEquals(expected, actual);
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册