Skip to content

Commit a023d98

Browse files
authored
Remove WindowManager reflection in SingleViewPresentation.java (flutter#49996)
Fixes flutter#106449. Changes it to a static proxy, as the comment recommended. This does mean we will have to update it to override new methods as they are added to the interface when updating the version of the Android sdk we use. [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 06801ca commit a023d98

File tree

2 files changed

+184
-56
lines changed

2 files changed

+184
-56
lines changed

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

Lines changed: 68 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,18 @@
2222
import android.view.View;
2323
import android.view.ViewGroup;
2424
import android.view.WindowManager;
25+
import android.view.WindowMetrics;
2526
import android.view.accessibility.AccessibilityEvent;
2627
import android.view.inputmethod.InputMethodManager;
2728
import android.widget.FrameLayout;
2829
import androidx.annotation.Keep;
2930
import androidx.annotation.NonNull;
3031
import androidx.annotation.Nullable;
32+
import androidx.annotation.RequiresApi;
33+
import androidx.annotation.VisibleForTesting;
3134
import io.flutter.Log;
32-
import java.lang.reflect.InvocationHandler;
33-
import java.lang.reflect.InvocationTargetException;
34-
import java.lang.reflect.Method;
35-
import java.lang.reflect.Proxy;
35+
import java.util.concurrent.Executor;
36+
import java.util.function.Consumer;
3637

3738
/*
3839
* A presentation used for hosting a single Android view in a virtual display.
@@ -359,7 +360,7 @@ public Object getSystemService(String name) {
359360

360361
private WindowManager getWindowManager() {
361362
if (windowManager == null) {
362-
windowManager = windowManagerHandler.getWindowManager();
363+
windowManager = windowManagerHandler;
363364
}
364365
return windowManager;
365366
}
@@ -377,21 +378,18 @@ private boolean isCalledFromAlertDialog() {
377378
}
378379

379380
/*
380-
* A dynamic proxy handler for a WindowManager with custom overrides.
381+
* A static proxy handler for a WindowManager with custom overrides.
381382
*
382383
* The presentation's window manager delegates all calls to the default window manager.
383384
* WindowManager#addView calls triggered by views that are attached to the virtual display are crashing
384385
* (see: https://github.com/flutter/flutter/issues/20714). This was triggered when selecting text in an embedded
385386
* WebView (as the selection handles are implemented as popup windows).
386387
*
387-
* This dynamic proxy overrides the addView, removeView, removeViewImmediate, and updateViewLayout methods
388-
* to prevent these crashes.
389-
*
390-
* This will be more efficient as a static proxy that's not using reflection, but as the engine is currently
391-
* not being built against the latest Android SDK we cannot override all relevant method.
392-
* Tracking issue for upgrading the engine's Android sdk: https://github.com/flutter/flutter/issues/20717
388+
* This static proxy overrides the addView, removeView, removeViewImmediate, and updateViewLayout methods
389+
* to prevent these crashes, and forwards all other calls to the delegate.
393390
*/
394-
static class WindowManagerHandler implements InvocationHandler {
391+
@VisibleForTesting
392+
static class WindowManagerHandler implements WindowManager {
395393
private static final String TAG = "PlatformViewsController";
396394

397395
private final WindowManager delegate;
@@ -402,72 +400,86 @@ static class WindowManagerHandler implements InvocationHandler {
402400
fakeWindowRootView = fakeWindowViewGroup;
403401
}
404402

405-
public WindowManager getWindowManager() {
406-
return (WindowManager)
407-
Proxy.newProxyInstance(
408-
WindowManager.class.getClassLoader(), new Class<?>[] {WindowManager.class}, this);
403+
@Override
404+
@Deprecated
405+
public Display getDefaultDisplay() {
406+
return delegate.getDefaultDisplay();
409407
}
410408

411409
@Override
412-
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
413-
switch (method.getName()) {
414-
case "addView":
415-
addView(args);
416-
return null;
417-
case "removeView":
418-
removeView(args);
419-
return null;
420-
case "removeViewImmediate":
421-
removeViewImmediate(args);
422-
return null;
423-
case "updateViewLayout":
424-
updateViewLayout(args);
425-
return null;
426-
}
427-
try {
428-
return method.invoke(delegate, args);
429-
} catch (InvocationTargetException e) {
430-
throw e.getCause();
410+
public void removeViewImmediate(View view) {
411+
if (fakeWindowRootView == null) {
412+
Log.w(TAG, "Embedded view called removeViewImmediate while detached from presentation");
413+
return;
431414
}
415+
view.clearAnimation();
416+
fakeWindowRootView.removeView(view);
432417
}
433418

434-
private void addView(Object[] args) {
419+
@Override
420+
public void addView(View view, ViewGroup.LayoutParams params) {
435421
if (fakeWindowRootView == null) {
436422
Log.w(TAG, "Embedded view called addView while detached from presentation");
437423
return;
438424
}
439-
View view = (View) args[0];
440-
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1];
441-
fakeWindowRootView.addView(view, layoutParams);
425+
fakeWindowRootView.addView(view, params);
442426
}
443427

444-
private void removeView(Object[] args) {
428+
@Override
429+
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
445430
if (fakeWindowRootView == null) {
446-
Log.w(TAG, "Embedded view called removeView while detached from presentation");
431+
Log.w(TAG, "Embedded view called updateViewLayout while detached from presentation");
447432
return;
448433
}
449-
View view = (View) args[0];
450-
fakeWindowRootView.removeView(view);
434+
fakeWindowRootView.updateViewLayout(view, params);
451435
}
452436

453-
private void removeViewImmediate(Object[] args) {
437+
@Override
438+
public void removeView(View view) {
454439
if (fakeWindowRootView == null) {
455-
Log.w(TAG, "Embedded view called removeViewImmediate while detached from presentation");
440+
Log.w(TAG, "Embedded view called removeView while detached from presentation");
456441
return;
457442
}
458-
View view = (View) args[0];
459-
view.clearAnimation();
460443
fakeWindowRootView.removeView(view);
461444
}
462445

463-
private void updateViewLayout(Object[] args) {
464-
if (fakeWindowRootView == null) {
465-
Log.w(TAG, "Embedded view called updateViewLayout while detached from presentation");
466-
return;
467-
}
468-
View view = (View) args[0];
469-
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1];
470-
fakeWindowRootView.updateViewLayout(view, layoutParams);
446+
@RequiresApi(api = Build.VERSION_CODES.R)
447+
@NonNull
448+
@Override
449+
public WindowMetrics getCurrentWindowMetrics() {
450+
return delegate.getCurrentWindowMetrics();
451+
}
452+
453+
@RequiresApi(api = Build.VERSION_CODES.R)
454+
@NonNull
455+
@Override
456+
public WindowMetrics getMaximumWindowMetrics() {
457+
return delegate.getMaximumWindowMetrics();
458+
}
459+
460+
@RequiresApi(api = Build.VERSION_CODES.S)
461+
@Override
462+
public boolean isCrossWindowBlurEnabled() {
463+
return delegate.isCrossWindowBlurEnabled();
464+
}
465+
466+
@RequiresApi(api = Build.VERSION_CODES.S)
467+
@Override
468+
public void addCrossWindowBlurEnabledListener(@NonNull Consumer<Boolean> listener) {
469+
delegate.addCrossWindowBlurEnabledListener(listener);
470+
}
471+
472+
@RequiresApi(api = Build.VERSION_CODES.S)
473+
@Override
474+
public void addCrossWindowBlurEnabledListener(
475+
@NonNull Executor executor, @NonNull Consumer<Boolean> listener) {
476+
delegate.addCrossWindowBlurEnabledListener(executor, listener);
477+
}
478+
479+
@RequiresApi(api = Build.VERSION_CODES.S)
480+
@Override
481+
public void removeCrossWindowBlurEnabledListener(@NonNull Consumer<Boolean> listener) {
482+
delegate.removeCrossWindowBlurEnabledListener(listener);
471483
}
472484
}
473485

shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,26 @@
77
import static android.os.Build.VERSION_CODES.KITKAT;
88
import static android.os.Build.VERSION_CODES.P;
99
import static android.os.Build.VERSION_CODES.R;
10+
import static android.os.Build.VERSION_CODES.S;
1011
import static org.junit.Assert.assertEquals;
1112
import static org.mockito.Mockito.mock;
1213
import static org.mockito.Mockito.spy;
14+
import static org.mockito.Mockito.verify;
15+
import static org.mockito.Mockito.verifyNoInteractions;
1316
import static org.mockito.Mockito.when;
1417

1518
import android.annotation.TargetApi;
1619
import android.content.Context;
1720
import android.hardware.display.DisplayManager;
1821
import android.view.Display;
22+
import android.view.View;
23+
import android.view.ViewGroup;
24+
import android.view.WindowManager;
1925
import android.view.inputmethod.InputMethodManager;
2026
import androidx.test.core.app.ApplicationProvider;
2127
import androidx.test.ext.junit.runners.AndroidJUnit4;
28+
import java.util.concurrent.Executor;
29+
import java.util.function.Consumer;
2230
import org.junit.Test;
2331
import org.junit.runner.RunWith;
2432
import org.robolectric.annotation.Config;
@@ -83,4 +91,112 @@ public void returnsOuterContextInputMethodManager_createDisplayContext() {
8391
// Android OS (or Robolectric's shadow, in this case).
8492
assertEquals(expected, actual);
8593
}
94+
95+
@Test
96+
@Config(minSdk = R)
97+
public void windowManagerHandler_passesCorrectlyToFakeWindowViewGroup() {
98+
// Mock the WindowManager and FakeWindowViewGroup that get used by the WindowManagerHandler.
99+
WindowManager mockWindowManager = mock(WindowManager.class);
100+
SingleViewPresentation.FakeWindowViewGroup mockFakeWindowViewGroup =
101+
mock(SingleViewPresentation.FakeWindowViewGroup.class);
102+
103+
View mockView = mock(View.class);
104+
ViewGroup.LayoutParams mockLayoutParams = mock(ViewGroup.LayoutParams.class);
105+
106+
SingleViewPresentation.WindowManagerHandler windowManagerHandler =
107+
new SingleViewPresentation.WindowManagerHandler(mockWindowManager, mockFakeWindowViewGroup);
108+
109+
// removeViewImmediate
110+
windowManagerHandler.removeViewImmediate(mockView);
111+
verify(mockView).clearAnimation();
112+
verify(mockFakeWindowViewGroup).removeView(mockView);
113+
verifyNoInteractions(mockWindowManager);
114+
115+
// addView
116+
windowManagerHandler.addView(mockView, mockLayoutParams);
117+
verify(mockFakeWindowViewGroup).addView(mockView, mockLayoutParams);
118+
verifyNoInteractions(mockWindowManager);
119+
120+
// updateViewLayout
121+
windowManagerHandler.updateViewLayout(mockView, mockLayoutParams);
122+
verify(mockFakeWindowViewGroup).updateViewLayout(mockView, mockLayoutParams);
123+
verifyNoInteractions(mockWindowManager);
124+
125+
// removeView
126+
windowManagerHandler.updateViewLayout(mockView, mockLayoutParams);
127+
verify(mockFakeWindowViewGroup).removeView(mockView);
128+
verifyNoInteractions(mockWindowManager);
129+
}
130+
131+
@Test
132+
@Config(minSdk = R)
133+
public void windowManagerHandler_logAndReturnEarly_whenFakeWindowViewGroupIsNull() {
134+
// Mock the WindowManager and FakeWindowViewGroup that get used by the WindowManagerHandler.
135+
WindowManager mockWindowManager = mock(WindowManager.class);
136+
137+
View mockView = mock(View.class);
138+
ViewGroup.LayoutParams mockLayoutParams = mock(ViewGroup.LayoutParams.class);
139+
140+
SingleViewPresentation.WindowManagerHandler windowManagerHandler =
141+
new SingleViewPresentation.WindowManagerHandler(mockWindowManager, null);
142+
143+
// removeViewImmediate
144+
windowManagerHandler.removeViewImmediate(mockView);
145+
verifyNoInteractions(mockView);
146+
verifyNoInteractions(mockWindowManager);
147+
148+
// addView
149+
windowManagerHandler.addView(mockView, mockLayoutParams);
150+
verifyNoInteractions(mockWindowManager);
151+
152+
// updateViewLayout
153+
windowManagerHandler.updateViewLayout(mockView, mockLayoutParams);
154+
verifyNoInteractions(mockWindowManager);
155+
156+
// removeView
157+
windowManagerHandler.updateViewLayout(mockView, mockLayoutParams);
158+
verifyNoInteractions(mockWindowManager);
159+
}
160+
161+
// This section tests that WindowManagerHandler forwards all of the non-special case calls to the
162+
// delegate WindowManager. Because this must include some deprecated WindowManager method calls
163+
// (because the proxy overrides every method), we suppress deprecation warnings here.
164+
@Test
165+
@Config(minSdk = S)
166+
@SuppressWarnings("deprecation")
167+
public void windowManagerHandler_forwardsAllOtherCallsToDelegate() {
168+
// Mock the WindowManager and FakeWindowViewGroup that get used by the WindowManagerHandler.
169+
WindowManager mockWindowManager = mock(WindowManager.class);
170+
SingleViewPresentation.FakeWindowViewGroup mockFakeWindowViewGroup =
171+
mock(SingleViewPresentation.FakeWindowViewGroup.class);
172+
173+
SingleViewPresentation.WindowManagerHandler windowManagerHandler =
174+
new SingleViewPresentation.WindowManagerHandler(mockWindowManager, mockFakeWindowViewGroup);
175+
176+
// Verify that all other calls get forwarded to the delegate.
177+
Executor mockExecutor = mock(Executor.class);
178+
@SuppressWarnings("Unchecked cast")
179+
Consumer<Boolean> mockListener = (Consumer<Boolean>) mock(Consumer.class);
180+
181+
windowManagerHandler.getDefaultDisplay();
182+
verify(mockWindowManager).getDefaultDisplay();
183+
184+
windowManagerHandler.getCurrentWindowMetrics();
185+
verify(mockWindowManager).getCurrentWindowMetrics();
186+
187+
windowManagerHandler.getMaximumWindowMetrics();
188+
verify(mockWindowManager).getMaximumWindowMetrics();
189+
190+
windowManagerHandler.isCrossWindowBlurEnabled();
191+
verify(mockWindowManager).isCrossWindowBlurEnabled();
192+
193+
windowManagerHandler.addCrossWindowBlurEnabledListener(mockListener);
194+
verify(mockWindowManager).addCrossWindowBlurEnabledListener(mockListener);
195+
196+
windowManagerHandler.addCrossWindowBlurEnabledListener(mockExecutor, mockListener);
197+
verify(mockWindowManager).addCrossWindowBlurEnabledListener(mockExecutor, mockListener);
198+
199+
windowManagerHandler.removeCrossWindowBlurEnabledListener(mockListener);
200+
verify(mockWindowManager).removeCrossWindowBlurEnabledListener(mockListener);
201+
}
86202
}

0 commit comments

Comments
 (0)