Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit dfa9498

Browse files
author
Michael Klimushyn
authored
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.
1 parent bf91a8d commit dfa9498

File tree

6 files changed

+151
-25
lines changed

6 files changed

+151
-25
lines changed

DEPS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ deps = {
483483
'packages': [
484484
{
485485
'package': 'flutter/android/robolectric_bundle',
486-
'version': 'last_updated:2019-08-02T16:01:27-0700'
486+
'version': 'last_updated:2019-09-09T16:47:38-0700'
487487
}
488488
],
489489
'condition': 'download_android_deps',

shell/platform/android/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ action("robolectric_tests") {
416416
"test/io/flutter/embedding/engine/RenderingComponentTest.java",
417417
"test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java",
418418
"test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java",
419+
"test/io/flutter/plugin/platform/SingleViewPresentationTest.java",
419420
"test/io/flutter/util/PreconditionsTest.java",
420421
]
421422

shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,24 @@
1313
import android.os.Build;
1414
import android.os.Bundle;
1515
import android.support.annotation.Keep;
16+
import android.support.annotation.NonNull;
17+
import android.support.annotation.Nullable;
1618
import android.util.Log;
17-
import android.view.*;
19+
import android.view.Display;
20+
import android.view.Gravity;
21+
import android.view.View;
22+
import android.view.ViewGroup;
23+
import android.view.WindowManager;
1824
import android.view.accessibility.AccessibilityEvent;
25+
import android.view.inputmethod.InputMethodManager;
1926
import android.widget.FrameLayout;
2027

21-
import java.lang.reflect.*;
28+
import java.lang.reflect.InvocationHandler;
29+
import java.lang.reflect.InvocationTargetException;
30+
import java.lang.reflect.Method;
31+
import java.lang.reflect.Proxy;
2232

33+
import static android.content.Context.INPUT_METHOD_SERVICE;
2334
import static android.content.Context.WINDOW_SERVICE;
2435
import static android.view.View.OnFocusChangeListener;
2536

@@ -99,7 +110,7 @@ public SingleViewPresentation(
99110
Object createParams,
100111
OnFocusChangeListener focusChangeListener
101112
) {
102-
super(outerContext, display);
113+
super(new ImmContext(outerContext), display);
103114
this.viewFactory = viewFactory;
104115
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
105116
this.viewId = viewId;
@@ -128,7 +139,7 @@ public SingleViewPresentation(
128139
OnFocusChangeListener focusChangeListener,
129140
boolean startFocused
130141
) {
131-
super(outerContext, display);
142+
super(new ImmContext(outerContext), display);
132143
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
133144
viewFactory = null;
134145
this.state = state;
@@ -154,7 +165,10 @@ protected void onCreate(Bundle savedInstanceState) {
154165
}
155166

156167
container = new FrameLayout(getContext());
157-
PresentationContext context = new PresentationContext(getContext(), state.windowManagerHandler);
168+
169+
// Our base mContext has already been wrapped with an IMM cache at instantiation time, but
170+
// we want to wrap it again here to also return state.windowManagerHandler.
171+
Context context = new PresentationContext(getContext(), state.windowManagerHandler);
158172

159173
if (state.platformView == null) {
160174
state.platformView = viewFactory.create(context, viewId, createParams);
@@ -235,14 +249,51 @@ private static int atMost(int measureSpec) {
235249
}
236250
}
237251

238-
/**
239-
* Proxies a Context replacing the WindowManager with our custom instance.
240-
*/
241-
static class PresentationContext extends ContextWrapper {
242-
private WindowManager windowManager;
243-
private final WindowManagerHandler windowManagerHandler;
252+
/** Answers calls for {@link InputMethodManager} with an instance cached at creation time. */
253+
// TODO(mklim): This caches the IMM at construction time and won't pick up any changes. In rare
254+
// cases where the FlutterView changes windows this will return an outdated instance. This
255+
// should be fixed to instead defer returning the IMM to something that know's FlutterView's
256+
// true Context.
257+
private static class ImmContext extends ContextWrapper {
258+
private @NonNull
259+
final InputMethodManager inputMethodManager;
260+
261+
ImmContext(Context base) {
262+
this(base, /*inputMethodManager=*/null);
263+
}
264+
265+
private ImmContext(Context base, @Nullable InputMethodManager inputMethodManager) {
266+
super(base);
267+
this.inputMethodManager = inputMethodManager != null ? inputMethodManager : (InputMethodManager) base.getSystemService(INPUT_METHOD_SERVICE);
268+
}
269+
270+
@Override
271+
public Object getSystemService(String name) {
272+
if (INPUT_METHOD_SERVICE.equals(name)) {
273+
return inputMethodManager;
274+
}
275+
return super.getSystemService(name);
276+
}
277+
278+
@Override
279+
public Context createDisplayContext(Display display) {
280+
Context displayContext = super.createDisplayContext(display);
281+
return new ImmContext(displayContext, inputMethodManager);
282+
}
283+
}
244284

245-
PresentationContext(Context base, WindowManagerHandler windowManagerHandler) {
285+
/** Proxies a Context replacing the WindowManager with our custom instance. */
286+
// TODO(mklim): This caches the IMM at construction time and won't pick up any changes. In rare
287+
// cases where the FlutterView changes windows this will return an outdated instance. This
288+
// should be fixed to instead defer returning the IMM to something that know's FlutterView's
289+
// true Context.
290+
private static class PresentationContext extends ContextWrapper {
291+
private @NonNull
292+
final WindowManagerHandler windowManagerHandler;
293+
private @Nullable
294+
WindowManager windowManager;
295+
296+
PresentationContext(Context base, @NonNull WindowManagerHandler windowManagerHandler) {
246297
super(base);
247298
this.windowManagerHandler = windowManagerHandler;
248299
}

shell/platform/android/test/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,13 @@ Once you've uploaded the new version, also make sure to tag it with the updated
6464
timestamp and robolectric version (most likely still 3.8, unless you've migrated
6565
all the packages to 4+).
6666

67-
$ cipd set-tag flutter/android/robolectric --version=<new_version_hash> -tag=last_updated:<timestamp>
67+
$ cipd set-tag flutter/android/robolectric_bundle --version=<new_version_hash> -tag=last_updated:<timestamp>
6868

6969
Example of a last-updated timestamp: 2019-07-29T15:27:42-0700
7070

7171
You can generate the same date format with `date +%Y-%m-%dT%T%z`.
7272

73-
$ cipd set-tag flutter/android/robolectric --version=<new_version_hash> -tag=robolectric_version:<robolectric_version>
73+
$ cipd set-tag flutter/android/robolectric_bundle --version=<new_version_hash> -tag=robolectric_version:<robolectric_version>
7474

7575
You can run `cipd describe flutter/android/robolectric_bundle
7676
--version=<new_version_hash>` to verify. You should see:

shell/platform/android/test/io/flutter/FlutterTestSuite.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,29 @@
88
import org.junit.runners.Suite;
99
import org.junit.runners.Suite.SuiteClasses;
1010

11-
import io.flutter.embedding.android.FlutterActivityAndFragmentDelegateTest;
1211
import io.flutter.embedding.android.FlutterActivityTest;
1312
import io.flutter.embedding.android.FlutterFragmentTest;
1413
import io.flutter.embedding.engine.FlutterEngineCacheTest;
15-
import io.flutter.embedding.engine.systemchannels.PlatformChannelTest;
14+
import io.flutter.embedding.engine.FlutterJNITest;
1615
import io.flutter.embedding.engine.RenderingComponentTest;
1716
import io.flutter.embedding.engine.renderer.FlutterRendererTest;
17+
import io.flutter.embedding.engine.systemchannels.PlatformChannelTest;
18+
import io.flutter.plugin.platform.SingleViewPresentationTest;
1819
import io.flutter.util.PreconditionsTest;
19-
import io.flutter.embedding.engine.FlutterJNITest;
2020

2121
@RunWith(Suite.class)
2222
@SuiteClasses({
23-
PreconditionsTest.class,
24-
SmokeTest.class,
25-
FlutterActivityTest.class,
26-
FlutterFragmentTest.class,
2723
// FlutterActivityAndFragmentDelegateTest.class, TODO(mklim): Fix and re-enable this
24+
FlutterActivityTest.class,
2825
FlutterEngineCacheTest.class,
26+
FlutterFragmentTest.class,
2927
FlutterJNITest.class,
30-
RenderingComponentTest.class,
3128
FlutterRendererTest.class,
32-
PlatformChannelTest.class
29+
PlatformChannelTest.class,
30+
PreconditionsTest.class,
31+
RenderingComponentTest.class,
32+
SingleViewPresentationTest.class,
33+
SmokeTest.class,
3334
})
3435
/** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */
35-
public class FlutterTestSuite {}
36+
public class FlutterTestSuite { }
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.flutter.plugin.platform;
2+
3+
import android.annotation.TargetApi;
4+
import android.content.Context;
5+
import android.hardware.display.DisplayManager;
6+
import android.view.Display;
7+
import android.view.inputmethod.InputMethodManager;
8+
9+
import org.junit.Test;
10+
import org.junit.runner.RunWith;
11+
import org.robolectric.RobolectricTestRunner;
12+
import org.robolectric.RuntimeEnvironment;
13+
import org.robolectric.annotation.Config;
14+
import org.robolectric.shadows.ShadowDisplay;
15+
import org.robolectric.shadows.ShadowDisplayManager;
16+
import org.robolectric.shadows.ShadowInputMethodManager;
17+
18+
import static junit.framework.Assert.assertEquals;
19+
import static org.mockito.Mockito.mock;
20+
import static org.mockito.Mockito.spy;
21+
import static org.mockito.Mockito.when;
22+
23+
@Config(manifest = Config.NONE, shadows = {ShadowInputMethodManager.class, ShadowDisplayManager.class, ShadowDisplay.class}, sdk = 27)
24+
@RunWith(RobolectricTestRunner.class)
25+
@TargetApi(27)
26+
public class SingleViewPresentationTest {
27+
@Test
28+
public void returnsOuterContextInputMethodManager() {
29+
// There's a bug in Android Q caused by the IMM being instanced per display.
30+
// https://github.com/flutter/flutter/issues/38375. We need the context returned by
31+
// SingleViewPresentation to be consistent from its instantiation instead of defaulting to
32+
// what the system would have returned at call time.
33+
34+
// It's not possible to set up the exact same conditions as the unit test in the bug here,
35+
// but we can make sure that we're wrapping the Context passed in at instantiation time and
36+
// returning the same InputMethodManager from it. This test passes in a Spy context instance
37+
// that initially returns a mock. Without the bugfix this test falls back to Robolectric's
38+
// system service instead of the spy's and fails.
39+
40+
// Create an SVP under test with a Context that returns a local IMM mock.
41+
Context context = spy(RuntimeEnvironment.application);
42+
InputMethodManager expected = mock(InputMethodManager.class);
43+
when(context.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(expected);
44+
DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
45+
SingleViewPresentation svp = new SingleViewPresentation(context, dm.getDisplay(0), null, null, null, false);
46+
47+
// Get the IMM from the SVP's context.
48+
InputMethodManager actual = (InputMethodManager) svp.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
49+
50+
// This should be the mocked instance from construction, not the IMM from the greater
51+
// Android OS (or Robolectric's shadow, in this case).
52+
assertEquals(expected, actual);
53+
}
54+
55+
@Test
56+
public void returnsOuterContextInputMethodManager_createDisplayContext() {
57+
// The IMM should also persist across display contexts created from the base context.
58+
59+
// Create an SVP under test with a Context that returns a local IMM mock.
60+
Context context = spy(RuntimeEnvironment.application);
61+
InputMethodManager expected = mock(InputMethodManager.class);
62+
when(context.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(expected);
63+
Display display = ((DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE)).getDisplay(0);
64+
SingleViewPresentation svp = new SingleViewPresentation(context, display, null, null, null, false);
65+
66+
// Get the IMM from the SVP's context.
67+
InputMethodManager actual = (InputMethodManager) svp.getContext().createDisplayContext(display).getSystemService(Context.INPUT_METHOD_SERVICE);
68+
69+
// This should be the mocked instance from construction, not the IMM from the greater
70+
// Android OS (or Robolectric's shadow, in this case).
71+
assertEquals(expected, actual);
72+
}
73+
}

0 commit comments

Comments
 (0)