diff --git a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java index b7ca559ac7c64..d8dc53049a5be 100644 --- a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +++ b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java @@ -22,17 +22,18 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.view.WindowMetrics; import android.view.accessibility.AccessibilityEvent; import android.view.inputmethod.InputMethodManager; import android.widget.FrameLayout; import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; import io.flutter.Log; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /* * A presentation used for hosting a single Android view in a virtual display. @@ -359,7 +360,7 @@ public Object getSystemService(String name) { private WindowManager getWindowManager() { if (windowManager == null) { - windowManager = windowManagerHandler.getWindowManager(); + windowManager = windowManagerHandler; } return windowManager; } @@ -377,21 +378,18 @@ private boolean isCalledFromAlertDialog() { } /* - * A dynamic proxy handler for a WindowManager with custom overrides. + * A static proxy handler for a WindowManager with custom overrides. * * The presentation's window manager delegates all calls to the default window manager. * WindowManager#addView calls triggered by views that are attached to the virtual display are crashing * (see: https://github.com/flutter/flutter/issues/20714). This was triggered when selecting text in an embedded * WebView (as the selection handles are implemented as popup windows). * - * This dynamic proxy overrides the addView, removeView, removeViewImmediate, and updateViewLayout methods - * to prevent these crashes. - * - * This will be more efficient as a static proxy that's not using reflection, but as the engine is currently - * not being built against the latest Android SDK we cannot override all relevant method. - * Tracking issue for upgrading the engine's Android sdk: https://github.com/flutter/flutter/issues/20717 + * This static proxy overrides the addView, removeView, removeViewImmediate, and updateViewLayout methods + * to prevent these crashes, and forwards all other calls to the delegate. */ - static class WindowManagerHandler implements InvocationHandler { + @VisibleForTesting + static class WindowManagerHandler implements WindowManager { private static final String TAG = "PlatformViewsController"; private final WindowManager delegate; @@ -402,72 +400,86 @@ static class WindowManagerHandler implements InvocationHandler { fakeWindowRootView = fakeWindowViewGroup; } - public WindowManager getWindowManager() { - return (WindowManager) - Proxy.newProxyInstance( - WindowManager.class.getClassLoader(), new Class[] {WindowManager.class}, this); + @Override + @Deprecated + public Display getDefaultDisplay() { + return delegate.getDefaultDisplay(); } @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - switch (method.getName()) { - case "addView": - addView(args); - return null; - case "removeView": - removeView(args); - return null; - case "removeViewImmediate": - removeViewImmediate(args); - return null; - case "updateViewLayout": - updateViewLayout(args); - return null; - } - try { - return method.invoke(delegate, args); - } catch (InvocationTargetException e) { - throw e.getCause(); + public void removeViewImmediate(View view) { + if (fakeWindowRootView == null) { + Log.w(TAG, "Embedded view called removeViewImmediate while detached from presentation"); + return; } + view.clearAnimation(); + fakeWindowRootView.removeView(view); } - private void addView(Object[] args) { + @Override + public void addView(View view, ViewGroup.LayoutParams params) { if (fakeWindowRootView == null) { Log.w(TAG, "Embedded view called addView while detached from presentation"); return; } - View view = (View) args[0]; - WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1]; - fakeWindowRootView.addView(view, layoutParams); + fakeWindowRootView.addView(view, params); } - private void removeView(Object[] args) { + @Override + public void updateViewLayout(View view, ViewGroup.LayoutParams params) { if (fakeWindowRootView == null) { - Log.w(TAG, "Embedded view called removeView while detached from presentation"); + Log.w(TAG, "Embedded view called updateViewLayout while detached from presentation"); return; } - View view = (View) args[0]; - fakeWindowRootView.removeView(view); + fakeWindowRootView.updateViewLayout(view, params); } - private void removeViewImmediate(Object[] args) { + @Override + public void removeView(View view) { if (fakeWindowRootView == null) { - Log.w(TAG, "Embedded view called removeViewImmediate while detached from presentation"); + Log.w(TAG, "Embedded view called removeView while detached from presentation"); return; } - View view = (View) args[0]; - view.clearAnimation(); fakeWindowRootView.removeView(view); } - private void updateViewLayout(Object[] args) { - if (fakeWindowRootView == null) { - Log.w(TAG, "Embedded view called updateViewLayout while detached from presentation"); - return; - } - View view = (View) args[0]; - WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1]; - fakeWindowRootView.updateViewLayout(view, layoutParams); + @RequiresApi(api = Build.VERSION_CODES.R) + @NonNull + @Override + public WindowMetrics getCurrentWindowMetrics() { + return delegate.getCurrentWindowMetrics(); + } + + @RequiresApi(api = Build.VERSION_CODES.R) + @NonNull + @Override + public WindowMetrics getMaximumWindowMetrics() { + return delegate.getMaximumWindowMetrics(); + } + + @RequiresApi(api = Build.VERSION_CODES.S) + @Override + public boolean isCrossWindowBlurEnabled() { + return delegate.isCrossWindowBlurEnabled(); + } + + @RequiresApi(api = Build.VERSION_CODES.S) + @Override + public void addCrossWindowBlurEnabledListener(@NonNull Consumer listener) { + delegate.addCrossWindowBlurEnabledListener(listener); + } + + @RequiresApi(api = Build.VERSION_CODES.S) + @Override + public void addCrossWindowBlurEnabledListener( + @NonNull Executor executor, @NonNull Consumer listener) { + delegate.addCrossWindowBlurEnabledListener(executor, listener); + } + + @RequiresApi(api = Build.VERSION_CODES.S) + @Override + public void removeCrossWindowBlurEnabledListener(@NonNull Consumer listener) { + delegate.removeCrossWindowBlurEnabledListener(listener); } } diff --git a/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java b/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java index d27e08bbbdc97..d99d344568d25 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java @@ -7,18 +7,26 @@ import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.S; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import android.annotation.TargetApi; import android.content.Context; import android.hardware.display.DisplayManager; import android.view.Display; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.Executor; +import java.util.function.Consumer; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; @@ -83,4 +91,112 @@ public void returnsOuterContextInputMethodManager_createDisplayContext() { // Android OS (or Robolectric's shadow, in this case). assertEquals(expected, actual); } + + @Test + @Config(minSdk = R) + public void windowManagerHandler_passesCorrectlyToFakeWindowViewGroup() { + // Mock the WindowManager and FakeWindowViewGroup that get used by the WindowManagerHandler. + WindowManager mockWindowManager = mock(WindowManager.class); + SingleViewPresentation.FakeWindowViewGroup mockFakeWindowViewGroup = + mock(SingleViewPresentation.FakeWindowViewGroup.class); + + View mockView = mock(View.class); + ViewGroup.LayoutParams mockLayoutParams = mock(ViewGroup.LayoutParams.class); + + SingleViewPresentation.WindowManagerHandler windowManagerHandler = + new SingleViewPresentation.WindowManagerHandler(mockWindowManager, mockFakeWindowViewGroup); + + // removeViewImmediate + windowManagerHandler.removeViewImmediate(mockView); + verify(mockView).clearAnimation(); + verify(mockFakeWindowViewGroup).removeView(mockView); + verifyNoInteractions(mockWindowManager); + + // addView + windowManagerHandler.addView(mockView, mockLayoutParams); + verify(mockFakeWindowViewGroup).addView(mockView, mockLayoutParams); + verifyNoInteractions(mockWindowManager); + + // updateViewLayout + windowManagerHandler.updateViewLayout(mockView, mockLayoutParams); + verify(mockFakeWindowViewGroup).updateViewLayout(mockView, mockLayoutParams); + verifyNoInteractions(mockWindowManager); + + // removeView + windowManagerHandler.updateViewLayout(mockView, mockLayoutParams); + verify(mockFakeWindowViewGroup).removeView(mockView); + verifyNoInteractions(mockWindowManager); + } + + @Test + @Config(minSdk = R) + public void windowManagerHandler_logAndReturnEarly_whenFakeWindowViewGroupIsNull() { + // Mock the WindowManager and FakeWindowViewGroup that get used by the WindowManagerHandler. + WindowManager mockWindowManager = mock(WindowManager.class); + + View mockView = mock(View.class); + ViewGroup.LayoutParams mockLayoutParams = mock(ViewGroup.LayoutParams.class); + + SingleViewPresentation.WindowManagerHandler windowManagerHandler = + new SingleViewPresentation.WindowManagerHandler(mockWindowManager, null); + + // removeViewImmediate + windowManagerHandler.removeViewImmediate(mockView); + verifyNoInteractions(mockView); + verifyNoInteractions(mockWindowManager); + + // addView + windowManagerHandler.addView(mockView, mockLayoutParams); + verifyNoInteractions(mockWindowManager); + + // updateViewLayout + windowManagerHandler.updateViewLayout(mockView, mockLayoutParams); + verifyNoInteractions(mockWindowManager); + + // removeView + windowManagerHandler.updateViewLayout(mockView, mockLayoutParams); + verifyNoInteractions(mockWindowManager); + } + + // This section tests that WindowManagerHandler forwards all of the non-special case calls to the + // delegate WindowManager. Because this must include some deprecated WindowManager method calls + // (because the proxy overrides every method), we suppress deprecation warnings here. + @Test + @Config(minSdk = S) + @SuppressWarnings("deprecation") + public void windowManagerHandler_forwardsAllOtherCallsToDelegate() { + // Mock the WindowManager and FakeWindowViewGroup that get used by the WindowManagerHandler. + WindowManager mockWindowManager = mock(WindowManager.class); + SingleViewPresentation.FakeWindowViewGroup mockFakeWindowViewGroup = + mock(SingleViewPresentation.FakeWindowViewGroup.class); + + SingleViewPresentation.WindowManagerHandler windowManagerHandler = + new SingleViewPresentation.WindowManagerHandler(mockWindowManager, mockFakeWindowViewGroup); + + // Verify that all other calls get forwarded to the delegate. + Executor mockExecutor = mock(Executor.class); + @SuppressWarnings("Unchecked cast") + Consumer mockListener = (Consumer) mock(Consumer.class); + + windowManagerHandler.getDefaultDisplay(); + verify(mockWindowManager).getDefaultDisplay(); + + windowManagerHandler.getCurrentWindowMetrics(); + verify(mockWindowManager).getCurrentWindowMetrics(); + + windowManagerHandler.getMaximumWindowMetrics(); + verify(mockWindowManager).getMaximumWindowMetrics(); + + windowManagerHandler.isCrossWindowBlurEnabled(); + verify(mockWindowManager).isCrossWindowBlurEnabled(); + + windowManagerHandler.addCrossWindowBlurEnabledListener(mockListener); + verify(mockWindowManager).addCrossWindowBlurEnabledListener(mockListener); + + windowManagerHandler.addCrossWindowBlurEnabledListener(mockExecutor, mockListener); + verify(mockWindowManager).addCrossWindowBlurEnabledListener(mockExecutor, mockListener); + + windowManagerHandler.removeCrossWindowBlurEnabledListener(mockListener); + verify(mockWindowManager).removeCrossWindowBlurEnabledListener(mockListener); + } }