diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 4b61cdc10d8d8..4515aa46c3284 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -157,7 +157,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega public void createForPlatformViewLayer( @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { // API level 19 is required for `android.graphics.ImageReader`. - ensureValidAndroidVersion(19); + enforceMinimumAndroidApiVersion(19); ensureValidRequest(request); final PlatformView platformView = createPlatformView(request, false); @@ -456,204 +456,202 @@ public void clearFocus(int viewId) { embeddedView.clearFocus(); } - private void ensureValidAndroidVersion(int minSdkVersion) { - if (Build.VERSION.SDK_INT < minSdkVersion) { - throw new IllegalStateException( - "Trying to use platform views with API " - + Build.VERSION.SDK_INT - + ", required API level is: " - + minSdkVersion); - } + @Override + public void synchronizeToNativeViewHierarchy(boolean yes) { + synchronizeToNativeViewHierarchy = yes; } + }; - private void ensureValidRequest( - @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { - if (!validateDirection(request.direction)) { - throw new IllegalStateException( - "Trying to create a view with unknown direction value: " - + request.direction - + "(view id: " - + request.viewId - + ")"); - } - } + /// Throws an exception if the SDK version is below minSdkVersion. + private void enforceMinimumAndroidApiVersion(int minSdkVersion) { + if (Build.VERSION.SDK_INT < minSdkVersion) { + throw new IllegalStateException( + "Trying to use platform views with API " + + Build.VERSION.SDK_INT + + ", required API level is: " + + minSdkVersion); + } + } - // Creates a platform view based on `request`, performs configuration that's common to - // all display modes, and adds it to `platformViews`. - @TargetApi(19) - private PlatformView createPlatformView( - @NonNull PlatformViewsChannel.PlatformViewCreationRequest request, - boolean wrapContext) { - final PlatformViewFactory viewFactory = registry.getFactory(request.viewType); - if (viewFactory == null) { - throw new IllegalStateException( - "Trying to create a platform view of unregistered type: " + request.viewType); - } + private void ensureValidRequest( + @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { + if (!validateDirection(request.direction)) { + throw new IllegalStateException( + "Trying to create a view with unknown direction value: " + + request.direction + + "(view id: " + + request.viewId + + ")"); + } + } - Object createParams = null; - if (request.params != null) { - createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params); - } + // Creates a platform view based on `request`, performs configuration that's common to + // all display modes, and adds it to `platformViews`. + @TargetApi(19) + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + public PlatformView createPlatformView( + @NonNull PlatformViewsChannel.PlatformViewCreationRequest request, boolean wrapContext) { + final PlatformViewFactory viewFactory = registry.getFactory(request.viewType); + if (viewFactory == null) { + throw new IllegalStateException( + "Trying to create a platform view of unregistered type: " + request.viewType); + } - // In some display modes, the context needs to be modified during display. - // TODO(stuartmorgan): Make this wrapping unconditional if possible; for context see - // https://github.com/flutter/flutter/issues/113449 - final Context mutableContext = wrapContext ? new MutableContextWrapper(context) : context; - final PlatformView platformView = - viewFactory.create(mutableContext, request.viewId, createParams); + Object createParams = null; + if (request.params != null) { + createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params); + } - // Configure the view to match the requested layout direction. - final View embeddedView = platformView.getView(); - if (embeddedView == null) { - throw new IllegalStateException( - "PlatformView#getView() returned null, but an Android view reference was expected."); - } - embeddedView.setLayoutDirection(request.direction); + // In some display modes, the context needs to be modified during display. + // TODO(stuartmorgan): Make this wrapping unconditional if possible; for context see + // https://github.com/flutter/flutter/issues/113449 + final Context mutableContext = wrapContext ? new MutableContextWrapper(context) : context; + final PlatformView platformView = + viewFactory.create(mutableContext, request.viewId, createParams); - platformViews.put(request.viewId, platformView); - return platformView; - } + // Configure the view to match the requested layout direction. + final View embeddedView = platformView.getView(); + if (embeddedView == null) { + throw new IllegalStateException( + "PlatformView#getView() returned null, but an Android view reference was expected."); + } + embeddedView.setLayoutDirection(request.direction); + platformViews.put(request.viewId, platformView); + maybeInvokeOnFlutterViewAttached(platformView); + return platformView; + } - // Configures the view for Hybrid Composition mode. - private void configureForHybridComposition( - @NonNull PlatformView platformView, - @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { - Log.i(TAG, "Using hybrid composition for platform view: " + request.viewId); - } + // Configures the view for Hybrid Composition mode. + private void configureForHybridComposition( + @NonNull PlatformView platformView, + @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { + enforceMinimumAndroidApiVersion(19); + Log.i(TAG, "Using hybrid composition for platform view: " + request.viewId); + } - // Configures the view for Virtual Display mode, returning the associated texture ID. - private long configureForVirtualDisplay( - @NonNull PlatformView platformView, - @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { - // This mode adds the view to a virtual display, which is wired up to a GL texture that - // is composed by the Flutter engine. - - // API level 20 is required to use VirtualDisplay#setSurface. - ensureValidAndroidVersion(20); - - Log.i(TAG, "Hosting view in a virtual display for platform view: " + request.viewId); - - final TextureRegistry.SurfaceTextureEntry textureEntry = - textureRegistry.createSurfaceTexture(); - final int physicalWidth = toPhysicalPixels(request.logicalWidth); - final int physicalHeight = toPhysicalPixels(request.logicalHeight); - final VirtualDisplayController vdController = - VirtualDisplayController.create( - context, - accessibilityEventsDelegate, - platformView, - textureEntry, - physicalWidth, - physicalHeight, - request.viewId, - null, - (view, hasFocus) -> { - if (hasFocus) { - platformViewsChannel.invokeViewFocused(request.viewId); - } - }); - - if (vdController == null) { - throw new IllegalStateException( - "Failed creating virtual display for a " - + request.viewType - + " with id: " - + request.viewId); - } + // Configures the view for Virtual Display mode, returning the associated texture ID. + private long configureForVirtualDisplay( + @NonNull PlatformView platformView, + @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { + // This mode adds the view to a virtual display, which is wired up to a GL texture that + // is composed by the Flutter engine. + + // API level 20 is required to use VirtualDisplay#setSurface. + enforceMinimumAndroidApiVersion(20); + + Log.i(TAG, "Hosting view in a virtual display for platform view: " + request.viewId); + + final TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture(); + final int physicalWidth = toPhysicalPixels(request.logicalWidth); + final int physicalHeight = toPhysicalPixels(request.logicalHeight); + final VirtualDisplayController vdController = + VirtualDisplayController.create( + context, + accessibilityEventsDelegate, + platformView, + textureEntry, + physicalWidth, + physicalHeight, + request.viewId, + null, + (view, hasFocus) -> { + if (hasFocus) { + platformViewsChannel.invokeViewFocused(request.viewId); + } + }); + + if (vdController == null) { + throw new IllegalStateException( + "Failed creating virtual display for a " + + request.viewType + + " with id: " + + request.viewId); + } - // If our FlutterEngine is already attached to a Flutter UI, provide that Android - // View to this new platform view. - if (flutterView != null) { - vdController.onFlutterViewAttached(flutterView); - } + // The embedded view doesn't need to be sized in Virtual Display mode because the + // virtual display itself is sized. - // The embedded view doesn't need to be sized in Virtual Display mode because the - // virtual display itself is sized. + vdControllers.put(request.viewId, vdController); + final View embeddedView = platformView.getView(); + contextToEmbeddedView.put(embeddedView.getContext(), embeddedView); - vdControllers.put(request.viewId, vdController); - final View embeddedView = platformView.getView(); - contextToEmbeddedView.put(embeddedView.getContext(), embeddedView); + return textureEntry.id(); + } - return textureEntry.id(); - } + // Configures the view for Texture Layer Hybrid Composition mode, returning the associated + // texture ID. + @TargetApi(23) + private long configureForTextureLayerComposition( + @NonNull PlatformView platformView, + @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { + // This mode attaches the view to the Android view hierarchy and record its drawing + // operations, so they can be forwarded to a GL texture that is composed by the + // Flutter engine. + + // API level 23 is required to use Surface#lockHardwareCanvas(). + enforceMinimumAndroidApiVersion(23); + Log.i(TAG, "Hosting view in view hierarchy for platform view: " + request.viewId); + + final int physicalWidth = toPhysicalPixels(request.logicalWidth); + final int physicalHeight = toPhysicalPixels(request.logicalHeight); + PlatformViewWrapper viewWrapper; + long textureId; + if (usesSoftwareRendering) { + viewWrapper = new PlatformViewWrapper(context); + textureId = -1; + } else { + final TextureRegistry.SurfaceTextureEntry textureEntry = + textureRegistry.createSurfaceTexture(); + viewWrapper = new PlatformViewWrapper(context, textureEntry); + textureId = textureEntry.id(); + } + viewWrapper.setTouchProcessor(androidTouchProcessor); + viewWrapper.setBufferSize(physicalWidth, physicalHeight); - // Configures the view for Texture Layer Hybrid Composition mode, returning the associated - // texture ID. - @TargetApi(23) - private long configureForTextureLayerComposition( - @NonNull PlatformView platformView, - @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { - // This mode attaches the view to the Android view hierarchy and record its drawing - // operations, so they can be forwarded to a GL texture that is composed by the - // Flutter engine. - - // API level 23 is required to use Surface#lockHardwareCanvas(). - ensureValidAndroidVersion(23); - Log.i(TAG, "Hosting view in view hierarchy for platform view: " + request.viewId); - - final int physicalWidth = toPhysicalPixels(request.logicalWidth); - final int physicalHeight = toPhysicalPixels(request.logicalHeight); - PlatformViewWrapper viewWrapper; - long textureId; - if (usesSoftwareRendering) { - viewWrapper = new PlatformViewWrapper(context); - textureId = -1; - } else { - final TextureRegistry.SurfaceTextureEntry textureEntry = - textureRegistry.createSurfaceTexture(); - viewWrapper = new PlatformViewWrapper(context, textureEntry); - textureId = textureEntry.id(); - } - viewWrapper.setTouchProcessor(androidTouchProcessor); - viewWrapper.setBufferSize(physicalWidth, physicalHeight); + final FrameLayout.LayoutParams viewWrapperLayoutParams = + new FrameLayout.LayoutParams(physicalWidth, physicalHeight); - final FrameLayout.LayoutParams viewWrapperLayoutParams = - new FrameLayout.LayoutParams(physicalWidth, physicalHeight); + // Size and position the view wrapper. + final int physicalTop = toPhysicalPixels(request.logicalTop); + final int physicalLeft = toPhysicalPixels(request.logicalLeft); + viewWrapperLayoutParams.topMargin = physicalTop; + viewWrapperLayoutParams.leftMargin = physicalLeft; + viewWrapper.setLayoutParams(viewWrapperLayoutParams); - // Size and position the view wrapper. - final int physicalTop = toPhysicalPixels(request.logicalTop); - final int physicalLeft = toPhysicalPixels(request.logicalLeft); - viewWrapperLayoutParams.topMargin = physicalTop; - viewWrapperLayoutParams.leftMargin = physicalLeft; - viewWrapper.setLayoutParams(viewWrapperLayoutParams); + // Size the embedded view. + final View embeddedView = platformView.getView(); + embeddedView.setLayoutParams(new FrameLayout.LayoutParams(physicalWidth, physicalHeight)); + + // Accessibility in the embedded view is initially disabled because if a Flutter app + // disabled accessibility in the first frame, the embedding won't receive an update to + // disable accessibility since the embedding never received an update to enable it. + // The AccessibilityBridge keeps track of the accessibility nodes, and handles the deltas + // when the framework sends a new a11y tree to the embedding. + // To prevent races, the framework populate the SemanticsNode after the platform view has + // been created. + embeddedView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - // Size the embedded view. - final View embeddedView = platformView.getView(); - embeddedView.setLayoutParams(new FrameLayout.LayoutParams(physicalWidth, physicalHeight)); - - // Accessibility in the embedded view is initially disabled because if a Flutter app - // disabled accessibility in the first frame, the embedding won't receive an update to - // disable accessibility since the embedding never received an update to enable it. - // The AccessibilityBridge keeps track of the accessibility nodes, and handles the deltas - // when the framework sends a new a11y tree to the embedding. - // To prevent races, the framework populate the SemanticsNode after the platform view has - // been created. - embeddedView.setImportantForAccessibility( - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - - // Add the embedded view to the wrapper. - viewWrapper.addView(embeddedView); - - // Listen for focus changed in any subview, so the framework is notified when the platform - // view is focused. - viewWrapper.setOnDescendantFocusChangeListener( - (v, hasFocus) -> { - if (hasFocus) { - platformViewsChannel.invokeViewFocused(request.viewId); - } else if (textInputPlugin != null) { - textInputPlugin.clearPlatformViewClient(request.viewId); - } - }); - flutterView.addView(viewWrapper); - viewWrappers.append(request.viewId, viewWrapper); - return textureId; - } + // Add the embedded view to the wrapper. + viewWrapper.addView(embeddedView); - @Override - public void synchronizeToNativeViewHierarchy(boolean yes) { - synchronizeToNativeViewHierarchy = yes; - } - }; + // Listen for focus changed in any subview, so the framework is notified when the platform + // view is focused. + viewWrapper.setOnDescendantFocusChangeListener( + (v, hasFocus) -> { + if (hasFocus) { + platformViewsChannel.invokeViewFocused(request.viewId); + } else if (textInputPlugin != null) { + textInputPlugin.clearPlatformViewClient(request.viewId); + } + }); + + flutterView.addView(viewWrapper); + viewWrappers.append(request.viewId, viewWrapper); + + maybeInvokeOnFlutterViewAttached(platformView); + + return textureId; + } @VisibleForTesting public MotionEvent toMotionEvent( @@ -840,6 +838,15 @@ public void detachFromView() { } } + private void maybeInvokeOnFlutterViewAttached(PlatformView view) { + if (flutterView == null) { + Log.i(TAG, "null flutterView"); + // There is currently no FlutterView that we are attached to. + return; + } + view.onFlutterViewAttached(flutterView); + } + @Override public void attachAccessibilityBridge(@NonNull AccessibilityBridge accessibilityBridge) { accessibilityEventsDelegate.setAccessibilityBridge(accessibilityBridge); @@ -1024,6 +1031,16 @@ private void diposeAllViews() { } } + /** + * Disposes a single + * + * @param viewId the PlatformView ID. + */ + @VisibleForTesting + public void disposePlatformView(int viewId) { + channelHandler.dispose(viewId); + } + private void initializeRootImageViewIfNeeded() { if (synchronizeToNativeViewHierarchy && !flutterViewConvertedToImageView) { flutterView.convertToImageView(); diff --git a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java index 7841452321878..26c70bae5b5d1 100644 --- a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +++ b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java @@ -218,8 +218,8 @@ public PresentationState detachState() { return state; } + @Nullable public PlatformView getView() { - if (state.platformView == null) return null; return state.platformView; } diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index c2cd76001fc16..7520c8f9f346d 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -38,9 +38,11 @@ import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; import io.flutter.embedding.engine.systemchannels.KeyboardChannel; import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; +import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.common.StandardMethodCodec; import io.flutter.plugin.localization.LocalizationPlugin; import io.flutter.view.TextureRegistry; @@ -61,47 +63,82 @@ @Config(manifest = Config.NONE) @RunWith(AndroidJUnit4.class) public class PlatformViewsControllerTest { + // An implementation of PlatformView that counts invocations of its lifecycle callbacks. + class CountingPlatformView implements PlatformView { + static final String VIEW_TYPE_ID = "CountingPlatformView"; + private View view; - @Ignore - @Test - public void itNotifiesVirtualDisplayControllersOfViewAttachmentAndDetachment() { - // Setup test structure. - FlutterView fakeFlutterView = new FlutterView(ApplicationProvider.getApplicationContext()); + public CountingPlatformView(Context context) { + view = new SurfaceView(context); + } - // Create fake VirtualDisplayControllers. This requires internal knowledge of - // PlatformViewsController. We know that all PlatformViewsController does is - // forward view attachment/detachment calls to it's VirtualDisplayControllers. - // - // TODO(mattcarroll): once PlatformViewsController is refactored into testable - // pieces, remove this test and avoid verifying private behavior. - VirtualDisplayController fakeVdController1 = mock(VirtualDisplayController.class); - VirtualDisplayController fakeVdController2 = mock(VirtualDisplayController.class); + public int disposeCalls = 0; + public int attachCalls = 0; + public int detachCalls = 0; - // Create the PlatformViewsController that is under test. - PlatformViewsController platformViewsController = new PlatformViewsController(); + @Override + public void dispose() { + disposeCalls++; + } + + @Override + public View getView() { + return view; + } - // Manually inject fake VirtualDisplayControllers into the PlatformViewsController. - platformViewsController.vdControllers.put(0, fakeVdController1); - platformViewsController.vdControllers.put(1, fakeVdController1); + @Override + public void onFlutterViewAttached(View flutterView) { + attachCalls++; + } - // Execute test & verify results. - // Attach PlatformViewsController to the fake Flutter View. - platformViewsController.attachToView(fakeFlutterView); + @Override + public void onFlutterViewDetached() { + detachCalls++; + } + } - // Verify that all virtual display controllers were notified of View attachment. - verify(fakeVdController1, times(1)).onFlutterViewAttached(eq(fakeFlutterView)); - verify(fakeVdController1, never()).onFlutterViewDetached(); - verify(fakeVdController2, times(1)).onFlutterViewAttached(eq(fakeFlutterView)); - verify(fakeVdController2, never()).onFlutterViewDetached(); + @Test + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) + public void itNotifiesPlatformViewsOfEngineAttachmentAndDetachment() { + PlatformViewsController platformViewsController = new PlatformViewsController(); + FlutterJNI jni = new FlutterJNI(); + attach(jni, platformViewsController); + // Get the platform view registry. + PlatformViewRegistry registry = platformViewsController.getRegistry(); - // Detach PlatformViewsController from the fake Flutter View. - platformViewsController.detachFromView(); + // Register a factory for our platform view. + registry.registerViewFactory( + CountingPlatformView.VIEW_TYPE_ID, + new PlatformViewFactory(StandardMessageCodec.INSTANCE) { + @Override + public PlatformView create(Context context, int viewId, Object args) { + return new CountingPlatformView(context); + } + }); - // Verify that all virtual display controllers were notified of the View detachment. - verify(fakeVdController1, times(1)).onFlutterViewAttached(eq(fakeFlutterView)); - verify(fakeVdController1, times(1)).onFlutterViewDetached(); - verify(fakeVdController2, times(1)).onFlutterViewAttached(eq(fakeFlutterView)); - verify(fakeVdController2, times(1)).onFlutterViewDetached(); + // Create the platform view. + int viewId = 0; + final PlatformViewsChannel.PlatformViewCreationRequest request = + new PlatformViewsChannel.PlatformViewCreationRequest( + viewId++, + CountingPlatformView.VIEW_TYPE_ID, + 0, + 0, + 128, + 128, + View.LAYOUT_DIRECTION_LTR, + null); + PlatformView pView = platformViewsController.createPlatformView(request, true); + assertTrue(pView instanceof CountingPlatformView); + CountingPlatformView cpv = (CountingPlatformView) pView; + assertEquals(1, cpv.attachCalls); + assertEquals(0, cpv.detachCalls); + assertEquals(0, cpv.disposeCalls); + platformViewsController.detachFromView(); + assertEquals(1, cpv.attachCalls); + assertEquals(1, cpv.detachCalls); + assertEquals(0, cpv.disposeCalls); + platformViewsController.disposePlatformView(viewId); } @Ignore