blob: 1cb44373b1bdf18160d93c8966d6928f037fa41d [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugin.platform;
import static android.view.View.OnFocusChangeListener;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnFocusChangeListener;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@TargetApi(20)
class VirtualDisplayController {
private static String TAG = "VirtualDisplayController";
public static VirtualDisplayController create(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
PlatformView view,
PlatformViewRenderTarget renderTarget,
int width,
int height,
int viewId,
Object createParams,
OnFocusChangeListener focusChangeListener) {
if (width == 0 || height == 0) {
return null;
}
DisplayManager displayManager =
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
// Virtual Display crashes for some PlatformViews if the width or height is bigger
// than the physical screen size. We have tried to clamp or scale down the size to prevent
// the crash, but both solutions lead to unwanted behavior because the
// AndroidPlatformView(https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/platform_view.dart#L677) widget doesn't
// scale or clamp, which leads to a mismatch between the size of the widget and the size of
// virtual display.
// This mismatch leads to some test failures: https://github.com/flutter/flutter/issues/106750
// TODO(cyanglaz): find a way to prevent the crash without introducing size mistach betewen
// virtual display and AndroidPlatformView widget.
// https://github.com/flutter/flutter/issues/93115
renderTarget.resize(width, height);
VirtualDisplay virtualDisplay =
displayManager.createVirtualDisplay(
"flutter-vd#" + viewId,
width,
height,
metrics.densityDpi,
renderTarget.getSurface(),
0);
if (virtualDisplay == null) {
return null;
}
VirtualDisplayController controller =
new VirtualDisplayController(
context,
accessibilityEventsDelegate,
virtualDisplay,
view,
renderTarget,
focusChangeListener,
viewId,
createParams);
return controller;
}
@VisibleForTesting SingleViewPresentation presentation;
private final Context context;
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
private final int densityDpi;
private final int viewId;
private final PlatformViewRenderTarget renderTarget;
private final OnFocusChangeListener focusChangeListener;
private VirtualDisplay virtualDisplay;
private VirtualDisplayController(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
VirtualDisplay virtualDisplay,
PlatformView view,
PlatformViewRenderTarget renderTarget,
OnFocusChangeListener focusChangeListener,
int viewId,
Object createParams) {
this.context = context;
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
this.renderTarget = renderTarget;
this.focusChangeListener = focusChangeListener;
this.viewId = viewId;
this.virtualDisplay = virtualDisplay;
this.densityDpi = context.getResources().getDisplayMetrics().densityDpi;
presentation =
new SingleViewPresentation(
context,
this.virtualDisplay.getDisplay(),
view,
accessibilityEventsDelegate,
viewId,
focusChangeListener);
presentation.show();
}
public int getRenderTargetWidth() {
if (renderTarget != null) {
return renderTarget.getWidth();
}
return 0;
}
public int getRenderTargetHeight() {
if (renderTarget != null) {
return renderTarget.getHeight();
}
return 0;
}
public void resize(final int width, final int height, final Runnable onNewSizeFrameAvailable) {
// When 'hot reload', although the resize method is triggered, the size of the native View has
// not changed.
if (width == getRenderTargetWidth() && height == getRenderTargetHeight()) {
getView().postDelayed(onNewSizeFrameAvailable, 0);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
resize31(getView(), width, height, onNewSizeFrameAvailable);
return;
}
boolean isFocused = getView().isFocused();
final SingleViewPresentation.PresentationState presentationState = presentation.detachState();
// We detach the surface to prevent it being destroyed when releasing the vd.
virtualDisplay.setSurface(null);
virtualDisplay.release();
final DisplayManager displayManager =
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
renderTarget.resize(width, height);
virtualDisplay =
displayManager.createVirtualDisplay(
"flutter-vd#" + viewId, width, height, densityDpi, renderTarget.getSurface(), 0);
final View embeddedView = getView();
// There's a bug in Android version older than O where view tree observer onDrawListeners don't
// get properly
// merged when attaching to window, as a workaround we register the on draw listener after the
// view is attached.
embeddedView.addOnAttachStateChangeListener(
new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
OneTimeOnDrawListener.schedule(
embeddedView,
new Runnable() {
@Override
public void run() {
// We need some delay here until the frame propagates through the vd surface to
// the texture,
// 128ms was picked pretty arbitrarily based on trial and error.
// As long as we invoke the runnable after a new frame is available we avoid the
// scaling jank
// described in: https://github.com/flutter/flutter/issues/19572
// We should ideally run onNewSizeFrameAvailable ASAP to make the embedded view
// more responsive
// following a resize.
embeddedView.postDelayed(onNewSizeFrameAvailable, 128);
}
});
embeddedView.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewDetachedFromWindow(View v) {}
});
// Create a new SingleViewPresentation and show() it before we cancel() the existing
// presentation. Calling show() and cancel() in this order fixes
// https://github.com/flutter/flutter/issues/26345 and maintains seamless transition
// of the contents of the presentation.
SingleViewPresentation newPresentation =
new SingleViewPresentation(
context,
virtualDisplay.getDisplay(),
accessibilityEventsDelegate,
presentationState,
focusChangeListener,
isFocused);
newPresentation.show();
presentation.cancel();
presentation = newPresentation;
}
public void dispose() {
// Fix rare crash on HuaWei device described in: https://github.com/flutter/engine/pull/9192
presentation.cancel();
presentation.detachState();
virtualDisplay.release();
renderTarget.release();
}
@TargetApi(31)
private void resize31(
View embeddedView, int width, int height, final Runnable onNewSizeFrameAvailable) {
renderTarget.resize(width, height);
// On Android versions 31+ resizing of a Virtual Display's Presentation is natively supported.
virtualDisplay.resize(width, height, densityDpi);
embeddedView.postDelayed(onNewSizeFrameAvailable, 0);
}
/** See {@link PlatformView#onFlutterViewAttached(View)} */
/*package*/ void onFlutterViewAttached(@NonNull View flutterView) {
if (presentation == null || presentation.getView() == null) {
return;
}
presentation.getView().onFlutterViewAttached(flutterView);
}
/** See {@link PlatformView#onFlutterViewDetached()} */
/*package*/ void onFlutterViewDetached() {
if (presentation == null || presentation.getView() == null) {
return;
}
presentation.getView().onFlutterViewDetached();
}
/*package*/ void onInputConnectionLocked() {
if (presentation == null || presentation.getView() == null) {
return;
}
presentation.getView().onInputConnectionLocked();
}
/*package*/ void onInputConnectionUnlocked() {
if (presentation == null || presentation.getView() == null) {
return;
}
presentation.getView().onInputConnectionUnlocked();
}
public View getView() {
if (presentation == null) return null;
PlatformView platformView = presentation.getView();
return platformView.getView();
}
/** Dispatches a motion event to the presentation for this controller. */
public void dispatchTouchEvent(MotionEvent event) {
if (presentation == null) return;
presentation.dispatchTouchEvent(event);
}
static class OneTimeOnDrawListener implements ViewTreeObserver.OnDrawListener {
static void schedule(View view, Runnable runnable) {
OneTimeOnDrawListener listener = new OneTimeOnDrawListener(view, runnable);
view.getViewTreeObserver().addOnDrawListener(listener);
}
final View mView;
Runnable mOnDrawRunnable;
OneTimeOnDrawListener(View view, Runnable onDrawRunnable) {
this.mView = view;
this.mOnDrawRunnable = onDrawRunnable;
}
@Override
public void onDraw() {
if (mOnDrawRunnable == null) {
return;
}
mOnDrawRunnable.run();
mOnDrawRunnable = null;
mView.post(
new Runnable() {
@Override
public void run() {
mView.getViewTreeObserver().removeOnDrawListener(OneTimeOnDrawListener.this);
}
});
}
}
}