mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Enable platform view keyboard input on Android Q (flutter/engine#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.
This commit is contained in:
parent
12694594ae
commit
eef9a08469
2
DEPS
2
DEPS
@ -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;
|
||||
|
||||
PresentationContext(Context base, WindowManagerHandler windowManagerHandler) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 { }
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user