Ensure PlatformView engine life cycle callbacks are invoked (flutter/engine#42491)

- Move some code off of the message handler onto the parent class.
- Call the engine life cycle callbacks on PlatformView regardless of
which mode is used.
- Re-enable and fix test that these callbacks are invoked.

Fixes [#120329](https://github.com/flutter/flutter/issues/120329)

*If you had to change anything in the [flutter/tests] repo, include a
link to the migration guide as per the [breaking change policy].*

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide] and the [C++,
Objective-C, Java style guides].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I added new tests to check the change I am making or feature I am
adding, or Hixie said the PR is test-exempt. See [testing the engine]
for instructions on writing and running engine tests.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I signed the [CLA].
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
This commit is contained in:
John McCutchan 2023-06-01 14:51:25 -07:00 committed by GitHub
parent 8bcecafd93
commit d72aace3ef
3 changed files with 284 additions and 230 deletions

View File

@ -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,205 +456,203 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
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);
}
}
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
+ ")");
}
}
// 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);
}
Object createParams = null;
if (request.params != null) {
createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params);
}
// 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);
// 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);
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 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);
}
// 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.
vdControllers.put(request.viewId, vdController);
final View embeddedView = platformView.getView();
contextToEmbeddedView.put(embeddedView.getContext(), embeddedView);
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().
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);
// 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);
// 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;
}
@Override
public void synchronizeToNativeViewHierarchy(boolean yes) {
synchronizeToNativeViewHierarchy = yes;
}
};
/// 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);
}
}
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
+ ")");
}
}
// 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);
}
Object createParams = null;
if (request.params != null) {
createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params);
}
// 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);
// 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) {
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.
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);
}
// 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);
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);
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 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);
maybeInvokeOnFlutterViewAttached(platformView);
return textureId;
}
@VisibleForTesting
public MotionEvent toMotionEvent(
float density, PlatformViewsChannel.PlatformViewTouch touch, boolean usingVirtualDiplay) {
@ -840,6 +838,15 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
}
}
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 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
}
}
/**
* 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();

View File

@ -218,8 +218,8 @@ class SingleViewPresentation extends Presentation {
return state;
}
@Nullable
public PlatformView getView() {
if (state.platformView == null) return null;
return state.platformView;
}

View File

@ -38,9 +38,11 @@ import io.flutter.embedding.engine.renderer.FlutterRenderer;
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 @@ import org.robolectric.shadows.ShadowSurfaceView;
@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;
public CountingPlatformView(Context context) {
view = new SurfaceView(context);
}
public int disposeCalls = 0;
public int attachCalls = 0;
public int detachCalls = 0;
@Override
public void dispose() {
disposeCalls++;
}
@Override
public View getView() {
return view;
}
@Override
public void onFlutterViewAttached(View flutterView) {
attachCalls++;
}
@Override
public void onFlutterViewDetached() {
detachCalls++;
}
}
@Ignore
@Test
public void itNotifiesVirtualDisplayControllersOfViewAttachmentAndDetachment() {
// Setup test structure.
FlutterView fakeFlutterView = new FlutterView(ApplicationProvider.getApplicationContext());
// 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);
// Create the PlatformViewsController that is under 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();
// Manually inject fake VirtualDisplayControllers into the PlatformViewsController.
platformViewsController.vdControllers.put(0, fakeVdController1);
platformViewsController.vdControllers.put(1, fakeVdController1);
// 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);
}
});
// Execute test & verify results.
// Attach PlatformViewsController to the fake Flutter View.
platformViewsController.attachToView(fakeFlutterView);
// 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();
// Detach PlatformViewsController from the fake Flutter View.
// 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();
// 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();
assertEquals(1, cpv.attachCalls);
assertEquals(1, cpv.detachCalls);
assertEquals(0, cpv.disposeCalls);
platformViewsController.disposePlatformView(viewId);
}
@Ignore