/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.widget.inline; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.content.Context; import android.graphics.PixelFormat; import android.graphics.PointF; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import java.lang.ref.WeakReference; import java.util.function.Consumer; /** * This class represents a view that holds opaque content from another app that you can inline in * your UI. * *

Since the content presented by this view is from another security domain,it is * shown on a remote surface preventing the host application from accessing that content. Also the * host application cannot interact with the inlined content by injecting touch events or clicking * programmatically. * *

This view can be overlaid by other windows, i.e. redressed, but if this is the case * the inlined UI would not be interactive. Sometimes this is desirable, e.g. animating transitions. * *

By default the surface backing this view is shown on top of the hosting window such * that the inlined content is interactive. However, you can temporarily move the surface under the * hosting window which could be useful in some cases, e.g. animating transitions. At this point the * inlined content will not be interactive and the touch events would be delivered to your app. * *

Instances of this class are created by the platform and can be programmatically attached to * your UI. Once the view is attached to the window, you may detach and reattach it to the window. * It should work seamlessly from the hosting process's point of view. */ public class InlineContentView extends ViewGroup { private static final String TAG = "InlineContentView"; private static final boolean DEBUG = false; /** * Callback for observing the lifecycle of the surface control that manipulates the backing * secure embedded UI surface. */ public interface SurfaceControlCallback { /** * Called when the backing surface is being created. * * @param surfaceControl The surface control to manipulate the surface. */ void onCreated(@NonNull SurfaceControl surfaceControl); /** * Called when the backing surface is being destroyed. * * @param surfaceControl The surface control to manipulate the surface. */ void onDestroyed(@NonNull SurfaceControl surfaceControl); } /** * Callback for sending an updated surface package in case the previous one is released * from the detached from window event, and for getting notified of such event. * * This is expected to be provided to the {@link InlineContentView} so it can get updates * from and send updates to the remote content (i.e. surface package) provider. * * @hide */ @TestApi public interface SurfacePackageUpdater { /** * Called when the previous surface package is released due to view being detached * from the window. */ void onSurfacePackageReleased(); /** * Called to request an updated surface package. * * @param consumer consumes the updated surface package. */ void getSurfacePackage(@NonNull Consumer consumer); } @NonNull private final SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() { @Override public void surfaceCreated(@NonNull SurfaceHolder holder) { final SurfaceControl surfaceControl = mSurfaceView.getSurfaceControl(); surfaceControl.addOnReparentListener(mOnReparentListener); mSurfaceControlCallback.onCreated(surfaceControl); } @Override public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { /* do nothing */ } @Override public void surfaceDestroyed(@NonNull SurfaceHolder holder) { final SurfaceControl surfaceControl = mSurfaceView.getSurfaceControl(); surfaceControl.removeOnReparentListener(mOnReparentListener); mSurfaceControlCallback.onDestroyed(surfaceControl); } }; @NonNull private final SurfaceControl.OnReparentListener mOnReparentListener = new SurfaceControl.OnReparentListener() { @Override public void onReparent(SurfaceControl.Transaction transaction, SurfaceControl parent) { final View parentSurfaceOwnerView = (parent != null) ? parent.getLocalOwnerView() : null; if (parentSurfaceOwnerView instanceof SurfaceView) { mParentSurfaceOwnerView = new WeakReference<>( (SurfaceView) parentSurfaceOwnerView); } else { mParentSurfaceOwnerView = null; } } }; @NonNull private final ViewTreeObserver.OnDrawListener mOnDrawListener = new ViewTreeObserver.OnDrawListener() { @Override public void onDraw() { computeParentPositionAndScale(); final int visibility = InlineContentView.this.isShown() ? VISIBLE : GONE; mSurfaceView.setVisibility(visibility); } }; @NonNull private final SurfaceView mSurfaceView; @Nullable private WeakReference mParentSurfaceOwnerView; @Nullable private int[] mParentPosition; @Nullable private PointF mParentScale; @Nullable private SurfaceControlCallback mSurfaceControlCallback; @Nullable private SurfacePackageUpdater mSurfacePackageUpdater; /** * @inheritDoc * @hide */ public InlineContentView(@NonNull Context context) { this(context, null); } /** * @inheritDoc * @hide */ public InlineContentView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } /** * @inheritDoc * @hide */ public InlineContentView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); mSurfaceView.setEnableSurfaceClipping(true); } /** * Gets the surface control. If the surface is not created this method returns {@code null}. * * @return The surface control. * @see #setSurfaceControlCallback(SurfaceControlCallback) */ @Nullable public SurfaceControl getSurfaceControl() { return mSurfaceView.getSurfaceControl(); } @Override public void setClipBounds(Rect clipBounds) { super.setClipBounds(clipBounds); mSurfaceView.setClipBounds(clipBounds); } /** * @inheritDoc * @hide */ public InlineContentView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mSurfaceView = new SurfaceView(context, attrs, defStyleAttr, defStyleRes) { // b/219807628 @Override protected void onSetSurfacePositionAndScale( @NonNull SurfaceControl.Transaction transaction, @NonNull SurfaceControl surface, int positionLeft, int positionTop, float postScaleX, float postScaleY) { // If we have a parent position, we need to make our coordinates relative // to the parent in the rendering space. if (mParentPosition != null) { positionLeft = (int) ((positionLeft - mParentPosition[0]) / mParentScale.x); positionTop = (int) ((positionTop - mParentPosition[1]) / mParentScale.y); } // Any scaling done to the parent or its predecessors would be applied // via the surfaces parent -> child relation, so we only propagate any // scaling set on the InlineContentView itself. postScaleX = InlineContentView.this.getScaleX(); postScaleY = InlineContentView.this.getScaleY(); super.onSetSurfacePositionAndScale(transaction, surface, positionLeft, positionTop, postScaleX, postScaleY); } }; mSurfaceView.setZOrderOnTop(true); mSurfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT); addView(mSurfaceView); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); } /** * Sets the embedded UI provider. * * @hide */ @TestApi public void setChildSurfacePackageUpdater( @Nullable SurfacePackageUpdater surfacePackageUpdater) { mSurfacePackageUpdater = surfacePackageUpdater; } @Override protected void onAttachedToWindow() { if (DEBUG) Log.v(TAG, "onAttachedToWindow"); super.onAttachedToWindow(); if (mSurfacePackageUpdater != null) { mSurfacePackageUpdater.getSurfacePackage( sp -> { if (DEBUG) Log.v(TAG, "Received new SurfacePackage"); if (getViewRootImpl() != null) { mSurfaceView.setChildSurfacePackage(sp); } }); } mSurfaceView.setVisibility(getVisibility()); getViewTreeObserver().addOnDrawListener(mOnDrawListener); } @Override protected void onDetachedFromWindow() { if (DEBUG) Log.v(TAG, "onDetachedFromWindow"); super.onDetachedFromWindow(); if (mSurfacePackageUpdater != null) { mSurfacePackageUpdater.onSurfacePackageReleased(); } getViewTreeObserver().removeOnDrawListener(mOnDrawListener); mSurfaceView.setVisibility(View.GONE); } @Override public void onLayout(boolean changed, int l, int t, int r, int b) { mSurfaceView.layout(0, 0, getMeasuredWidth(), getMeasuredHeight()); } /** * Sets a callback to observe the lifecycle of the surface control for managing the backing * surface. * * @param callback The callback to set or {@code null} to clear. */ public void setSurfaceControlCallback(@Nullable SurfaceControlCallback callback) { if (mSurfaceControlCallback != null) { mSurfaceView.getHolder().removeCallback(mSurfaceCallback); } mSurfaceControlCallback = callback; if (mSurfaceControlCallback != null) { mSurfaceView.getHolder().addCallback(mSurfaceCallback); } } /** * @return Whether the surface backing this view appears on top of its parent. * @see #setZOrderedOnTop(boolean) */ public boolean isZOrderedOnTop() { return mSurfaceView.isZOrderedOnTop(); } /** * Controls whether the backing surface is placed on top of this view's window. Normally, it is * placed on top of the window, to allow interaction with the inlined UI. Via this method, you * can place the surface below the window. This means that all of the contents of the window * this view is in will be visible on top of its surface. * *

The Z ordering can be changed dynamically if the backing surface is * created, otherwise the ordering would be applied at surface construction time. * * @param onTop Whether to show the surface on top of this view's window. * @see #isZOrderedOnTop() */ public boolean setZOrderedOnTop(boolean onTop) { return mSurfaceView.setZOrderedOnTop(onTop, /*allowDynamicChange*/ true); } private void computeParentPositionAndScale() { boolean contentPositionOrScaleChanged = false; // This method can be called on the UI or render thread but for the cases // it is called these threads are not running concurrently, so no need to lock. final SurfaceView parentSurfaceOwnerView = (mParentSurfaceOwnerView != null) ? mParentSurfaceOwnerView.get() : null; if (parentSurfaceOwnerView != null) { if (mParentPosition == null) { mParentPosition = new int[2]; } final int oldParentPositionX = mParentPosition[0]; final int oldParentPositionY = mParentPosition[1]; parentSurfaceOwnerView.getLocationInSurface(mParentPosition); if (oldParentPositionX != mParentPosition[0] || oldParentPositionY != mParentPosition[1]) { contentPositionOrScaleChanged = true; } if (mParentScale == null) { mParentScale = new PointF(); } final float lastParentSurfaceWidth = parentSurfaceOwnerView .getSurfaceRenderPosition().width(); final float oldParentScaleX = mParentScale.x; if (lastParentSurfaceWidth > 0) { mParentScale.x = lastParentSurfaceWidth / (float) parentSurfaceOwnerView.getWidth(); } else { mParentScale.x = 1.0f; } if (!contentPositionOrScaleChanged && Float.compare(oldParentScaleX, mParentScale.x) != 0) { contentPositionOrScaleChanged = true; } final float lastParentSurfaceHeight = parentSurfaceOwnerView .getSurfaceRenderPosition().height(); final float oldParentScaleY = mParentScale.y; if (lastParentSurfaceHeight > 0) { mParentScale.y = lastParentSurfaceHeight / (float) parentSurfaceOwnerView.getHeight(); } else { mParentScale.y = 1.0f; } if (!contentPositionOrScaleChanged && Float.compare(oldParentScaleY, mParentScale.y) != 0) { contentPositionOrScaleChanged = true; } } else if (mParentPosition != null || mParentScale != null) { contentPositionOrScaleChanged = true; mParentPosition = null; mParentScale = null; } if (contentPositionOrScaleChanged) { mSurfaceView.requestUpdateSurfacePositionAndScale(); } } }