| /* |
| * Copyright (C) 2019 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.view; |
| |
| import android.annotation.FlaggedApi; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.TestApi; |
| import android.content.Context; |
| import android.content.res.Configuration; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.os.IBinder; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.os.RemoteException; |
| import android.util.Log; |
| import android.view.accessibility.IAccessibilityEmbeddedConnection; |
| import android.window.ISurfaceSyncGroup; |
| import android.window.InputTransferToken; |
| import android.window.WindowTokenClient; |
| |
| import com.android.window.flags.Flags; |
| |
| import dalvik.system.CloseGuard; |
| |
| import java.util.Objects; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| |
| /** |
| * Utility class for adding a View hierarchy to a {@link SurfaceControl}. The View hierarchy |
| * will render in to a root SurfaceControl, and receive input based on the SurfaceControl's |
| * placement on-screen. The primary usage of this class is to embed a View hierarchy from |
| * one process in to another. After the SurfaceControlViewHost has been set up in the embedded |
| * content provider, we can send the {@link SurfaceControlViewHost.SurfacePackage} |
| * to the host process. The host process can then attach the hierarchy to a SurfaceView within |
| * its own by calling |
| * {@link SurfaceView#setChildSurfacePackage}. |
| */ |
| public class SurfaceControlViewHost { |
| private final static String TAG = "SurfaceControlViewHost"; |
| private final ViewRootImpl mViewRoot; |
| private final CloseGuard mCloseGuard = CloseGuard.get(); |
| private final WindowlessWindowManager mWm; |
| |
| private SurfaceControl mSurfaceControl; |
| private IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection; |
| private boolean mReleased = false; |
| |
| private final class ISurfaceControlViewHostImpl extends ISurfaceControlViewHost.Stub { |
| @Override |
| public void onConfigurationChanged(Configuration configuration) { |
| if (mViewRoot == null) { |
| return; |
| } |
| mViewRoot.mHandler.post(() -> { |
| mWm.setConfiguration(configuration); |
| if (mViewRoot != null) { |
| mViewRoot.forceWmRelayout(); |
| } |
| }); |
| } |
| |
| @Override |
| public void onDispatchDetachedFromWindow() { |
| if (mViewRoot == null) { |
| return; |
| } |
| mViewRoot.mHandler.post(() -> { |
| release(); |
| }); |
| } |
| |
| @Override |
| public void onInsetsChanged(InsetsState state, Rect frame) { |
| if (mViewRoot != null) { |
| mViewRoot.mHandler.post(() -> { |
| mViewRoot.setOverrideInsetsFrame(frame); |
| }); |
| } |
| mWm.setInsetsState(state); |
| } |
| |
| @Override |
| public ISurfaceSyncGroup getSurfaceSyncGroup() { |
| CompletableFuture<ISurfaceSyncGroup> surfaceSyncGroup = new CompletableFuture<>(); |
| // If the call came from in process and it's already running on the UI thread, return |
| // results immediately instead of posting to the main thread. If we post to the main |
| // thread, it will block itself and the return value will always be null. |
| if (Thread.currentThread() == mViewRoot.mThread) { |
| return mViewRoot.getOrCreateSurfaceSyncGroup().mISurfaceSyncGroup; |
| } else { |
| mViewRoot.mHandler.post( |
| () -> surfaceSyncGroup.complete( |
| mViewRoot.getOrCreateSurfaceSyncGroup().mISurfaceSyncGroup)); |
| } |
| try { |
| return surfaceSyncGroup.get(1, TimeUnit.SECONDS); |
| } catch (InterruptedException | ExecutionException | TimeoutException e) { |
| Log.e(TAG, "Failed to get SurfaceSyncGroup for SCVH", e); |
| } |
| return null; |
| } |
| |
| @Override |
| public void attachParentInterface(@Nullable ISurfaceControlViewHostParent parentInterface) { |
| mViewRoot.mHandler.post(() -> mWm.setParentInterface(parentInterface)); |
| } |
| } |
| |
| private ISurfaceControlViewHost mRemoteInterface = new ISurfaceControlViewHostImpl(); |
| |
| private ViewRootImpl.ConfigChangedCallback mConfigChangedCallback; |
| |
| /** |
| * Package encapsulating a Surface hierarchy which contains interactive view |
| * elements. It's expected to get this object from |
| * {@link SurfaceControlViewHost#getSurfacePackage} afterwards it can be embedded within |
| * a SurfaceView by calling {@link SurfaceView#setChildSurfacePackage}. |
| * |
| * Note that each {@link SurfacePackage} must be released by calling |
| * {@link SurfacePackage#release}. However, if you use the recommended flow, |
| * the framework will automatically handle the lifetime for you. |
| * |
| * 1. When sending the package to the remote process, return it from an AIDL method |
| * or manually use FLAG_WRITE_RETURN_VALUE in writeToParcel. This will automatically |
| * release the package in the local process. |
| * 2. In the remote process, consume the package using SurfaceView. This way the |
| * SurfaceView will take over the lifetime and call {@link SurfacePackage#release} |
| * for the user. |
| * |
| * One final note: The {@link SurfacePackage} lifetime is totally de-coupled |
| * from the lifetime of the underlying {@link SurfaceControlViewHost}. Regardless |
| * of the lifetime of the package the user should still call |
| * {@link SurfaceControlViewHost#release} when finished. |
| */ |
| public static final class SurfacePackage implements Parcelable { |
| private SurfaceControl mSurfaceControl; |
| private final IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection; |
| private final InputTransferToken mInputTransferToken; |
| @NonNull |
| private final ISurfaceControlViewHost mRemoteInterface; |
| |
| SurfacePackage(SurfaceControl sc, IAccessibilityEmbeddedConnection connection, |
| InputTransferToken inputTransferToken, |
| @NonNull ISurfaceControlViewHost ri) { |
| mSurfaceControl = sc; |
| mAccessibilityEmbeddedConnection = connection; |
| mInputTransferToken = inputTransferToken; |
| mRemoteInterface = ri; |
| } |
| |
| /** |
| * Constructs a copy of {@code SurfacePackage} with an independent lifetime. |
| * |
| * The caller can use this to create an independent copy in situations where ownership of |
| * the {@code SurfacePackage} would be transferred elsewhere, such as attaching to a |
| * {@code SurfaceView}, returning as {@code Binder} result value, etc. The caller is |
| * responsible for releasing this copy when its done. |
| * |
| * @param other {@code SurfacePackage} to create a copy of. |
| */ |
| public SurfacePackage(@NonNull SurfacePackage other) { |
| SurfaceControl otherSurfaceControl = other.mSurfaceControl; |
| if (otherSurfaceControl != null && otherSurfaceControl.isValid()) { |
| mSurfaceControl = new SurfaceControl(otherSurfaceControl, "SurfacePackage"); |
| } |
| mAccessibilityEmbeddedConnection = other.mAccessibilityEmbeddedConnection; |
| mInputTransferToken = other.mInputTransferToken; |
| mRemoteInterface = other.mRemoteInterface; |
| } |
| |
| private SurfacePackage(Parcel in) { |
| mSurfaceControl = new SurfaceControl(); |
| mSurfaceControl.readFromParcel(in); |
| mSurfaceControl.setUnreleasedWarningCallSite("SurfacePackage(Parcel)"); |
| mAccessibilityEmbeddedConnection = IAccessibilityEmbeddedConnection.Stub.asInterface( |
| in.readStrongBinder()); |
| mInputTransferToken = InputTransferToken.CREATOR.createFromParcel(in); |
| mRemoteInterface = ISurfaceControlViewHost.Stub.asInterface(in.readStrongBinder()); |
| } |
| |
| /** |
| * Returns the {@link android.view.SurfaceControl} associated with this SurfacePackage for |
| * cases where more control is required. |
| * |
| * @return the SurfaceControl associated with this SurfacePackage and its containing |
| * SurfaceControlViewHost |
| */ |
| public @NonNull SurfaceControl getSurfaceControl() { |
| return mSurfaceControl; |
| } |
| |
| /** |
| * Gets an accessibility embedded connection interface for this SurfaceControlViewHost. |
| * |
| * @return {@link IAccessibilityEmbeddedConnection} interface. |
| * @hide |
| */ |
| public IAccessibilityEmbeddedConnection getAccessibilityEmbeddedConnection() { |
| return mAccessibilityEmbeddedConnection; |
| } |
| |
| /** |
| * @hide |
| */ |
| @NonNull |
| public ISurfaceControlViewHost getRemoteInterface() { |
| return mRemoteInterface; |
| } |
| |
| /** |
| * Forward a configuration to the remote SurfaceControlViewHost. |
| * This will cause View#onConfigurationChanged to be invoked on the remote |
| * end. This does not automatically cause the SurfaceControlViewHost |
| * to be resized. The root View of a SurfaceControlViewHost |
| * is more akin to a PopupWindow in that the size is user specified |
| * independent of configuration width and height. |
| * |
| * In order to receive the configuration change via |
| * {@link View#onConfigurationChanged}, the context used with the |
| * SurfaceControlViewHost and it's embedded view hierarchy must |
| * be a WindowContext obtained from {@link Context#createWindowContext}. |
| * |
| * If a regular service context is used, then your embedded view hierarchy |
| * will always perceive the global configuration. |
| * |
| * @param c The configuration to forward |
| */ |
| public void notifyConfigurationChanged(@NonNull Configuration c) { |
| try { |
| getRemoteInterface().onConfigurationChanged(c); |
| } catch (RemoteException e) { |
| e.rethrowAsRuntimeException(); |
| } |
| } |
| |
| /** |
| * Tear down the remote SurfaceControlViewHost and cause |
| * View#onDetachedFromWindow to be invoked on the other side. |
| */ |
| public void notifyDetachedFromWindow() { |
| try { |
| getRemoteInterface().onDispatchDetachedFromWindow(); |
| } catch (RemoteException e) { |
| e.rethrowAsRuntimeException(); |
| } |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(@NonNull Parcel out, int flags) { |
| mSurfaceControl.writeToParcel(out, flags); |
| out.writeStrongBinder(mAccessibilityEmbeddedConnection.asBinder()); |
| mInputTransferToken.writeToParcel(out, flags); |
| out.writeStrongBinder(mRemoteInterface.asBinder()); |
| } |
| |
| /** |
| * Release the {@link SurfaceControl} associated with this package. |
| * It's not necessary to call this if you pass the package to |
| * {@link SurfaceView#setChildSurfacePackage} as {@link SurfaceView} will |
| * take ownership in that case. |
| */ |
| public void release() { |
| if (mSurfaceControl != null) { |
| mSurfaceControl.release(); |
| } |
| mSurfaceControl = null; |
| } |
| |
| /** |
| * Gets an {@link InputTransferToken} which can be used to request focus on the embedded |
| * surface or to transfer touch gesture to the embedded surface. |
| * |
| * @return the InputTransferToken associated with {@link SurfacePackage} or {@code null} if |
| * the embedded hasn't set up its view or doesn't have input. |
| * @see WindowManager#transferTouchGesture(InputTransferToken, InputTransferToken) |
| */ |
| @Nullable |
| @FlaggedApi(Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER) |
| public InputTransferToken getInputTransferToken() { |
| return mInputTransferToken; |
| } |
| |
| @Override |
| public String toString() { |
| return "{inputTransferToken=" + getInputTransferToken() + " remoteInterface=" |
| + getRemoteInterface() + "}"; |
| } |
| |
| public static final @NonNull Creator<SurfacePackage> CREATOR |
| = new Creator<SurfacePackage>() { |
| public SurfacePackage createFromParcel(Parcel in) { |
| return new SurfacePackage(in); |
| } |
| public SurfacePackage[] newArray(int size) { |
| return new SurfacePackage[size]; |
| } |
| }; |
| } |
| |
| /** @hide */ |
| public SurfaceControlViewHost(@NonNull Context c, @NonNull Display d, |
| @NonNull WindowlessWindowManager wwm, @NonNull String callsite) { |
| mSurfaceControl = wwm.mRootSurface; |
| mWm = wwm; |
| mViewRoot = new ViewRootImpl(c, d, mWm, new WindowlessWindowLayout()); |
| mCloseGuard.openWithCallSite("release", callsite); |
| setConfigCallback(c, d); |
| |
| WindowManagerGlobal.getInstance().addWindowlessRoot(mViewRoot); |
| |
| mAccessibilityEmbeddedConnection = mViewRoot.getAccessibilityEmbeddedConnection(); |
| } |
| |
| /** |
| * Construct a new SurfaceControlViewHost. The root Surface will be |
| * allocated internally and is accessible via getSurfacePackage(). |
| * |
| * The {@param hostToken} parameter, primarily used for ANR reporting, |
| * must be obtained from whomever will be hosting the embedded hierarchy. |
| * It's accessible from {@link SurfaceView#getHostToken}. |
| * |
| * @param context The Context object for your activity or application. |
| * @param display The Display the hierarchy will be placed on. |
| * @param hostToken The host token, as discussed above. |
| */ |
| public SurfaceControlViewHost(@NonNull Context context, @NonNull Display display, |
| @Nullable IBinder hostToken) { |
| this(context, display, hostToken == null ? null : new InputTransferToken(hostToken), |
| "untracked"); |
| |
| } |
| |
| /** |
| * Construct a new SurfaceControlViewHost. The root Surface will be |
| * allocated internally and is accessible via getSurfacePackage(). |
| * <p> |
| * The hostInputTransferToken parameter allows the host and embedded to be associated with |
| * each other to allow transferring touch gesture and focus. This is also used for ANR |
| * reporting. It's accessible from {@link AttachedSurfaceControl#getInputTransferToken()}. |
| * |
| * @param context The Context object for your activity or application. |
| * @param display The Display the hierarchy will be placed on. |
| * @param hostInputTransferToken The host input transfer token, as discussed above. |
| */ |
| @FlaggedApi(Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER) |
| public SurfaceControlViewHost(@NonNull Context context, @NonNull Display display, |
| @Nullable InputTransferToken hostInputTransferToken) { |
| this(context, display, hostInputTransferToken, "untracked"); |
| } |
| |
| /** |
| * Construct a new SurfaceControlViewHost. The root Surface will be |
| * allocated internally and is accessible via getSurfacePackage(). |
| * |
| * The {@param hostToken} parameter, primarily used for ANR reporting, |
| * must be obtained from whomever will be hosting the embedded hierarchy. |
| * It's accessible from {@link SurfaceView#getHostToken}. |
| * |
| * @param context The Context object for your activity or application. |
| * @param display The Display the hierarchy will be placed on. |
| * @param hostToken The host token, as discussed above. |
| * @param callsite The call site, used for tracking leakage of the host |
| * @hide |
| */ |
| public SurfaceControlViewHost(@NonNull Context context, @NonNull Display display, |
| @Nullable InputTransferToken hostToken, @NonNull String callsite) { |
| mSurfaceControl = new SurfaceControl.Builder() |
| .setContainerLayer() |
| .setName("SurfaceControlViewHost") |
| .setCallsite("SurfaceControlViewHost[" + callsite + "]") |
| .build(); |
| mWm = new WindowlessWindowManager(context.getResources().getConfiguration(), |
| mSurfaceControl, hostToken); |
| |
| mViewRoot = new ViewRootImpl(context, display, mWm, new WindowlessWindowLayout()); |
| mCloseGuard.openWithCallSite("release", callsite); |
| setConfigCallback(context, display); |
| |
| WindowManagerGlobal.getInstance().addWindowlessRoot(mViewRoot); |
| |
| mAccessibilityEmbeddedConnection = mViewRoot.getAccessibilityEmbeddedConnection(); |
| } |
| |
| private void setConfigCallback(Context c, Display d) { |
| final IBinder token = c.getWindowContextToken(); |
| mConfigChangedCallback = conf -> { |
| if (token instanceof WindowTokenClient) { |
| final WindowTokenClient w = (WindowTokenClient) token; |
| w.onConfigurationChanged(conf, d.getDisplayId(), true); |
| } |
| }; |
| |
| ViewRootImpl.addConfigCallback(mConfigChangedCallback); |
| } |
| |
| /** |
| * @hide |
| */ |
| @Override |
| protected void finalize() throws Throwable { |
| if (mReleased) { |
| return; |
| } |
| if (mCloseGuard != null) { |
| mCloseGuard.warnIfOpen(); |
| } |
| // We aren't on the UI thread here so we need to pass false to doDie |
| doRelease(false /* immediate */); |
| } |
| |
| /** |
| * Return a SurfacePackage for the root SurfaceControl of the embedded hierarchy. |
| * Rather than be directly reparented using {@link SurfaceControl.Transaction} this |
| * SurfacePackage should be passed to {@link SurfaceView#setChildSurfacePackage} |
| * which will not only reparent the Surface, but ensure the accessibility hierarchies |
| * are linked. |
| */ |
| public @Nullable SurfacePackage getSurfacePackage() { |
| if (mSurfaceControl != null && mAccessibilityEmbeddedConnection != null) { |
| return new SurfacePackage(new SurfaceControl(mSurfaceControl, "getSurfacePackage"), |
| mAccessibilityEmbeddedConnection, getInputTransferToken(), mRemoteInterface); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| public @NonNull AttachedSurfaceControl getRootSurfaceControl() { |
| return mViewRoot; |
| } |
| |
| /** |
| * Set the root view of the SurfaceControlViewHost. This view will render in to |
| * the SurfaceControl, and receive input based on the SurfaceControls positioning on |
| * screen. It will be laid as if it were in a window of the passed in width and height. |
| * |
| * @param view The View to add |
| * @param width The width to layout the View within, in pixels. |
| * @param height The height to layout the View within, in pixels. |
| */ |
| public void setView(@NonNull View view, int width, int height) { |
| final WindowManager.LayoutParams lp = |
| new WindowManager.LayoutParams(width, height, |
| WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.TRANSPARENT); |
| setView(view, lp); |
| } |
| |
| /** |
| * @hide |
| */ |
| @TestApi |
| public void setView(@NonNull View view, @NonNull WindowManager.LayoutParams attrs) { |
| Objects.requireNonNull(view); |
| attrs.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; |
| addWindowToken(attrs); |
| view.setLayoutParams(attrs); |
| mViewRoot.setView(view, attrs, null); |
| mViewRoot.setBackKeyCallbackForWindowlessWindow(mWm::forwardBackKeyToParent); |
| } |
| |
| /** |
| * @return The view passed to setView, or null if none has been passed. |
| */ |
| public @Nullable View getView() { |
| return mViewRoot.getView(); |
| } |
| |
| /** |
| * @return the ViewRootImpl wrapped by this host. |
| * @hide |
| */ |
| public IWindow getWindowToken() { |
| return mViewRoot.mWindow; |
| } |
| |
| /** |
| * @return the WindowlessWindowManager instance that this host is attached to. |
| * @hide |
| */ |
| public @NonNull WindowlessWindowManager getWindowlessWM() { |
| return mWm; |
| } |
| |
| /** |
| * Forces relayout and draw and allows to set a custom callback when it is finished |
| * @hide |
| */ |
| public void relayout(WindowManager.LayoutParams attrs, |
| WindowlessWindowManager.ResizeCompleteCallback callback) { |
| mViewRoot.setLayoutParams(attrs, false); |
| mViewRoot.setReportNextDraw(true /* syncBuffer */, "scvh_relayout"); |
| mWm.setCompletionCallback(mViewRoot.mWindow.asBinder(), callback); |
| } |
| |
| /** |
| * @hide |
| */ |
| @TestApi |
| public void relayout(WindowManager.LayoutParams attrs) { |
| mViewRoot.setLayoutParams(attrs, false); |
| } |
| |
| /** |
| * Modify the size of the root view. |
| * |
| * @param width Width in pixels |
| * @param height Height in pixels |
| */ |
| public void relayout(int width, int height) { |
| final WindowManager.LayoutParams lp = |
| new WindowManager.LayoutParams(width, height, |
| WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.TRANSPARENT); |
| relayout(lp); |
| } |
| |
| /** |
| * Trigger the tear down of the embedded view hierarchy and release the SurfaceControl. |
| * This will result in onDispatchedFromWindow being dispatched to the embedded view hierarchy |
| * and render the object unusable. |
| */ |
| public void release() { |
| // ViewRoot will release mSurfaceControl for us. |
| doRelease(true /* immediate */); |
| } |
| |
| private void doRelease(boolean immediate) { |
| if (mConfigChangedCallback != null) { |
| ViewRootImpl.removeConfigCallback(mConfigChangedCallback); |
| mConfigChangedCallback = null; |
| } |
| |
| mViewRoot.die(immediate); |
| WindowManagerGlobal.getInstance().removeWindowlessRoot(mViewRoot); |
| mReleased = true; |
| mCloseGuard.close(); |
| } |
| |
| /** |
| * Returns an input token used which can be used to request focus on the embedded surface |
| * or to transfer touch gesture to the embedded surface. |
| * |
| * @hide |
| */ |
| public InputTransferToken getInputTransferToken() { |
| return mWm.getInputTransferToken(getWindowToken().asBinder()); |
| } |
| |
| private void addWindowToken(WindowManager.LayoutParams attrs) { |
| final WindowManager wm = |
| (WindowManager) mViewRoot.mContext.getSystemService(Context.WINDOW_SERVICE); |
| attrs.token = wm.getDefaultToken(); |
| } |
| |
| /** |
| * Transfer the currently in progress touch gesture to the parent (if any) of this |
| * SurfaceControlViewHost. This requires that the SurfaceControlViewHost was created with an |
| * associated host {@link InputTransferToken}. |
| * |
| * @return Whether the touch stream was transferred. |
| * @deprecated Use {@link WindowManager#transferTouchGesture(InputTransferToken, |
| * InputTransferToken)} instead. |
| */ |
| @Deprecated |
| public boolean transferTouchGestureToHost() { |
| if (mViewRoot == null) { |
| return false; |
| } |
| final WindowManager wm = (WindowManager) mViewRoot.mContext.getSystemService( |
| Context.WINDOW_SERVICE); |
| InputTransferToken embeddedToken = getInputTransferToken(); |
| InputTransferToken hostToken = mWm.mHostInputTransferToken; |
| if (embeddedToken == null || hostToken == null) { |
| Log.w(TAG, "Failed to transferTouchGestureToHost. Host or embedded token is null"); |
| return false; |
| } |
| return wm.transferTouchGesture(getInputTransferToken(), mWm.mHostInputTransferToken); |
| } |
| } |