diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index c8326a387e..eeb55d2b99 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -205,11 +205,19 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega layoutParams.topMargin = physicalTop; layoutParams.leftMargin = physicalLeft; wrapperView.setLayoutParams(layoutParams); - wrapperView.setLayoutDirection(request.direction); - wrapperView.addView(platformView.getView()); + + final View view = platformView.getView(); + if (view == null) { + throw new IllegalStateException( + "PlatformView#getView() returned null, but an Android view reference was expected."); + } else if (view.getParent() != null) { + throw new IllegalStateException( + "The Android view returned from PlatformView#getView() was already added to a parent view."); + } + wrapperView.addView(view); wrapperView.setOnDescendantFocusChangeListener( - (view, hasFocus) -> { + (v, hasFocus) -> { if (hasFocus) { platformViewsChannel.invokeViewFocused(viewId); } else if (textInputPlugin != null) { @@ -226,16 +234,13 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega public void dispose(int viewId) { final PlatformView platformView = platformViews.get(viewId); if (platformView != null) { - final ViewGroup pvParent = (ViewGroup) platformView.getView().getParent(); - if (pvParent != null) { - pvParent.removeView(platformView.getView()); - } platformViews.remove(viewId); platformView.dispose(); } // The platform view is displayed using a TextureLayer. final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId); if (viewWrapper != null) { + viewWrapper.removeAllViews(); viewWrapper.release(); viewWrapper.unsetOnDescendantFocusChangeListener(); @@ -251,6 +256,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega // https://github.com/flutter/flutter/issues/96679 final FlutterMutatorView parentView = platformViewParent.get(viewId); if (parentView != null) { + parentView.removeAllViews(); parentView.unsetOnDescendantFocusChangeListener(); final ViewGroup mutatorViewParent = (ViewGroup) parentView.getParent(); @@ -322,7 +328,12 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); final float density = context.getResources().getDisplayMetrics().density; final MotionEvent event = toMotionEvent(density, touch); - platformView.getView().dispatchTouchEvent(event); + final View view = platformView.getView(); + if (view == null) { + Log.e(TAG, "Sending touch to a null view with id: " + viewId); + return; + } + view.dispatchTouchEvent(event); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @@ -342,7 +353,12 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega return; } ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); - platformViews.get(viewId).getView().setLayoutDirection(direction); + final View view = platformView.getView(); + if (view == null) { + Log.e(TAG, "Setting direction to a null view with id: " + viewId); + return; + } + view.setLayoutDirection(direction); } @Override @@ -352,7 +368,12 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId); return; } - platformView.getView().clearFocus(); + final View view = platformView.getView(); + if (view == null) { + Log.e(TAG, "Clearing focus on a null view with id: " + viewId); + return; + } + view.clearFocus(); } private void ensureValidAndroidVersion(int minSdkVersion) { diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 0398f88003..37a0f588ed 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -167,6 +167,35 @@ public class PlatformViewsControllerTest { FlutterJNI jni = new FlutterJNI(); attach(jni, platformViewsController); + // Simulate create call from the framework. + createPlatformView( + jni, platformViewsController, platformViewId, "testType", /* hybrid=*/ false); + assertEquals(ShadowFlutterJNI.getResponses().size(), 1); + + assertThrows( + IllegalStateException.class, + () -> { + platformViewsController.initializePlatformViewIfNeeded(platformViewId); + }); + } + + @Test + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) + public void createHybridPlatformViewMessage__throwsIfViewIsNull() { + PlatformViewsController platformViewsController = new PlatformViewsController(); + + int platformViewId = 0; + assertNull(platformViewsController.getPlatformViewById(platformViewId)); + + PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); + PlatformView platformView = mock(PlatformView.class); + when(platformView.getView()).thenReturn(null); + when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); + platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); + + FlutterJNI jni = new FlutterJNI(); + attach(jni, platformViewsController); + // Simulate create call from the framework. createPlatformView(jni, platformViewsController, platformViewId, "testType", /* hybrid=*/ true); assertEquals(ShadowFlutterJNI.getResponses().size(), 1); @@ -197,6 +226,37 @@ public class PlatformViewsControllerTest { FlutterJNI jni = new FlutterJNI(); attach(jni, platformViewsController); + // Simulate create call from the framework. + createPlatformView( + jni, platformViewsController, platformViewId, "testType", /* hybrid=*/ false); + assertEquals(ShadowFlutterJNI.getResponses().size(), 1); + + assertThrows( + IllegalStateException.class, + () -> { + platformViewsController.initializePlatformViewIfNeeded(platformViewId); + }); + } + + @Test + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) + public void createHybridPlatformViewMessage__throwsIfViewHasParent() { + PlatformViewsController platformViewsController = new PlatformViewsController(); + + int platformViewId = 0; + assertNull(platformViewsController.getPlatformViewById(platformViewId)); + + PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); + PlatformView platformView = mock(PlatformView.class); + View androidView = mock(View.class); + when(androidView.getParent()).thenReturn(mock(ViewParent.class)); + when(platformView.getView()).thenReturn(androidView); + when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); + platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); + + FlutterJNI jni = new FlutterJNI(); + attach(jni, platformViewsController); + // Simulate create call from the framework. createPlatformView(jni, platformViewsController, platformViewId, "testType", /* hybrid=*/ true); assertEquals(ShadowFlutterJNI.getResponses().size(), 1); @@ -283,6 +343,38 @@ public class PlatformViewsControllerTest { verify(platformView, times(1)).dispose(); } + @Test + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) + public void disposeNullAndroidView() { + PlatformViewsController platformViewsController = new PlatformViewsController(); + + int platformViewId = 0; + assertNull(platformViewsController.getPlatformViewById(platformViewId)); + + PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); + PlatformView platformView = mock(PlatformView.class); + + Context context = RuntimeEnvironment.application.getApplicationContext(); + View androidView = new View(context); + when(platformView.getView()).thenReturn(androidView); + when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); + platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); + + FlutterJNI jni = new FlutterJNI(); + attach(jni, platformViewsController); + + // Simulate create call from the framework. + createPlatformView( + jni, platformViewsController, platformViewId, "testType", /* hybrid=*/ false); + platformViewsController.initializePlatformViewIfNeeded(platformViewId); + + when(platformView.getView()).thenReturn(null); + + // Simulate dispose call from the framework. + disposePlatformView(jni, platformViewsController, platformViewId); + verify(platformView, times(1)).dispose(); + } + @Test @Config( shadows = {