diff --git a/DEPS b/DEPS index fda68d9893df5..887046120fe2b 100644 --- a/DEPS +++ b/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', diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 71df245f06012..522fafd631b74 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -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", ] diff --git a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java index 1d3587b6594ea..cde4b0c49fffc 100644 --- a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +++ b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java @@ -13,13 +13,24 @@ 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 @@ public SingleViewPresentation( 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 @@ public SingleViewPresentation( OnFocusChangeListener focusChangeListener, boolean startFocused ) { - super(outerContext, display); + super(new ImmContext(outerContext), display); this.accessibilityEventsDelegate = accessibilityEventsDelegate; viewFactory = null; this.state = state; @@ -154,7 +165,10 @@ protected void onCreate(Bundle savedInstanceState) { } 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 @@ private static int atMost(int measureSpec) { } } - /** - * 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; } diff --git a/shell/platform/android/test/README.md b/shell/platform/android/test/README.md index 88f7fc5c59d33..f17c2fd5b42c4 100644 --- a/shell/platform/android/test/README.md +++ b/shell/platform/android/test/README.md @@ -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= -tag=last_updated: + $ cipd set-tag flutter/android/robolectric_bundle --version= -tag=last_updated: 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= -tag=robolectric_version: + $ cipd set-tag flutter/android/robolectric_bundle --version= -tag=robolectric_version: You can run `cipd describe flutter/android/robolectric_bundle --version=` to verify. You should see: diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 036cbd56173b9..ca2a9ab5d24ca 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -8,28 +8,29 @@ 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 { } diff --git a/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java b/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java new file mode 100644 index 0000000000000..cb70e89e68a21 --- /dev/null +++ b/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java @@ -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); + } +}