Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add views added to the WindowManager into the presentation view tree. #6043

Merged
merged 1 commit into from
Aug 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,69 @@
import android.annotation.TargetApi;
import android.app.Presentation;
import android.content.Context;
import android.content.ContextWrapper;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.view.Display;
import android.view.View;
import android.view.WindowManager;
import android.util.Log;
import android.view.*;
import android.widget.FrameLayout;

import java.lang.reflect.*;

import static android.content.Context.WINDOW_SERVICE;

/*
* A presentation used for hosting a single Android view in a virtual display.
*
* This presentation overrides the WindowManager's addView/removeView/updateViewLayout methods, such that views added
* directly to the WindowManager are added as part of the presentation's view hierarchy (to mFakeWindowRootView).
*
* The view hierarchy for the presentation is as following:
*
* mRootView
* / \
* / \
* / \
* mContainer mState.mFakeWindowRootView
* |
* EmbeddedView
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
class SingleViewPresentation extends Presentation {

/*
* When an embedded view is resized in Flutterverse we move the Android view to a new virtual display
* that has the new size. This class keeps the presentation state that moves with the view to the presentation of
* the new virtual display.
*/
static class PresentationState {
// The Android view we are embedding in the Flutter app.
private PlatformView mView;

// The InvocationHandler for a WindowManager proxy. This is essentially the custom window manager for the
// presentation.
private WindowManagerHandler mWindowManagerHandler;

// Contains views that were added directly to the window manager (e.g android.widget.PopupWindow).
private FakeWindowViewGroup mFakeWindowRootView;
}

private final PlatformViewFactory mViewFactory;

private PlatformView mView;
// This is the view id assigned by the Flutter framework to the embedded view, we keep it here
// so when we create the platform we can tell it its view id.
private int mViewId;

// As the root view of a display cannot be detached, we use this mContainer
// as the root, and attach mView to it. This allows us to detach mView.
// The root view for the presentation, it has 2 childs: mContainer which contains the embedded view, and
// mFakeWindowRootView which contains views that were added directly to the presentation's window manager.
private FrameLayout mRootView;

// Contains the embedded platform view (mView.getView()) when it is attached to the presentation.
private FrameLayout mContainer;

private PresentationState mState;

/**
* Creates a presentation that will use the view factory to create a new
* platform view in the presentation's onCreate, and attach it.
Expand All @@ -33,6 +78,7 @@ public SingleViewPresentation(Context outerContext, Display display, PlatformVie
super(outerContext, display);
mViewFactory = viewFactory;
mViewId = viewId;
mState = new PresentationState();
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
Expand All @@ -46,10 +92,10 @@ public SingleViewPresentation(Context outerContext, Display display, PlatformVie
* <p>The display's density must match the density of the context used
* when the view was created.
*/
public SingleViewPresentation(Context outerContext, Display display, PlatformView view) {
public SingleViewPresentation(Context outerContext, Display display, PresentationState state) {
super(outerContext, display);
mViewFactory = null;
mView = view;
mState = state;
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
Expand All @@ -59,22 +105,195 @@ public SingleViewPresentation(Context outerContext, Display display, PlatformVie
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (mView == null) {
mView = mViewFactory.create(getContext(), mViewId);
if (mState.mFakeWindowRootView == null) {
mState.mFakeWindowRootView = new FakeWindowViewGroup(getContext());
}
if (mState.mWindowManagerHandler == null) {
WindowManager windowManagerDelegate = (WindowManager) getContext().getSystemService(WINDOW_SERVICE);
mState.mWindowManagerHandler = new WindowManagerHandler(windowManagerDelegate, mState.mFakeWindowRootView);
}

mContainer = new FrameLayout(getContext());
mContainer.addView(mView.getView());
setContentView(mContainer);
PresentationContext context = new PresentationContext(getContext(), mState.mWindowManagerHandler);

if (mState.mView == null) {
mState.mView = mViewFactory.create(context, mViewId);
}

mContainer.addView(mState.mView.getView());
mRootView = new FrameLayout(getContext());
mRootView.addView(mContainer);
mRootView.addView(mState.mFakeWindowRootView);
setContentView(mRootView);
}

public PlatformView detachView() {
mContainer.removeView(mView.getView());
return mView;
public PresentationState detachState() {
mContainer.removeAllViews();
mRootView.removeAllViews();
return mState;
}

public View getView() {
if (mView == null)
public PlatformView getView() {
if (mState.mView == null)
return null;
return mView.getView();
return mState.mView;
}

/*
* A view group that implements the same layout protocol that exist between the WindowManager and its direct
* children.
*
* Currently only a subset of the protocol is supported (gravity, x, and y).
*/
static class FakeWindowViewGroup extends ViewGroup {
// Used in onLayout to keep the bounds of the current view.
// We keep it as a member to avoid object allocations during onLayout which are discouraged.
private final Rect mViewBounds;

// Used in onLayout to keep the bounds of the child views.
// We keep it as a member to avoid object allocations during onLayout which are discouraged.
private final Rect mChildRect;

public FakeWindowViewGroup(Context context) {
super(context);
mViewBounds = new Rect();
mChildRect = new Rect();
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for(int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
WindowManager.LayoutParams params = (WindowManager.LayoutParams) child.getLayoutParams();
mViewBounds.set(l, t, r, b);
Gravity.apply(params.gravity, child.getMeasuredWidth(), child.getMeasuredHeight(), mViewBounds, params.x,
params.y, mChildRect);
child.layout(mChildRect.left, mChildRect.top, mChildRect.right, mChildRect.bottom);
}
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
for(int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.measure(atMost(widthMeasureSpec), atMost(heightMeasureSpec));
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

private static int atMost(int measureSpec) {
return MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(measureSpec), MeasureSpec.AT_MOST);
}
}

/**
* Proxies a Context replacing the WindowManager with our custom instance.
*/
static class PresentationContext extends ContextWrapper {
private WindowManager mWindowManager;
private final WindowManagerHandler mWindowManagerHandler;

PresentationContext(Context base, WindowManagerHandler windowManagerHandler) {
super(base);
mWindowManagerHandler = windowManagerHandler;
}

@Override
public Object getSystemService(String name) {
if (WINDOW_SERVICE.equals(name)) {
return getWindowManager();
}
return super.getSystemService(name);
}

private WindowManager getWindowManager() {
if (mWindowManager == null) {
mWindowManager = mWindowManagerHandler.getWindowManager();
}
return mWindowManager;
}
}

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

private final WindowManager mDelegate;
FakeWindowViewGroup mFakeWindowRootView;

WindowManagerHandler(WindowManager delegate, FakeWindowViewGroup fakeWindowViewGroup) {
mDelegate = delegate;
mFakeWindowRootView = fakeWindowViewGroup;
}

public WindowManager getWindowManager() {
return (WindowManager) Proxy.newProxyInstance(
WindowManager.class.getClassLoader(),
new Class[] { WindowManager.class },
this
);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
switch (method.getName()) {
case "addView":
addView(args);
return null;
case "removeView":
removeView(args);
return null;
case "updateViewLayout":
updateViewLayout(args);
return null;
}
try {
return method.invoke(mDelegate, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}

private void addView(Object[] args) {
if (mFakeWindowRootView == null) {
Log.w(TAG, "Embedded view called addView while detached from presentation");
return;
}
View view = (View) args[0];
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1];
mFakeWindowRootView.addView(view, layoutParams);
}

private void removeView(Object[] args) {
if (mFakeWindowRootView == null) {
Log.w(TAG, "Embedded view called removeView while detached from presentation");
return;
}
View view = (View) args[0];
mFakeWindowRootView.removeView(view);
}

private void updateViewLayout(Object[] args) {
if (mFakeWindowRootView == null) {
Log.w(TAG, "Embedded view called updateViewLayout while detached from presentation");
return;
}
View view = (View) args[0];
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1];
mFakeWindowRootView.updateViewLayout(view, layoutParams);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ private VirtualDisplayController(
}

public void resize(final int width, final int height, final Runnable onNewSizeFrameAvailable) {
final PlatformView view = mPresentation.detachView();
final SingleViewPresentation.PresentationState presentationState = mPresentation.detachState();
// We detach the surface to prevent it being destroyed when releasing the vd.
//
// setSurface is only available starting API 20. We could support API 19 by re-creating a new
Expand Down Expand Up @@ -118,18 +118,21 @@ public void onDraw() {
public void onViewDetachedFromWindow(View v) {}
});

mPresentation = new SingleViewPresentation(mContext, mVirtualDisplay.getDisplay(), view);
mPresentation = new SingleViewPresentation(mContext, mVirtualDisplay.getDisplay(), presentationState);
mPresentation.show();
}

public void dispose() {
mPresentation.detachView().dispose();
PlatformView view = mPresentation.getView();
mPresentation.detachState();
view.dispose();
mVirtualDisplay.release();
}

public View getView() {
if (mPresentation == null)
return null;
return mPresentation.getView();
PlatformView platformView = mPresentation.getView();
return platformView.getView();
}
}