diff options
author | 2020-03-18 23:30:16 -0700 | |
---|---|---|
committer | 2020-03-20 12:29:07 -0700 | |
commit | a57dadde241a77f390c79b845a6990a9514d8fc8 (patch) | |
tree | 67af938f5e9f7ac84efb7c39ac897a61d0b0065d | |
parent | d9773f2ca654fd8ecc3cb88aea31fdf59c45290f (diff) |
Add APIs to move suggestions below/above window
Based on feedback from developers (GBoard) there are cases
where they want the app UI to cover the suggestions during
animations (keypress popup should cover the suggestion area).
This change adds a dedicated InlineContentView that is
returned when a suggestion is inflated. This view has APIs
to dynamically move its surface above/below the host window.
Also the new InlineContentView has no public constructors
as these are always created by the system via other APIs.
Finally, the InlineContentView only exposes the surface
control of the inlined UI which is useful for reparenting
to achieve clipping of multiple such views in a given area
on the screen.
When the content surface is below the app window it is not
be interactive and all touches go to the hosting app. In this
state the app can draw on top of the suggestions. When the
content surface is above the app window it is interactive
and the hosting app cannot render on top of it.
While at this this also fixes the case where a surface can
cover the suggestion surface even if it was on top of the
app window. Now if the embedded content surface is covered,
even partially, by another one the embedded UI is not
interactive.
bug:15140337
Test: atest AutofillTestCases
Change-Id: If1db185506ae6916b9d655ab647dd59b626cf61e
-rw-r--r-- | api/current.txt | 15 | ||||
-rw-r--r-- | core/java/android/service/autofill/InlineSuggestionRoot.java | 4 | ||||
-rw-r--r-- | core/java/android/view/SurfaceView.java | 97 | ||||
-rw-r--r-- | core/java/android/view/inline/InlineContentView.java | 187 | ||||
-rw-r--r-- | core/java/android/view/inputmethod/InlineSuggestion.java | 24 |
5 files changed, 300 insertions, 27 deletions
diff --git a/api/current.txt b/api/current.txt index 6bc59e1cb8b4..ceed0ef2cc06 100644 --- a/api/current.txt +++ b/api/current.txt @@ -56793,6 +56793,19 @@ package android.view.contentcapture { package android.view.inline { + public class InlineContentView extends android.view.ViewGroup { + method @Nullable public android.view.SurfaceControl getSurfaceControl(); + method public boolean isZOrderedOnTop(); + method public void onLayout(boolean, int, int, int, int); + method public void setSurfaceControlCallback(@Nullable android.view.inline.InlineContentView.SurfaceControlCallback); + method public boolean setZOrderedOnTop(boolean); + } + + public static interface InlineContentView.SurfaceControlCallback { + method public void onCreated(@NonNull android.view.SurfaceControl); + method public void onDestroyed(@NonNull android.view.SurfaceControl); + } + public final class InlinePresentationSpec implements android.os.Parcelable { method public int describeContents(); method @NonNull public android.util.Size getMaxSize(); @@ -56981,7 +56994,7 @@ package android.view.inputmethod { public final class InlineSuggestion implements android.os.Parcelable { method public int describeContents(); method @NonNull public android.view.inputmethod.InlineSuggestionInfo getInfo(); - method public void inflate(@NonNull android.content.Context, @NonNull android.util.Size, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.View>); + method public void inflate(@NonNull android.content.Context, @NonNull android.util.Size, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.inline.InlineContentView>); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.InlineSuggestion> CREATOR; } diff --git a/core/java/android/service/autofill/InlineSuggestionRoot.java b/core/java/android/service/autofill/InlineSuggestionRoot.java index 6c9d36b329ff..653e513f13d8 100644 --- a/core/java/android/service/autofill/InlineSuggestionRoot.java +++ b/core/java/android/service/autofill/InlineSuggestionRoot.java @@ -68,7 +68,9 @@ public class InlineSuggestionRoot extends FrameLayout { case MotionEvent.ACTION_MOVE: { final float distance = MathUtils.dist(mDownX, mDownY, event.getX(), event.getY()); - if (distance > mTouchSlop) { + final boolean isSecure = (event.getFlags() + & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) == 0; + if (!isSecure || distance > mTouchSlop) { try { mCallback.onTransferTouchFocusToImeWindow(getViewRootImpl().getInputToken(), getContext().getDisplayId()); diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 3e1e3939d570..59fc6e9b5ede 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -686,16 +686,107 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall * SurfaceView is in will be visible on top of its surface. * * <p>Note that this must be set before the surface view's containing - * window is attached to the window manager. + * window is attached to the window manager. If you target {@link Build.VERSION_CODES#R} + * the Z ordering can be changed dynamically if the backing surface is + * created, otherwise it would be applied at surface construction time. * * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. + * + * @param onTop Whether to show the surface on top of this view's window. */ public void setZOrderOnTop(boolean onTop) { + // In R and above we allow dynamic layer changes. + final boolean allowDynamicChange = getContext().getApplicationInfo().targetSdkVersion + > Build.VERSION_CODES.Q; + setZOrderedOnTop(onTop, allowDynamicChange); + } + + /** + * @return Whether the surface backing this view appears on top of its parent. + * + * @hide + */ + public boolean isZOrderedOnTop() { + return mSubLayer > 0; + } + + /** + * Controls whether the surface view's surface is placed on top of its + * window. Normally it is placed behind the window, to allow it to + * (for the most part) appear to composite with the views in the + * hierarchy. By setting this, you cause it to be placed above the + * window. This means that none of the contents of the window this + * SurfaceView is in will be visible on top of its surface. + * + * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. + * + * @param onTop Whether to show the surface on top of this view's window. + * @param allowDynamicChange Whether this can happen after the surface is created. + * @return Whether the Z ordering changed. + * + * @hide + */ + public boolean setZOrderedOnTop(boolean onTop, boolean allowDynamicChange) { + final int subLayer; if (onTop) { - mSubLayer = APPLICATION_PANEL_SUBLAYER; + subLayer = APPLICATION_PANEL_SUBLAYER; } else { - mSubLayer = APPLICATION_MEDIA_SUBLAYER; + subLayer = APPLICATION_MEDIA_SUBLAYER; + } + if (mSubLayer == subLayer) { + return false; + } + mSubLayer = subLayer; + + if (!allowDynamicChange) { + return false; + } + if (mSurfaceControl == null) { + return true; + } + final ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot == null) { + return true; } + final Surface parent = viewRoot.mSurface; + if (parent == null || !parent.isValid()) { + return true; + } + + /* + * Schedule a callback that reflects an alpha value onto the underlying surfaces. + * This gets called on a RenderThread worker thread, so members accessed here must + * be protected by a lock. + */ + final boolean useBLAST = viewRoot.useBLAST(); + viewRoot.registerRtFrameCallback(frame -> { + try { + final SurfaceControl.Transaction t = useBLAST + ? viewRoot.getBLASTSyncTransaction() + : new SurfaceControl.Transaction(); + synchronized (mSurfaceControlLock) { + if (!parent.isValid() || mSurfaceControl == null) { + return; + } + updateRelativeZ(t); + if (!useBLAST) { + t.deferTransactionUntil(mSurfaceControl, + viewRoot.getRenderSurfaceControl(), frame); + } + } + // It's possible that mSurfaceControl is released in the UI thread before + // the transaction completes. If that happens, an exception is thrown, which + // must be caught immediately. + t.apply(); + } catch (Exception e) { + Log.e(TAG, System.identityHashCode(this) + + "setZOrderOnTop RT: Exception during surface transaction", e); + } + }); + + invalidate(); + + return true; } /** diff --git a/core/java/android/view/inline/InlineContentView.java b/core/java/android/view/inline/InlineContentView.java index 2a8ca0b6a354..df5bc2fb0cb4 100644 --- a/core/java/android/view/inline/InlineContentView.java +++ b/core/java/android/view/inline/InlineContentView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -17,22 +17,189 @@ package android.view.inline; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.graphics.PixelFormat; +import android.util.AttributeSet; +import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; +import android.view.SurfaceHolder; import android.view.SurfaceView; +import android.view.ViewGroup; /** - * This class represents a view that can hold an opaque content that may be from a different source. + * This class represents a view that holds opaque content from another app that + * you can inline in your UI. * - * @hide + * <p>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. + * + * <p>This view can be overlaid by other windows, i.e. redressed, but if this is the case + * the inined UI would not be interactive. Sometimes this is desirable, e.g. animating + * transitions. + * + * <p>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. */ -public class InlineContentView extends SurfaceView { - public InlineContentView(@NonNull Context context, - @NonNull SurfaceControlViewHost.SurfacePackage surfacePackage) { - super(context); - setZOrderOnTop(true); - setChildSurfacePackage(surfacePackage); - getHolder().setFormat(PixelFormat.TRANSPARENT); +public class InlineContentView extends ViewGroup { + + /** + * 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); + } + + private final @NonNull SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(@NonNull SurfaceHolder holder) { + mSurfaceControlCallback.onCreated(mSurfaceView.getSurfaceControl()); + } + + @Override + public void surfaceChanged(@NonNull SurfaceHolder holder, + int format, int width, int height) { + /* do nothing */ + } + + @Override + public void surfaceDestroyed(@NonNull SurfaceHolder holder) { + mSurfaceControlCallback.onDestroyed(mSurfaceView.getSurfaceControl()); + } + }; + + private final @NonNull SurfaceView mSurfaceView; + + private @Nullable SurfaceControlCallback mSurfaceControlCallback; + + /** + * @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); + } + + /** + * Gets the surface control. If the surface is not created this method + * returns {@code null}. + * + * @return The surface control. + * + * @see #setSurfaceControlCallback(SurfaceControlCallback) + */ + public @Nullable SurfaceControl getSurfaceControl() { + return mSurfaceView.getSurfaceControl(); + } + + /** + * @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); + mSurfaceView.setZOrderOnTop(true); + mSurfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT); + addView(mSurfaceView); + } + + /** + * Sets the embedded UI. + * @param surfacePackage The embedded UI. + * + * @hide + */ + public void setChildSurfacePackage( + @Nullable SurfaceControlViewHost.SurfacePackage surfacePackage) { + mSurfaceView.setChildSurfacePackage(surfacePackage); + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + mSurfaceView.layout(l, t, r, b); + } + + /** + * 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. + * + * <p> 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); } } diff --git a/core/java/android/view/inputmethod/InlineSuggestion.java b/core/java/android/view/inputmethod/InlineSuggestion.java index dd1738a5ff29..ab8f36d85400 100644 --- a/core/java/android/view/inputmethod/InlineSuggestion.java +++ b/core/java/android/view/inputmethod/InlineSuggestion.java @@ -29,7 +29,6 @@ import android.os.RemoteException; import android.util.Size; import android.util.Slog; import android.view.SurfaceControlViewHost; -import android.view.View; import android.view.inline.InlineContentView; import android.view.inline.InlinePresentationSpec; @@ -94,15 +93,15 @@ public final class InlineSuggestion implements Parcelable { this(info, contentProvider, /* inlineContentCallback */ null); } - /** * Inflates a view with the content of this suggestion at a specific size. * The size must be between the {@link InlinePresentationSpec#getMinSize() min size} * and the {@link InlinePresentationSpec#getMaxSize() max size} of the presentation * spec returned by {@link InlineSuggestionInfo#getPresentationSpec()}. * - * <p> The caller can attach an {@link View.OnClickListener} and/or an - * {@link View.OnLongClickListener} to the view in the {@code callback} to receive click and + * <p> The caller can attach an {@link android.view.View.OnClickListener} and/or an + * {@link android.view.View.OnLongClickListener} to the view in the + * {@code callback} to receive click and * long click events on the view. * * @param context Context in which to inflate the view. @@ -113,7 +112,7 @@ public final class InlineSuggestion implements Parcelable { */ public void inflate(@NonNull Context context, @NonNull Size size, @NonNull @CallbackExecutor Executor callbackExecutor, - @NonNull Consumer<View> callback) { + @NonNull Consumer<InlineContentView> callback) { final Size minSize = mInfo.getPresentationSpec().getMinSize(); final Size maxSize = mInfo.getPresentationSpec().getMaxSize(); if (size.getHeight() < minSize.getHeight() || size.getHeight() > maxSize.getHeight() @@ -138,7 +137,7 @@ public final class InlineSuggestion implements Parcelable { } private synchronized InlineContentCallbackImpl getInlineContentCallback(Context context, - Executor callbackExecutor, Consumer<View> callback) { + Executor callbackExecutor, Consumer<InlineContentView> callback) { if (mInlineContentCallback != null) { throw new IllegalStateException("Already called #inflate()"); } @@ -185,12 +184,12 @@ public final class InlineSuggestion implements Parcelable { private final @NonNull Context mContext; private final @NonNull Executor mCallbackExecutor; - private final @NonNull Consumer<View> mCallback; - private @Nullable View mView; + private final @NonNull Consumer<InlineContentView> mCallback; + private @Nullable InlineContentView mView; InlineContentCallbackImpl(@NonNull Context context, @NonNull @CallbackExecutor Executor callbackExecutor, - @NonNull Consumer<View> callback) { + @NonNull Consumer<InlineContentView> callback) { mContext = context; mCallbackExecutor = callbackExecutor; mCallback = callback; @@ -201,7 +200,8 @@ public final class InlineSuggestion implements Parcelable { if (content == null) { mCallbackExecutor.execute(() -> mCallback.accept(/* view */null)); } else { - mView = new InlineContentView(mContext, content); + mView = new InlineContentView(mContext); + mView.setChildSurfacePackage(content); mCallbackExecutor.execute(() -> mCallback.accept(mView)); } } @@ -398,10 +398,10 @@ public final class InlineSuggestion implements Parcelable { }; @DataClass.Generated( - time = 1583889058241L, + time = 1584679775946L, codegenVersion = "1.0.15", sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestion.java", - inputSignatures = "private static final java.lang.String TAG\nprivate final @android.annotation.NonNull android.view.inputmethod.InlineSuggestionInfo mInfo\nprivate final @android.annotation.Nullable com.android.internal.view.inline.IInlineContentProvider mContentProvider\nprivate @com.android.internal.util.DataClass.ParcelWith(android.view.inputmethod.InlineSuggestion.InlineContentCallbackImplParceling.class) @android.annotation.Nullable android.view.inputmethod.InlineSuggestion.InlineContentCallbackImpl mInlineContentCallback\npublic static @android.annotation.TestApi @android.annotation.NonNull android.view.inputmethod.InlineSuggestion newInlineSuggestion(android.view.inputmethod.InlineSuggestionInfo)\npublic void inflate(android.content.Context,android.util.Size,java.util.concurrent.Executor,java.util.function.Consumer<android.view.View>)\nprivate synchronized android.view.inputmethod.InlineSuggestion.InlineContentCallbackImpl getInlineContentCallback(android.content.Context,java.util.concurrent.Executor,java.util.function.Consumer<android.view.View>)\nclass InlineSuggestion extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstDefs=true, genHiddenConstructor=true)") + inputSignatures = "private static final java.lang.String TAG\nprivate final @android.annotation.NonNull android.view.inputmethod.InlineSuggestionInfo mInfo\nprivate final @android.annotation.Nullable com.android.internal.view.inline.IInlineContentProvider mContentProvider\nprivate @com.android.internal.util.DataClass.ParcelWith(android.view.inputmethod.InlineSuggestion.InlineContentCallbackImplParceling.class) @android.annotation.Nullable android.view.inputmethod.InlineSuggestion.InlineContentCallbackImpl mInlineContentCallback\npublic static @android.annotation.TestApi @android.annotation.NonNull android.view.inputmethod.InlineSuggestion newInlineSuggestion(android.view.inputmethod.InlineSuggestionInfo)\npublic void inflate(android.content.Context,android.util.Size,java.util.concurrent.Executor,java.util.function.Consumer<android.view.inline.InlineContentView>)\nprivate synchronized android.view.inputmethod.InlineSuggestion.InlineContentCallbackImpl getInlineContentCallback(android.content.Context,java.util.concurrent.Executor,java.util.function.Consumer<android.view.inline.InlineContentView>)\nclass InlineSuggestion extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstDefs=true, genHiddenConstructor=true)") @Deprecated private void __metadata() {} |