diff options
| author | Mark Renouf <mrenouf@google.com> | 2021-02-26 13:06:47 +0000 |
|---|---|---|
| committer | Android (Google) Code Review <android-gerrit@google.com> | 2021-02-26 13:06:47 +0000 |
| commit | 20bd3d4935bdd00a1603cd14b03d1529e694125d (patch) | |
| tree | f8a6ab8b7c63a9577da26759bf281b3be55a115d /core/java/android | |
| parent | f39926455750b2a1354ac0497d18522e0d24690a (diff) | |
| parent | 8f1b73e1839803c9d8665846e50a3010697fe8b9 (diff) | |
Merge "Long screenshots framework update and API unhide" into sc-dev
Diffstat (limited to 'core/java/android')
| -rw-r--r-- | core/java/android/view/IScrollCaptureCallbacks.aidl | 36 | ||||
| -rw-r--r-- | core/java/android/view/IScrollCaptureConnection.aidl | 34 | ||||
| -rw-r--r-- | core/java/android/view/ScrollCaptureCallback.java | 103 | ||||
| -rw-r--r-- | core/java/android/view/ScrollCaptureConnection.java | 373 | ||||
| -rw-r--r-- | core/java/android/view/ScrollCaptureResponse.aidl | 19 | ||||
| -rw-r--r-- | core/java/android/view/ScrollCaptureResponse.java | 381 | ||||
| -rw-r--r-- | core/java/android/view/ScrollCaptureSearchResults.java | 271 | ||||
| -rw-r--r-- | core/java/android/view/ScrollCaptureSession.java | 56 | ||||
| -rw-r--r-- | core/java/android/view/ScrollCaptureTarget.java | 50 | ||||
| -rw-r--r-- | core/java/android/view/ScrollCaptureTargetResolver.java | 337 | ||||
| -rw-r--r-- | core/java/android/view/View.java | 53 | ||||
| -rw-r--r-- | core/java/android/view/ViewGroup.java | 127 | ||||
| -rw-r--r-- | core/java/android/view/ViewRootImpl.java | 138 | ||||
| -rw-r--r-- | core/java/android/view/Window.java | 2 |
14 files changed, 1186 insertions, 794 deletions
diff --git a/core/java/android/view/IScrollCaptureCallbacks.aidl b/core/java/android/view/IScrollCaptureCallbacks.aidl index d97e3c66cc5d..26eaac0a2bf5 100644 --- a/core/java/android/view/IScrollCaptureCallbacks.aidl +++ b/core/java/android/view/IScrollCaptureCallbacks.aidl @@ -16,12 +16,10 @@ package android.view; -import android.graphics.Point; import android.graphics.Rect; +import android.view.ScrollCaptureResponse; import android.view.Surface; -import android.view.IScrollCaptureConnection; - /** * Asynchronous callback channel for responses to scroll capture requests. * @@ -29,34 +27,30 @@ import android.view.IScrollCaptureConnection; */ interface IScrollCaptureCallbacks { /** - * Scroll capture is available, and a connection has been provided. + * Provides the result of WindowManagerService#requestScrollCapture * - * @param connection a connection to a window process and scrollable content - * @param scrollAreaInWindow the location of scrolling in global (window) coordinate space + * @param response the response which describes the result */ - oneway void onConnected(in IScrollCaptureConnection connection, in Rect scrollBounds, - in Point positionInWindow); + oneway void onScrollCaptureResponse(in ScrollCaptureResponse response); /** - * The window does not support scroll capture. - */ - oneway void onUnavailable(); - - /** - * Called when the remote end has confirmed the request and is ready to begin providing image - * requests. + * Called in reply to IScrollCaptureConnection#startCapture, when the remote end has confirmed + * the request and is ready to begin capturing images. */ oneway void onCaptureStarted(); /** - * Received a response from a capture request. + * Received a response from a capture request. The provided rectangle indicates the portion + * of the requested rectangle which was captured. An empty rectangle indicates that the request + * could not be satisfied (most commonly due to the available scrolling range). + * + * @param flags flags describing additional status of the result + * @param capturedArea the actual area of the image captured */ - oneway void onCaptureBufferSent(long frameNumber, in Rect capturedArea); + oneway void onImageRequestCompleted(int flags, in Rect capturedArea); /** - * Signals that the capture session has completed and the target window may be returned to - * normal interactive use. This may be due to normal shutdown, or after a timeout or other - * unrecoverable state change such as activity lifecycle, window visibility or focus. + * Signals that the capture session has completed and the target window is ready for normal use. */ - oneway void onConnectionClosed(); + oneway void onCaptureEnded(); } diff --git a/core/java/android/view/IScrollCaptureConnection.aidl b/core/java/android/view/IScrollCaptureConnection.aidl index 63a4f48aeb20..c55e88800393 100644 --- a/core/java/android/view/IScrollCaptureConnection.aidl +++ b/core/java/android/view/IScrollCaptureConnection.aidl @@ -17,33 +17,45 @@ package android.view; import android.graphics.Rect; +import android.os.ICancellationSignal; import android.view.Surface; /** - * Interface implemented by a client of the Scroll Capture framework to receive requests - * to start, capture images and end the session. + * A remote connection to a scroll capture target. * * {@hide} */ interface IScrollCaptureConnection { /** - * Informs the client that it has been selected for scroll capture and should prepare to - * to begin handling capture requests. + * Informs the target that it has been selected for scroll capture. + * + * @param surface a return channel for image buffers + * + * @return a cancallation signal which is used cancel the request + */ + ICancellationSignal startCapture(in Surface surface); + + /** + * Request the target capture an image within the provided rectangle. + * + * @param surface a return channel for image buffers + * @param signal a cancallation signal which can interrupt the request + * + * @return a cancallation signal which is used cancel the request */ - oneway void startCapture(in Surface surface); + ICancellationSignal requestImage(in Rect captureArea); /** - * Request the client capture an image within the provided rectangle. + * Inform the target that capture has ended. * - * @see android.view.ScrollCaptureCallback#onScrollCaptureRequest + * @return a cancallation signal which is used cancel the request */ - oneway void requestImage(in Rect captureArea); + ICancellationSignal endCapture(); /** - * Inform the client that capture has ended. The client should shut down and release all - * local resources in use and prepare for return to normal interactive usage. + * Closes the connection. */ - oneway void endCapture(); + oneway void close(); } diff --git a/core/java/android/view/ScrollCaptureCallback.java b/core/java/android/view/ScrollCaptureCallback.java index d3aad2c72d27..16886160baca 100644 --- a/core/java/android/view/ScrollCaptureCallback.java +++ b/core/java/android/view/ScrollCaptureCallback.java @@ -19,6 +19,7 @@ package android.view; import android.annotation.NonNull; import android.annotation.UiThread; import android.graphics.Rect; +import android.os.CancellationSignal; import java.util.function.Consumer; @@ -58,8 +59,6 @@ import java.util.function.Consumer; * @see View#setScrollCaptureHint(int) * @see View#setScrollCaptureCallback(ScrollCaptureCallback) * @see Window#registerScrollCaptureCallback(ScrollCaptureCallback) - * - * @hide */ @UiThread public interface ScrollCaptureCallback { @@ -68,80 +67,84 @@ public interface ScrollCaptureCallback { * The system is searching for the appropriate scrolling container to capture and would like to * know the size and position of scrolling content handled by this callback. * <p> - * Implementations should inset {@code containingViewBounds} to cover only the area within the - * containing view where scrolling content may be positioned. This should cover only the content - * which tracks with scrolling movement. - * <p> - * Return the updated rectangle to {@code resultConsumer}. If for any reason the scrolling - * content is not available to capture, a {@code null} rectangle may be returned, and this view - * will be excluded as the target for this request. + * To determine scroll bounds, an implementation should inset the visible bounds of the + * containing view to cover only the area where scrolling content may be positioned. This + * should cover only the content which tracks with scrolling movement. * <p> - * Responses received after XXXms will be discarded. + * Return the updated rectangle to {@link Consumer#accept onReady.accept}. If for any reason the + * scrolling content is not available to capture, a empty rectangle may be returned which will + * exclude this view from consideration. * <p> - * TODO: finalize timeout + * This request may be cancelled via the provided {@link CancellationSignal}. When this happens, + * any future call to {@link Consumer#accept onReady.accept} will have no effect and this + * content will be omitted from the search results. * - * @param onReady consumer for the updated rectangle + * @param signal signal to cancel the operation in progress + * @param onReady consumer for the updated rectangle */ - void onScrollCaptureSearch(@NonNull Consumer<Rect> onReady); + void onScrollCaptureSearch(@NonNull CancellationSignal signal, @NonNull Consumer<Rect> onReady); /** * Scroll Capture has selected this callback to provide the scrolling image content. * <p> - * The onReady signal should be called when ready to begin handling image requests. + * {@link Runnable#run onReady.run} should be called when ready to begin handling image + * requests. + * <p> + * This request may be cancelled via the provided {@link CancellationSignal}. When this happens, + * any future call to {@link Runnable#run onReady.run} will have no effect and provided session + * will not be activated. + * + * @param session the current session, resources provided by it are valid for use until the + * {@link #onScrollCaptureEnd(Runnable) session ends} + * @param signal signal to cancel the operation in progress + * @param onReady signal used to report completion of the request */ - void onScrollCaptureStart(@NonNull ScrollCaptureSession session, @NonNull Runnable onReady); + void onScrollCaptureStart(@NonNull ScrollCaptureSession session, + @NonNull CancellationSignal signal, @NonNull Runnable onReady); /** * An image capture has been requested from the scrolling content. * <p> - * <code>captureArea</code> contains the bounds of the image requested, relative to the - * rectangle provided by {@link ScrollCaptureCallback#onScrollCaptureSearch}, referred to as - * {@code scrollBounds}. - * here. - * <p> - * A series of requests will step by a constant vertical amount relative to {@code - * scrollBounds}, moving through the scrolling range of content, above and below the current - * visible area. The rectangle's vertical position will not account for any scrolling movement - * since capture started. Implementations therefore must track any scroll position changes and - * subtract this distance from requests. + * The requested rectangle describes an area inside the target view, relative to + * <code>scrollBounds</code>. The content may be offscreen, above or below the current visible + * portion of the target view. To handle the request, render the available portion of this + * rectangle to a buffer and return it via the Surface available from {@link + * ScrollCaptureSession#getSurface()}. * <p> - * To handle a request, the content should be scrolled to maximize the visible area of the - * requested rectangle. Offset {@code captureArea} again to account for any further scrolling. + * Note: Implementations are only required to render the requested content, and may do so into + * off-screen buffers without scrolling if they are able. * <p> - * Finally, clip this rectangle against scrollBounds to determine what portion, if any is - * visible content to capture. If the rectangle is completely clipped, set it to {@link - * Rect#setEmpty() empty} and skip the next step. - * <p> - * Make a copy of {@code captureArea}, transform to window coordinates and draw the window, - * clipped to this rectangle, into the {@link ScrollCaptureSession#getSurface() surface} at - * offset (0,0). - * <p> - * Finally, return the resulting {@code captureArea} using - * {@link ScrollCaptureSession#notifyBufferSent}. - * <p> - * If the response is not supplied within XXXms, the session will end with a call to {@link - * #onScrollCaptureEnd}, after which {@code session} is invalid and should be discarded. - * <p> - * TODO: finalize timeout + * The resulting available portion of the request must be computed as a portion of {@code + * captureArea}, and sent to signal the operation is complete, using {@link Consumer#accept + * onComplete.accept}. If the requested rectangle is partially or fully out of bounds the + * resulting portion should be returned. If no portion is available (outside of available + * content), then skip sending any buffer and report an empty Rect as result. * <p> + * This request may be cancelled via the provided {@link CancellationSignal}. When this happens, + * any future call to {@link Consumer#accept onComplete.accept} will be ignored until the next + * request. * + * @param session the current session, resources provided by it are valid for use until the + * {@link #onScrollCaptureEnd(Runnable) session ends} + * @param signal signal to cancel the operation in progress * @param captureArea the area to capture, a rectangle within {@code scrollBounds} + * @param onComplete a consumer for the captured area */ - void onScrollCaptureImageRequest( - @NonNull ScrollCaptureSession session, @NonNull Rect captureArea); + void onScrollCaptureImageRequest(@NonNull ScrollCaptureSession session, + @NonNull CancellationSignal signal, @NonNull Rect captureArea, + @NonNull Consumer<Rect> onComplete); /** * Signals that capture has ended. Implementations should release any temporary resources or * references to objects in use during the capture. Any resources obtained from the session are * now invalid and attempts to use them after this point may throw an exception. * <p> - * The window should be returned as much as possible to its original state when capture started. - * At a minimum, the content should be scrolled to its original position. - * <p> - * <code>onReady</code> should be called when the window should be made visible and - * interactive. The system will wait up to XXXms for this call before proceeding. + * The window should be returned to its original state when capture started. At a minimum, the + * content should be scrolled to its original position. * <p> - * TODO: finalize timeout + * {@link Runnable#run onReady.run} should be called as soon as possible after the window is + * ready for normal interactive use. After the callback (or after a timeout, if not called) the + * screenshot tool will be dismissed and the window may become visible to the user at any time. * * @param onReady a callback to inform the system that the application has completed any * cleanup and is ready to become visible diff --git a/core/java/android/view/ScrollCaptureConnection.java b/core/java/android/view/ScrollCaptureConnection.java index 0e6cdd1dbec5..3456e016c42c 100644 --- a/core/java/android/view/ScrollCaptureConnection.java +++ b/core/java/android/view/ScrollCaptureConnection.java @@ -18,18 +18,23 @@ package android.view; import static java.util.Objects.requireNonNull; +import android.annotation.BinderThread; import android.annotation.NonNull; import android.annotation.UiThread; -import android.annotation.WorkerThread; import android.graphics.Point; import android.graphics.Rect; -import android.os.Handler; +import android.os.CancellationSignal; +import android.os.ICancellationSignal; import android.os.RemoteException; +import android.os.Trace; import android.util.CloseGuard; +import android.util.Log; import com.android.internal.annotations.VisibleForTesting; -import java.util.concurrent.atomic.AtomicBoolean; +import java.lang.ref.WeakReference; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Mediator between a selected scroll capture target view and a remote process. @@ -41,270 +46,276 @@ import java.util.concurrent.atomic.AtomicBoolean; public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub { private static final String TAG = "ScrollCaptureConnection"; - private static final int DEFAULT_TIMEOUT = 1000; - - private final Handler mHandler; - private ScrollCaptureTarget mSelectedTarget; - private int mTimeoutMillis = DEFAULT_TIMEOUT; - - protected Surface mSurface; - private IScrollCaptureCallbacks mCallbacks; + private final Object mLock = new Object(); private final Rect mScrollBounds; private final Point mPositionInWindow; private final CloseGuard mCloseGuard; + private final Executor mUiThread; + + private ScrollCaptureCallback mLocal; + private IScrollCaptureCallbacks mRemote; - // The current session instance in use by the callback. private ScrollCaptureSession mSession; - // Helps manage timeout callbacks registered to handler and aids testing. - private DelayedAction mTimeoutAction; + private CancellationSignal mCancellation; + + private volatile boolean mStarted; + private volatile boolean mConnected; /** * Constructs a ScrollCaptureConnection. * * @param selectedTarget the target the client is controlling - * @param callbacks the callbacks to reply to system requests + * @param remote the callbacks to reply to system requests * * @hide */ public ScrollCaptureConnection( + @NonNull Executor uiThread, @NonNull ScrollCaptureTarget selectedTarget, - @NonNull IScrollCaptureCallbacks callbacks) { + @NonNull IScrollCaptureCallbacks remote) { + mUiThread = requireNonNull(uiThread, "<uiThread> must non-null"); requireNonNull(selectedTarget, "<selectedTarget> must non-null"); - requireNonNull(callbacks, "<callbacks> must non-null"); - final Rect scrollBounds = requireNonNull(selectedTarget.getScrollBounds(), + mRemote = requireNonNull(remote, "<callbacks> must non-null"); + mScrollBounds = requireNonNull(Rect.copyOrNull(selectedTarget.getScrollBounds()), "target.getScrollBounds() must be non-null to construct a client"); - mSelectedTarget = selectedTarget; - mHandler = selectedTarget.getContainingView().getHandler(); - mScrollBounds = new Rect(scrollBounds); + mLocal = selectedTarget.getCallback(); mPositionInWindow = new Point(selectedTarget.getPositionInWindow()); - mCallbacks = callbacks; mCloseGuard = new CloseGuard(); mCloseGuard.open("close"); - - selectedTarget.getContainingView().addOnAttachStateChangeListener( - new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - - } - - @Override - public void onViewDetachedFromWindow(View v) { - selectedTarget.getContainingView().removeOnAttachStateChangeListener(this); - endCapture(); - } - }); - } - - @VisibleForTesting - public void setTimeoutMillis(int timeoutMillis) { - mTimeoutMillis = timeoutMillis; + mConnected = true; } - @VisibleForTesting - public DelayedAction getTimeoutAction() { - return mTimeoutAction; - } - - private void checkConnected() { - if (mSelectedTarget == null || mCallbacks == null) { - throw new IllegalStateException("This client has been disconnected."); + @BinderThread + @Override + public ICancellationSignal startCapture(Surface surface) throws RemoteException { + checkConnected(); + if (!surface.isValid()) { + throw new RemoteException(new IllegalArgumentException("surface must be valid")); } - } - private void checkStarted() { - if (mSession == null) { - throw new IllegalStateException("Capture session has not been started!"); - } - } + ICancellationSignal cancellation = CancellationSignal.createTransport(); + mCancellation = CancellationSignal.fromTransport(cancellation); + mSession = new ScrollCaptureSession(surface, mScrollBounds, mPositionInWindow); - @WorkerThread // IScrollCaptureConnection - @Override - public void startCapture(Surface surface) throws RemoteException { - checkConnected(); - mSurface = surface; - scheduleTimeout(mTimeoutMillis, this::onStartCaptureTimeout); - mSession = new ScrollCaptureSession(mSurface, mScrollBounds, mPositionInWindow, this); - mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureStart(mSession, - this::onStartCaptureCompleted)); + Runnable listener = + SafeCallback.create(mCancellation, mUiThread, this::onStartCaptureCompleted); + // -> UiThread + mUiThread.execute(() -> mLocal.onScrollCaptureStart(mSession, mCancellation, listener)); + return cancellation; } @UiThread private void onStartCaptureCompleted() { - if (cancelTimeout()) { - mHandler.post(() -> { - try { - mCallbacks.onCaptureStarted(); - } catch (RemoteException e) { - doShutdown(); - } - }); + mStarted = true; + try { + mRemote.onCaptureStarted(); + } catch (RemoteException e) { + Log.w(TAG, "Shutting down due to error: ", e); + close(); } } - @UiThread - private void onStartCaptureTimeout() { - endCapture(); - } - @WorkerThread // IScrollCaptureConnection + @BinderThread @Override - public void requestImage(Rect requestRect) { + public ICancellationSignal requestImage(Rect requestRect) throws RemoteException { + Trace.beginSection("requestImage"); checkConnected(); checkStarted(); - scheduleTimeout(mTimeoutMillis, this::onRequestImageTimeout); - // Response is dispatched via ScrollCaptureSession, to onRequestImageCompleted - mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureImageRequest( - mSession, new Rect(requestRect))); - } - @UiThread - void onRequestImageCompleted(long frameNumber, Rect capturedArea) { - final Rect finalCapturedArea = new Rect(capturedArea); - if (cancelTimeout()) { - mHandler.post(() -> { - try { - mCallbacks.onCaptureBufferSent(frameNumber, finalCapturedArea); - } catch (RemoteException e) { - doShutdown(); - } - }); - } + ICancellationSignal cancellation = CancellationSignal.createTransport(); + mCancellation = CancellationSignal.fromTransport(cancellation); + + Consumer<Rect> listener = + SafeCallback.create(mCancellation, mUiThread, this::onImageRequestCompleted); + // -> UiThread + mUiThread.execute(() -> mLocal.onScrollCaptureImageRequest( + mSession, mCancellation, new Rect(requestRect), listener)); + Trace.endSection(); + return cancellation; } @UiThread - private void onRequestImageTimeout() { - endCapture(); + void onImageRequestCompleted(Rect capturedArea) { + try { + mRemote.onImageRequestCompleted(0, capturedArea); + } catch (RemoteException e) { + Log.w(TAG, "Shutting down due to error: ", e); + close(); + } } - @WorkerThread // IScrollCaptureConnection + @BinderThread @Override - public void endCapture() { - if (isStarted()) { - scheduleTimeout(mTimeoutMillis, this::onEndCaptureTimeout); - mHandler.post(() -> - mSelectedTarget.getCallback().onScrollCaptureEnd(this::onEndCaptureCompleted)); - } else { - disconnect(); - } - } + public ICancellationSignal endCapture() throws RemoteException { + checkConnected(); + checkStarted(); - private boolean isStarted() { - return mCallbacks != null && mSelectedTarget != null; - } + ICancellationSignal cancellation = CancellationSignal.createTransport(); + mCancellation = CancellationSignal.fromTransport(cancellation); - @UiThread - private void onEndCaptureCompleted() { // onEndCaptureCompleted - if (cancelTimeout()) { - doShutdown(); - } + Runnable listener = + SafeCallback.create(mCancellation, mUiThread, this::onEndCaptureCompleted); + // -> UiThread + mUiThread.execute(() -> mLocal.onScrollCaptureEnd(listener)); + return cancellation; } @UiThread - private void onEndCaptureTimeout() { - doShutdown(); + private void onEndCaptureCompleted() { + synchronized (mLock) { + mStarted = false; + try { + mRemote.onCaptureEnded(); + } catch (RemoteException e) { + Log.w(TAG, "Shutting down due to error: ", e); + close(); + } + } } + @BinderThread + @Override + public void close() { + if (mStarted) { + Log.w(TAG, "close(): capture is still started?! Ending now."); - private void doShutdown() { - try { - if (mCallbacks != null) { - mCallbacks.onConnectionClosed(); - } - } catch (RemoteException e) { - // Ignore - } finally { - disconnect(); + // -> UiThread + mUiThread.execute(() -> mLocal.onScrollCaptureEnd(() -> { /* ignore */ })); + mStarted = false; } + disconnect(); } /** * Shuts down this client and releases references to dependent objects. No attempt is made * to notify the controller, use with caution! */ - public void disconnect() { - if (mSession != null) { - mSession.disconnect(); + private void disconnect() { + synchronized (mLock) { mSession = null; + mConnected = false; + mStarted = false; + mRemote = null; + mLocal = null; + mCloseGuard.close(); } + } + + public boolean isConnected() { + return mConnected; + } - mSelectedTarget = null; - mCallbacks = null; + public boolean isStarted() { + return mStarted; + } + + private synchronized void checkConnected() throws RemoteException { + synchronized (mLock) { + if (!mConnected) { + throw new RemoteException(new IllegalStateException("Not connected")); + } + } + } + + private void checkStarted() throws RemoteException { + synchronized (mLock) { + if (!mStarted) { + throw new RemoteException(new IllegalStateException("Not started!")); + } + } } /** @return a string representation of the state of this client */ public String toString() { return "ScrollCaptureConnection{" + + "connected=" + mConnected + + ", started=" + mStarted + ", session=" + mSession - + ", selectedTarget=" + mSelectedTarget - + ", clientCallbacks=" + mCallbacks + + ", remote=" + mRemote + + ", local=" + mLocal + "}"; } - private boolean cancelTimeout() { - if (mTimeoutAction != null) { - return mTimeoutAction.cancel(); - } - return false; + @VisibleForTesting + public CancellationSignal getCancellation() { + return mCancellation; } - private void scheduleTimeout(long timeoutMillis, Runnable action) { - if (mTimeoutAction != null) { - mTimeoutAction.cancel(); + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + close(); + } finally { + super.finalize(); } - mTimeoutAction = new DelayedAction(mHandler, timeoutMillis, action); } - /** @hide */ - @VisibleForTesting - public static class DelayedAction { - private final AtomicBoolean mCompleted = new AtomicBoolean(); - private final Object mToken = new Object(); - private final Handler mHandler; - private final Runnable mAction; - - @VisibleForTesting - public DelayedAction(Handler handler, long timeoutMillis, Runnable action) { - mHandler = handler; - mAction = action; - mHandler.postDelayed(this::onTimeout, mToken, timeoutMillis); + private static class SafeCallback<T> { + private final CancellationSignal mSignal; + private final WeakReference<T> mTargetRef; + private final Executor mExecutor; + private boolean mExecuted; + + protected SafeCallback(CancellationSignal signal, Executor executor, T target) { + mSignal = signal; + mTargetRef = new WeakReference<>(target); + mExecutor = executor; } - private boolean onTimeout() { - if (mCompleted.compareAndSet(false, true)) { - mAction.run(); - return true; + // Provide the target to the consumer to invoke, forward on handler thread ONCE, + // and only if noy cancelled, and the target is still available (not collected) + protected final void maybeAccept(Consumer<T> targetConsumer) { + if (mExecuted) { + return; + } + mExecuted = true; + if (mSignal.isCanceled()) { + return; + } + T target = mTargetRef.get(); + if (target == null) { + return; } - return false; + mExecutor.execute(() -> targetConsumer.accept(target)); } - /** - * Cause the timeout action to run immediately and mark as timed out. - * - * @return true if the timeout was run, false if the timeout had already been canceled - */ - @VisibleForTesting - public boolean timeoutNow() { - return onTimeout(); + static Runnable create(CancellationSignal signal, Executor executor, Runnable target) { + return new RunnableCallback(signal, executor, target); } - /** - * Attempt to cancel the timeout action (such as after a callback is made) - * - * @return true if the timeout was canceled and will not run, false if time has expired and - * the timeout action has or will run momentarily - */ - public boolean cancel() { - if (!mCompleted.compareAndSet(false, true)) { - // Whoops, too late! - return false; - } - mHandler.removeCallbacksAndMessages(mToken); - return true; + static <T> Consumer<T> create(CancellationSignal signal, Executor executor, + Consumer<T> target) { + return new ConsumerCallback<T>(signal, executor, target); + } + } + + private static final class RunnableCallback extends SafeCallback<Runnable> implements Runnable { + RunnableCallback(CancellationSignal signal, Executor executor, Runnable target) { + super(signal, executor, target); + } + + @Override + public void run() { + maybeAccept(Runnable::run); + } + } + + private static final class ConsumerCallback<T> extends SafeCallback<Consumer<T>> + implements Consumer<T> { + ConsumerCallback(CancellationSignal signal, Executor executor, Consumer<T> target) { + super(signal, executor, target); + } + + @Override + public void accept(T value) { + maybeAccept((target) -> target.accept(value)); } } } diff --git a/core/java/android/view/ScrollCaptureResponse.aidl b/core/java/android/view/ScrollCaptureResponse.aidl new file mode 100644 index 000000000000..3de2b80ef16e --- /dev/null +++ b/core/java/android/view/ScrollCaptureResponse.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 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; + +parcelable ScrollCaptureResponse; diff --git a/core/java/android/view/ScrollCaptureResponse.java b/core/java/android/view/ScrollCaptureResponse.java new file mode 100644 index 000000000000..564113edb3c7 --- /dev/null +++ b/core/java/android/view/ScrollCaptureResponse.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2021 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.NonNull; +import android.annotation.Nullable; +import android.graphics.Rect; +import android.os.Parcelable; + +import com.android.internal.util.DataClass; + +import java.util.ArrayList; + +/** @hide */ +@DataClass(genToString = true, genGetters = true) +public class ScrollCaptureResponse implements Parcelable { + + /** Developer-facing human readable description of the result. */ + @NonNull + private String mDescription = ""; + + // Remaining fields are non-null when isConnected() == true + + /** The active connection for a successful result. */ + @Nullable + @DataClass.MaySetToNull + private IScrollCaptureConnection mConnection = null; + + /** The bounds of the window within the display */ + @Nullable + private Rect mWindowBounds = null; + + /** The bounds of the scrolling content, in window space. */ + @Nullable + private Rect mBoundsInWindow = null; + + /** The current window title. */ + @Nullable + private String mWindowTitle = null; + + /** Carries additional logging and debugging information when enabled. */ + @NonNull + @DataClass.PluralOf("message") + private ArrayList<String> mMessages = new ArrayList<>(); + + /** Whether a connection has been returned. */ + public boolean isConnected() { + return mConnection != null; + } + + + + + // Code below generated by codegen v1.0.22. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/ScrollCaptureResponse.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + /* package-private */ ScrollCaptureResponse( + @NonNull String description, + @Nullable IScrollCaptureConnection connection, + @Nullable Rect windowBounds, + @Nullable Rect boundsInWindow, + @Nullable String windowTitle, + @NonNull ArrayList<String> messages) { + this.mDescription = description; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mDescription); + this.mConnection = connection; + this.mWindowBounds = windowBounds; + this.mBoundsInWindow = boundsInWindow; + this.mWindowTitle = windowTitle; + this.mMessages = messages; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mMessages); + + // onConstructed(); // You can define this method to get a callback + } + + /** + * Developer-facing human readable description of the result. + */ + @DataClass.Generated.Member + public @NonNull String getDescription() { + return mDescription; + } + + /** + * The active connection for a successful result. + */ + @DataClass.Generated.Member + public @Nullable IScrollCaptureConnection getConnection() { + return mConnection; + } + + /** + * The bounds of the window within the display + */ + @DataClass.Generated.Member + public @Nullable Rect getWindowBounds() { + return mWindowBounds; + } + + /** + * The bounds of the scrolling content, in window space. + */ + @DataClass.Generated.Member + public @Nullable Rect getBoundsInWindow() { + return mBoundsInWindow; + } + + /** + * The current window title. + */ + @DataClass.Generated.Member + public @Nullable String getWindowTitle() { + return mWindowTitle; + } + + /** + * Carries additional logging and debugging information when enabled. + */ + @DataClass.Generated.Member + public @NonNull ArrayList<String> getMessages() { + return mMessages; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "ScrollCaptureResponse { " + + "description = " + mDescription + ", " + + "connection = " + mConnection + ", " + + "windowBounds = " + mWindowBounds + ", " + + "boundsInWindow = " + mBoundsInWindow + ", " + + "windowTitle = " + mWindowTitle + ", " + + "messages = " + mMessages + + " }"; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + byte flg = 0; + if (mConnection != null) flg |= 0x2; + if (mWindowBounds != null) flg |= 0x4; + if (mBoundsInWindow != null) flg |= 0x8; + if (mWindowTitle != null) flg |= 0x10; + dest.writeByte(flg); + dest.writeString(mDescription); + if (mConnection != null) dest.writeStrongInterface(mConnection); + if (mWindowBounds != null) dest.writeTypedObject(mWindowBounds, flags); + if (mBoundsInWindow != null) dest.writeTypedObject(mBoundsInWindow, flags); + if (mWindowTitle != null) dest.writeString(mWindowTitle); + dest.writeStringList(mMessages); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + protected ScrollCaptureResponse(@NonNull android.os.Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + String description = in.readString(); + IScrollCaptureConnection connection = (flg & 0x2) == 0 ? null : IScrollCaptureConnection.Stub.asInterface(in.readStrongBinder()); + Rect windowBounds = (flg & 0x4) == 0 ? null : (Rect) in.readTypedObject(Rect.CREATOR); + Rect boundsInWindow = (flg & 0x8) == 0 ? null : (Rect) in.readTypedObject(Rect.CREATOR); + String windowTitle = (flg & 0x10) == 0 ? null : in.readString(); + ArrayList<String> messages = new ArrayList<>(); + in.readStringList(messages); + + this.mDescription = description; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mDescription); + this.mConnection = connection; + this.mWindowBounds = windowBounds; + this.mBoundsInWindow = boundsInWindow; + this.mWindowTitle = windowTitle; + this.mMessages = messages; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mMessages); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<ScrollCaptureResponse> CREATOR + = new Parcelable.Creator<ScrollCaptureResponse>() { + @Override + public ScrollCaptureResponse[] newArray(int size) { + return new ScrollCaptureResponse[size]; + } + + @Override + public ScrollCaptureResponse createFromParcel(@NonNull android.os.Parcel in) { + return new ScrollCaptureResponse(in); + } + }; + + /** + * A builder for {@link ScrollCaptureResponse} + */ + @SuppressWarnings("WeakerAccess") + @DataClass.Generated.Member + public static class Builder { + + private @NonNull String mDescription; + private @Nullable IScrollCaptureConnection mConnection; + private @Nullable Rect mWindowBounds; + private @Nullable Rect mBoundsInWindow; + private @Nullable String mWindowTitle; + private @NonNull ArrayList<String> mMessages; + + private long mBuilderFieldsSet = 0L; + + public Builder() { + } + + /** + * Developer-facing human readable description of the result. + */ + @DataClass.Generated.Member + public @NonNull Builder setDescription(@NonNull String value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1; + mDescription = value; + return this; + } + + /** + * The active connection for a successful result. + */ + @DataClass.Generated.Member + public @NonNull Builder setConnection(@Nullable IScrollCaptureConnection value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; + mConnection = value; + return this; + } + + /** + * The bounds of the window within the display + */ + @DataClass.Generated.Member + public @NonNull Builder setWindowBounds(@NonNull Rect value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x4; + mWindowBounds = value; + return this; + } + + /** + * The bounds of the scrolling content, in window space. + */ + @DataClass.Generated.Member + public @NonNull Builder setBoundsInWindow(@NonNull Rect value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x8; + mBoundsInWindow = value; + return this; + } + + /** + * The current window title. + */ + @DataClass.Generated.Member + public @NonNull Builder setWindowTitle(@NonNull String value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x10; + mWindowTitle = value; + return this; + } + + /** + * Carries additional logging and debugging information when enabled. + */ + @DataClass.Generated.Member + public @NonNull Builder setMessages(@NonNull ArrayList<String> value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x20; + mMessages = value; + return this; + } + + /** @see #setMessages */ + @DataClass.Generated.Member + public @NonNull Builder addMessage(@NonNull String value) { + if (mMessages == null) setMessages(new ArrayList<>()); + mMessages.add(value); + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull ScrollCaptureResponse build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x40; // Mark builder used + + if ((mBuilderFieldsSet & 0x1) == 0) { + mDescription = ""; + } + if ((mBuilderFieldsSet & 0x2) == 0) { + mConnection = null; + } + if ((mBuilderFieldsSet & 0x4) == 0) { + mWindowBounds = null; + } + if ((mBuilderFieldsSet & 0x8) == 0) { + mBoundsInWindow = null; + } + if ((mBuilderFieldsSet & 0x10) == 0) { + mWindowTitle = null; + } + if ((mBuilderFieldsSet & 0x20) == 0) { + mMessages = new ArrayList<>(); + } + ScrollCaptureResponse o = new ScrollCaptureResponse( + mDescription, + mConnection, + mWindowBounds, + mBoundsInWindow, + mWindowTitle, + mMessages); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x40) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } + + @DataClass.Generated( + time = 1612282689462L, + codegenVersion = "1.0.22", + sourceFile = "frameworks/base/core/java/android/view/ScrollCaptureResponse.java", + inputSignatures = "private @android.annotation.NonNull java.lang.String mDescription\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.MaySetToNull android.view.IScrollCaptureConnection mConnection\nprivate @android.annotation.Nullable android.graphics.Rect mWindowBounds\nprivate @android.annotation.Nullable android.graphics.Rect mBoundsInWindow\nprivate @android.annotation.Nullable java.lang.String mWindowTitle\nprivate @android.annotation.NonNull @com.android.internal.util.DataClass.PluralOf(\"message\") java.util.ArrayList<java.lang.String> mMessages\npublic boolean isConnected()\nclass ScrollCaptureResponse extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genGetters=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/view/ScrollCaptureSearchResults.java b/core/java/android/view/ScrollCaptureSearchResults.java new file mode 100644 index 000000000000..3469b9dc7103 --- /dev/null +++ b/core/java/android/view/ScrollCaptureSearchResults.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2021 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 static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.annotation.UiThread; +import android.graphics.Rect; +import android.os.CancellationSignal; +import android.util.IndentingPrintWriter; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * Collects nodes in the view hierarchy which have been identified as scrollable content. + * + * @hide + */ +@UiThread +public final class ScrollCaptureSearchResults { + private final Executor mExecutor; + private final List<ScrollCaptureTarget> mTargets; + private final CancellationSignal mCancel; + + private Runnable mOnCompleteListener; + private int mCompleted; + private boolean mComplete = true; + + public ScrollCaptureSearchResults(Executor executor) { + mExecutor = executor; + mTargets = new ArrayList<>(); + mCancel = new CancellationSignal(); + } + + // Public + + /** + * Add the given target to the results. + * + * @param target the target to consider + */ + public void addTarget(@NonNull ScrollCaptureTarget target) { + requireNonNull(target); + + mTargets.add(target); + mComplete = false; + final ScrollCaptureCallback callback = target.getCallback(); + final Consumer<Rect> consumer = new SearchRequest(target); + + // Defer so the view hierarchy scan completes first + mExecutor.execute( + () -> callback.onScrollCaptureSearch(mCancel, consumer)); + } + + public boolean isComplete() { + return mComplete; + } + + /** + * Provides a callback to be invoked as soon as all responses have been received from all + * targets to this point. + * + * @param onComplete listener to add + */ + public void setOnCompleteListener(Runnable onComplete) { + if (mComplete) { + onComplete.run(); + } else { + mOnCompleteListener = onComplete; + } + } + + /** + * Indicates whether the search results are empty. + * + * @return true if no targets have been added + */ + public boolean isEmpty() { + return mTargets.isEmpty(); + } + + /** + * Force the results to complete now, cancelling any pending requests and calling a complete + * listener if provided. + */ + public void finish() { + if (!mComplete) { + mCancel.cancel(); + signalComplete(); + } + } + + private void signalComplete() { + mComplete = true; + mTargets.sort(PRIORITY_ORDER); + if (mOnCompleteListener != null) { + mOnCompleteListener.run(); + mOnCompleteListener = null; + } + } + + @VisibleForTesting + public List<ScrollCaptureTarget> getTargets() { + return new ArrayList<>(mTargets); + } + + /** + * Get the top ranked result out of all completed requests. + * + * @return the top ranked result + */ + public ScrollCaptureTarget getTopResult() { + ScrollCaptureTarget target = mTargets.isEmpty() ? null : mTargets.get(0); + return target != null && target.getScrollBounds() != null ? target : null; + } + + private class SearchRequest implements Consumer<Rect> { + private ScrollCaptureTarget mTarget; + + SearchRequest(ScrollCaptureTarget target) { + mTarget = target; + } + + @Override + public void accept(Rect scrollBounds) { + if (mTarget == null || mCancel.isCanceled()) { + return; + } + mExecutor.execute(() -> consume(scrollBounds)); + } + + private void consume(Rect scrollBounds) { + if (mTarget == null || mCancel.isCanceled()) { + return; + } + if (!nullOrEmpty(scrollBounds)) { + mTarget.setScrollBounds(scrollBounds); + mTarget.updatePositionInWindow(); + } + mCompleted++; + mTarget = null; + + // All done? + if (mCompleted == mTargets.size()) { + signalComplete(); + } + } + } + + private static final int AFTER = 1; + private static final int BEFORE = -1; + private static final int EQUAL = 0; + + static final Comparator<ScrollCaptureTarget> PRIORITY_ORDER = (a, b) -> { + if (a == null && b == null) { + return 0; + } else if (a == null || b == null) { + return (a == null) ? 1 : -1; + } + + boolean emptyScrollBoundsA = nullOrEmpty(a.getScrollBounds()); + boolean emptyScrollBoundsB = nullOrEmpty(b.getScrollBounds()); + if (emptyScrollBoundsA || emptyScrollBoundsB) { + if (emptyScrollBoundsA && emptyScrollBoundsB) { + return EQUAL; + } + // Prefer the one with a non-empty scroll bounds + if (emptyScrollBoundsA) { + return AFTER; + } + return BEFORE; + } + + final View viewA = a.getContainingView(); + final View viewB = b.getContainingView(); + + // Prefer any view with scrollCaptureHint="INCLUDE", over one without + // This is an escape hatch for the next rule (descendants first) + boolean hintIncludeA = hasIncludeHint(viewA); + boolean hintIncludeB = hasIncludeHint(viewB); + if (hintIncludeA != hintIncludeB) { + return (hintIncludeA) ? BEFORE : AFTER; + } + // If the views are relatives, prefer the descendant. This allows implementations to + // leverage nested scrolling APIs by interacting with the innermost scrollable view (as + // would happen with touch input). + if (isDescendant(viewA, viewB)) { + return BEFORE; + } + if (isDescendant(viewB, viewA)) { + return AFTER; + } + + // finally, prefer one with larger scroll bounds + int scrollAreaA = area(a.getScrollBounds()); + int scrollAreaB = area(b.getScrollBounds()); + return (scrollAreaA >= scrollAreaB) ? BEFORE : AFTER; + }; + + private static int area(Rect r) { + return r.width() * r.height(); + } + + private static boolean nullOrEmpty(Rect r) { + return r == null || r.isEmpty(); + } + + private static boolean hasIncludeHint(View view) { + return (view.getScrollCaptureHint() & View.SCROLL_CAPTURE_HINT_INCLUDE) != 0; + } + + /** + * Determines if {@code otherView} is a descendant of {@code view}. + * + * @param view a view + * @param otherView another view + * @return true if {@code view} is an ancestor of {@code otherView} + */ + private static boolean isDescendant(@NonNull View view, @NonNull View otherView) { + if (view == otherView) { + return false; + } + ViewParent otherParent = otherView.getParent(); + while (otherParent != view && otherParent != null) { + otherParent = otherParent.getParent(); + } + return otherParent == view; + } + + void dump(IndentingPrintWriter writer) { + writer.println("results:"); + writer.increaseIndent(); + writer.println("complete: " + isComplete()); + writer.println("cancelled: " + mCancel.isCanceled()); + writer.println("targets:"); + writer.increaseIndent(); + if (isEmpty()) { + writer.println("None"); + } else { + for (int i = 0; i < mTargets.size(); i++) { + writer.println("[" + i + "]"); + writer.increaseIndent(); + mTargets.get(i).dump(writer); + writer.decreaseIndent(); + } + writer.decreaseIndent(); + } + writer.decreaseIndent(); + } +} diff --git a/core/java/android/view/ScrollCaptureSession.java b/core/java/android/view/ScrollCaptureSession.java index 92617a325265..748e7ea56f41 100644 --- a/core/java/android/view/ScrollCaptureSession.java +++ b/core/java/android/view/ScrollCaptureSession.java @@ -16,18 +16,15 @@ package android.view; +import static java.util.Objects.requireNonNull; + import android.annotation.NonNull; -import android.annotation.Nullable; import android.graphics.Point; import android.graphics.Rect; /** * A session represents the scope of interaction between a {@link ScrollCaptureCallback} and the - * system during an active scroll capture operation. During the scope of a session, a callback - * will receive a series of requests for image data. Resources provided here are valid for use - * until {@link ScrollCaptureCallback#onScrollCaptureEnd(Runnable)}. - * - * @hide + * system during an active scroll capture operation. */ public class ScrollCaptureSession { @@ -35,22 +32,27 @@ public class ScrollCaptureSession { private final Rect mScrollBounds; private final Point mPositionInWindow; - @Nullable - private ScrollCaptureConnection mConnection; - - /** @hide */ - public ScrollCaptureSession(Surface surface, Rect scrollBounds, Point positionInWindow, - ScrollCaptureConnection connection) { - mSurface = surface; - mScrollBounds = scrollBounds; - mPositionInWindow = positionInWindow; - mConnection = connection; + /** + * Constructs a new session instance. + * + * @param surface the surface to consume generated images + * @param scrollBounds the bounds of the capture area within the containing view + * @param positionInWindow the offset of scrollBounds within the window + */ + public ScrollCaptureSession(@NonNull Surface surface, @NonNull Rect scrollBounds, + @NonNull Point positionInWindow) { + mSurface = requireNonNull(surface); + mScrollBounds = requireNonNull(scrollBounds); + mPositionInWindow = requireNonNull(positionInWindow); } /** * Returns a * <a href="https://source.android.com/devices/graphics/arch-bq-gralloc">BufferQueue</a> in the * form of a {@link Surface} for transfer of image buffers. + * <p> + * The surface is guaranteed to remain {@link Surface#isValid() valid} until the session + * {@link ScrollCaptureCallback#onScrollCaptureEnd(Runnable) ends}. * * @return the surface for transferring image buffers * @throws IllegalStateException if the session has been closed @@ -80,26 +82,4 @@ public class ScrollCaptureSession { public Point getPositionInWindow() { return mPositionInWindow; } - - /** - * Notify the system that an a buffer has been posted via the getSurface() channel. - * - * @param frameNumber the frame number of the queued buffer - * @param capturedArea the area captured, relative to scroll bounds - */ - public void notifyBufferSent(long frameNumber, @NonNull Rect capturedArea) { - if (mConnection != null) { - mConnection.onRequestImageCompleted(frameNumber, capturedArea); - } - } - - /** - * @hide - */ - public void disconnect() { - mConnection = null; - if (mSurface.isValid()) { - mSurface.release(); - } - } } diff --git a/core/java/android/view/ScrollCaptureTarget.java b/core/java/android/view/ScrollCaptureTarget.java index f3fcabb26b31..4fd48892da70 100644 --- a/core/java/android/view/ScrollCaptureTarget.java +++ b/core/java/android/view/ScrollCaptureTarget.java @@ -22,14 +22,16 @@ import android.annotation.UiThread; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; +import android.os.CancellationSignal; import com.android.internal.util.FastMath; +import java.io.PrintWriter; +import java.util.function.Consumer; + /** * A target collects the set of contextual information for a ScrollCaptureHandler discovered during * a {@link View#dispatchScrollCaptureSearch scroll capture search}. - * - * @hide */ public final class ScrollCaptureTarget { private final View mContainingView; @@ -41,7 +43,6 @@ public final class ScrollCaptureTarget { private final float[] mTmpFloatArr = new float[2]; private final Matrix mMatrixViewLocalToWindow = new Matrix(); - private final Rect mTmpRect = new Rect(); public ScrollCaptureTarget(@NonNull View scrollTarget, @NonNull Rect localVisibleRect, @NonNull Point positionInWindow, @NonNull ScrollCaptureCallback callback) { @@ -52,7 +53,10 @@ public final class ScrollCaptureTarget { mPositionInWindow = positionInWindow; } - /** @return the hint that the {@code containing view} had during the scroll capture search */ + /** + * @return the hint that the {@code containing view} had during the scroll capture search + * @see View#getScrollCaptureHint() + */ @View.ScrollCaptureHint public int getHint() { return mHint; @@ -71,8 +75,7 @@ public final class ScrollCaptureTarget { } /** - * Returns the un-clipped, visible bounds of the containing view during the scroll capture - * search. This is used to determine on-screen area to assist in selecting the primary target. + * Returns the visible bounds of the containing view. * * @return the visible bounds of the {@code containing view} in view-local coordinates */ @@ -81,13 +84,17 @@ public final class ScrollCaptureTarget { return mLocalVisibleRect; } - /** @return the position of the {@code containing view} within the window */ + /** @return the position of the visible bounds of the containing view within the window */ @NonNull public Point getPositionInWindow() { return mPositionInWindow; } - /** @return the {@code scroll bounds} for this {@link ScrollCaptureCallback callback} */ + /** + * @return the {@code scroll bounds} for this {@link ScrollCaptureCallback callback} + * + * @see ScrollCaptureCallback#onScrollCaptureSearch(CancellationSignal, Consumer) + */ @Nullable public Rect getScrollBounds() { return mScrollBounds; @@ -119,8 +126,8 @@ public final class ScrollCaptureTarget { } /** - * Refresh the value of {@link #mLocalVisibleRect} and {@link #mPositionInWindow} based on the - * current state of the {@code containing view}. + * Refresh the local visible bounds and it's offset within the window, based on the current + * state of the {@code containing view}. */ @UiThread public void updatePositionInWindow() { @@ -132,4 +139,27 @@ public final class ScrollCaptureTarget { roundIntoPoint(mPositionInWindow, mTmpFloatArr); } + public String toString() { + return "ScrollCaptureTarget{" + "view=" + mContainingView + + ", callback=" + mCallback + + ", scrollBounds=" + mScrollBounds + + ", localVisibleRect=" + mLocalVisibleRect + + ", positionInWindow=" + mPositionInWindow + + "}"; + } + + void dump(@NonNull PrintWriter writer) { + View view = getContainingView(); + writer.println("view: " + view); + writer.println("hint: " + mHint); + writer.println("callback: " + mCallback); + writer.println("scrollBounds: " + + (mScrollBounds == null ? "null" : mScrollBounds.toShortString())); + Point inWindow = getPositionInWindow(); + writer.println("positionInWindow: " + + ((inWindow == null) ? "null" : "[" + inWindow.x + "," + inWindow.y + "]")); + Rect localVisible = getLocalVisibleRect(); + writer.println("localVisibleRect: " + + (localVisible == null ? "null" : localVisible.toShortString())); + } } diff --git a/core/java/android/view/ScrollCaptureTargetResolver.java b/core/java/android/view/ScrollCaptureTargetResolver.java deleted file mode 100644 index e4316bbc9397..000000000000 --- a/core/java/android/view/ScrollCaptureTargetResolver.java +++ /dev/null @@ -1,337 +0,0 @@ -/* - * 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.view; - -import android.annotation.AnyThread; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.UiThread; -import android.graphics.Rect; -import android.os.Handler; -import android.os.Looper; -import android.os.SystemClock; -import android.util.Log; - -import com.android.internal.annotations.VisibleForTesting; - - -import java.util.Queue; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -/** - * Queries additional state from a list of {@link ScrollCaptureTarget targets} via asynchronous - * callbacks, then aggregates and reduces the target list to a single target, or null if no target - * is suitable. - * <p> - * The rules for selection are (in order): - * <ul> - * <li>prefer getScrollBounds(): non-empty - * <li>prefer View.getScrollCaptureHint == SCROLL_CAPTURE_HINT_INCLUDE - * <li>prefer descendants before parents - * <li>prefer larger area for getScrollBounds() (clipped to view bounds) - * </ul> - * - * <p> - * All calls to {@link ScrollCaptureCallback#onScrollCaptureSearch} are made on the main thread, - * with results are queued and consumed to the main thread as well. - * - * @see #start(Handler, long, Consumer) - * - * @hide - */ -@UiThread -public class ScrollCaptureTargetResolver { - private static final String TAG = "ScrollCaptureTargetRes"; - - private final Object mLock = new Object(); - - private final Queue<ScrollCaptureTarget> mTargets; - private Handler mHandler; - private long mTimeLimitMillis; - - private Consumer<ScrollCaptureTarget> mWhenComplete; - private int mPendingBoundsRequests; - private long mDeadlineMillis; - - private ScrollCaptureTarget mResult; - private boolean mFinished; - - private boolean mStarted; - - private static int area(Rect r) { - return r.width() * r.height(); - } - - private static boolean nullOrEmpty(Rect r) { - return r == null || r.isEmpty(); - } - - /** - * Binary operator which selects the best {@link ScrollCaptureTarget}. - */ - private static ScrollCaptureTarget chooseTarget(ScrollCaptureTarget a, ScrollCaptureTarget b) { - if (a == null && b == null) { - return null; - } else if (a == null || b == null) { - ScrollCaptureTarget c = (a == null) ? b : a; - return c; - } - - boolean emptyScrollBoundsA = nullOrEmpty(a.getScrollBounds()); - boolean emptyScrollBoundsB = nullOrEmpty(b.getScrollBounds()); - if (emptyScrollBoundsA || emptyScrollBoundsB) { - if (emptyScrollBoundsA && emptyScrollBoundsB) { - // Both have an empty or null scrollBounds - Log.d(TAG, "chooseTarget: (both have empty or null bounds) return " + null); - return null; - } - // Prefer the one with a non-empty scroll bounds - if (emptyScrollBoundsA) { - Log.d(TAG, "chooseTarget: (a has empty or null bounds) return " + b); - return b; - } - Log.d(TAG, "chooseTarget: (b has empty or null bounds) return " + a); - return a; - } - - final View viewA = a.getContainingView(); - final View viewB = b.getContainingView(); - - // Prefer any view with scrollCaptureHint="INCLUDE", over one without - // This is an escape hatch for the next rule (descendants first) - boolean hintIncludeA = hasIncludeHint(viewA); - boolean hintIncludeB = hasIncludeHint(viewB); - if (hintIncludeA != hintIncludeB) { - ScrollCaptureTarget c = (hintIncludeA) ? a : b; - Log.d(TAG, "chooseTarget: (has hint=INCLUDE) return " + c); - return c; - } - - // If the views are relatives, prefer the descendant. This allows implementations to - // leverage nested scrolling APIs by interacting with the innermost scrollable view (as - // would happen with touch input). - if (isDescendant(viewA, viewB)) { - Log.d(TAG, "chooseTarget: (b is descendant of a) return " + b); - return b; - } - if (isDescendant(viewB, viewA)) { - Log.d(TAG, "chooseTarget: (a is descendant of b) return " + a); - return a; - } - - // finally, prefer one with larger scroll bounds - int scrollAreaA = area(a.getScrollBounds()); - int scrollAreaB = area(b.getScrollBounds()); - ScrollCaptureTarget c = (scrollAreaA >= scrollAreaB) ? a : b; - Log.d(TAG, "chooseTarget: return " + c); - return c; - } - - /** - * Creates an instance to query and filter {@code target}. - * - * @param targets a list of {@link ScrollCaptureTarget} as collected by {@link - * View#dispatchScrollCaptureSearch}. - * @see #start(Handler, long, Consumer) - */ - public ScrollCaptureTargetResolver(Queue<ScrollCaptureTarget> targets) { - mTargets = targets; - } - - void checkThread() { - if (mHandler.getLooper() != Looper.myLooper()) { - throw new IllegalStateException("Called from wrong thread! (" - + Thread.currentThread().getName() + ")"); - } - } - - /** - * Blocks until a result is returned (after completion or timeout). - * <p> - * For testing only. Normal usage should receive a callback after calling {@link #start}. - */ - @VisibleForTesting - public ScrollCaptureTarget waitForResult() throws InterruptedException { - synchronized (mLock) { - while (!mFinished) { - mLock.wait(); - } - } - return mResult; - } - - private void supplyResult(ScrollCaptureTarget target) { - checkThread(); - if (mFinished) { - return; - } - mResult = chooseTarget(mResult, target); - boolean finish = mPendingBoundsRequests == 0 - || SystemClock.uptimeMillis() >= mDeadlineMillis; - if (finish) { - mPendingBoundsRequests = 0; - mWhenComplete.accept(mResult); - synchronized (mLock) { - mFinished = true; - mLock.notify(); - } - mWhenComplete = null; - } - } - - /** - * Asks all targets for {@link ScrollCaptureCallback#onScrollCaptureSearch(Consumer) - * scrollBounds}, and selects the primary target according to the {@link - * #chooseTarget} function. - * - * @param timeLimitMillis the amount of time to wait for all responses before delivering the top - * result - * @param resultConsumer the consumer to receive the primary target - */ - @AnyThread - public void start(Handler uiHandler, long timeLimitMillis, - Consumer<ScrollCaptureTarget> resultConsumer) { - synchronized (mLock) { - if (mStarted) { - throw new IllegalStateException("already started!"); - } - if (timeLimitMillis < 0) { - throw new IllegalArgumentException("Time limit must be positive"); - } - mHandler = uiHandler; - mTimeLimitMillis = timeLimitMillis; - mWhenComplete = resultConsumer; - if (mTargets.isEmpty()) { - mHandler.post(() -> supplyResult(null)); - return; - } - mStarted = true; - uiHandler.post(this::run); - } - } - - private void run() { - checkThread(); - - mPendingBoundsRequests = mTargets.size(); - for (ScrollCaptureTarget target : mTargets) { - queryTarget(target); - } - mDeadlineMillis = SystemClock.uptimeMillis() + mTimeLimitMillis; - mHandler.postAtTime(mTimeoutRunnable, mDeadlineMillis); - } - - private final Runnable mTimeoutRunnable = () -> { - checkThread(); - supplyResult(null); - }; - - /** - * Adds a target to the list and requests {@link ScrollCaptureCallback#onScrollCaptureSearch} - * scrollBounds} from it. Results are returned by a call to {@link #onScrollBoundsProvided}. - * - * @param target the target to add - */ - @UiThread - private void queryTarget(@NonNull ScrollCaptureTarget target) { - checkThread(); - final ScrollCaptureCallback callback = target.getCallback(); - // from the UI thread, request scroll bounds - callback.onScrollCaptureSearch( - // allow only one callback to onReady.accept(): - new SingletonConsumer<Rect>( - // Queue and consume on the UI thread - ((scrollBounds) -> mHandler.post( - () -> onScrollBoundsProvided(target, scrollBounds))))); - } - - @UiThread - private void onScrollBoundsProvided(ScrollCaptureTarget target, @Nullable Rect scrollBounds) { - checkThread(); - if (mFinished) { - return; - } - - // Record progress. - mPendingBoundsRequests--; - - // Remove the timeout. - mHandler.removeCallbacks(mTimeoutRunnable); - - boolean doneOrTimedOut = mPendingBoundsRequests == 0 - || SystemClock.uptimeMillis() >= mDeadlineMillis; - - final View containingView = target.getContainingView(); - if (!nullOrEmpty(scrollBounds) && containingView.isAggregatedVisible()) { - target.updatePositionInWindow(); - target.setScrollBounds(scrollBounds); - supplyResult(target); - } - - if (!mFinished) { - // Reschedule the timeout. - mHandler.postAtTime(mTimeoutRunnable, mDeadlineMillis); - } - } - - private static boolean hasIncludeHint(View view) { - return (view.getScrollCaptureHint() & View.SCROLL_CAPTURE_HINT_INCLUDE) != 0; - } - - /** - * Determines if {@code otherView} is a descendant of {@code view}. - * - * @param view a view - * @param otherView another view - * @return true if {@code view} is an ancestor of {@code otherView} - */ - private static boolean isDescendant(@NonNull View view, @NonNull View otherView) { - if (view == otherView) { - return false; - } - ViewParent otherParent = otherView.getParent(); - while (otherParent != view && otherParent != null) { - otherParent = otherParent.getParent(); - } - return otherParent == view; - } - - /** - * A safe wrapper for a consumer callbacks intended to accept a single value. It ensures - * that the receiver of the consumer does not retain a reference to {@code target} after use nor - * cause race conditions by invoking {@link Consumer#accept accept} more than once. - */ - static class SingletonConsumer<T> implements Consumer<T> { - final AtomicReference<Consumer<T>> mAtomicRef; - - /** - * @param target the target consumer - **/ - SingletonConsumer(Consumer<T> target) { - mAtomicRef = new AtomicReference<>(target); - } - - @Override - public void accept(T t) { - final Consumer<T> consumer = mAtomicRef.getAndSet(null); - if (consumer != null) { - consumer.accept(t); - } - } - } -} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 3789324a038c..ebef4646b0d9 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -184,10 +184,10 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Queue; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.function.Predicate; /** @@ -1484,7 +1484,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see #getScrollCaptureHint() * @see #setScrollCaptureHint(int) - * @hide */ public static final int SCROLL_CAPTURE_HINT_AUTO = 0; @@ -1495,7 +1494,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see #getScrollCaptureHint() * @see #setScrollCaptureHint(int) - * @hide */ public static final int SCROLL_CAPTURE_HINT_EXCLUDE = 0x1; @@ -1506,7 +1504,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see #getScrollCaptureHint() * @see #setScrollCaptureHint(int) - * @hide */ public static final int SCROLL_CAPTURE_HINT_INCLUDE = 0x2; @@ -1517,7 +1514,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see #getScrollCaptureHint() * @see #setScrollCaptureHint(int) - * @hide */ public static final int SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS = 0x4; @@ -30053,8 +30049,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Returns the current scroll capture hint for this view. * * @return the current scroll capture hint - * - * @hide */ @ScrollCaptureHint public int getScrollCaptureHint() { @@ -30067,8 +30061,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * scroll capture targets. * * @param hint the scrollCaptureHint flags value to set - * - * @hide */ public void setScrollCaptureHint(@ScrollCaptureHint int hint) { mPrivateFlags4 &= ~PFLAG4_SCROLL_CAPTURE_HINT_MASK; @@ -30088,10 +30080,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * setting a custom callback to help ensure it is selected as the target. * * @param callback the new callback to assign - * - * @hide */ - public void setScrollCaptureCallback(@Nullable ScrollCaptureCallback callback) { + public final void setScrollCaptureCallback(@Nullable ScrollCaptureCallback callback) { getListenerInfo().mScrollCaptureCallback = callback; } @@ -30110,29 +30100,54 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Dispatch a scroll capture search request down the view hierarchy. + * + * @param localVisibleRect the visible area of this ViewGroup in local coordinates, according to + * the parent + * @param windowOffset the offset of this view within the window + * @param targets accepts potential scroll capture targets; {@link Consumer#accept + * results.accept} may be called zero or more times on the calling + * thread before onScrollCaptureSearch returns + */ + public void dispatchScrollCaptureSearch( + @NonNull Rect localVisibleRect, @NonNull Point windowOffset, + @NonNull Consumer<ScrollCaptureTarget> targets) { + onScrollCaptureSearch(localVisibleRect, windowOffset, targets); + } + + /** * Called when scroll capture is requested, to search for appropriate content to scroll. If * applicable, this view adds itself to the provided list for consideration, subject to the * flags set by {@link #setScrollCaptureHint}. * * @param localVisibleRect the local visible rect of this view * @param windowOffset the offset of localVisibleRect within the window - * @param targets a queue which collects potential targets - * + * @param targets accepts potential scroll capture targets; {@link Consumer#accept + * results.accept} may be called zero or more times on the calling + * thread before onScrollCaptureSearch returns * @throws IllegalStateException if this view is not attached to a window - * @hide */ - public void dispatchScrollCaptureSearch(@NonNull Rect localVisibleRect, - @NonNull Point windowOffset, @NonNull Queue<ScrollCaptureTarget> targets) { + public void onScrollCaptureSearch(@NonNull Rect localVisibleRect, + @NonNull Point windowOffset, @NonNull Consumer<ScrollCaptureTarget> targets) { int hint = getScrollCaptureHint(); if ((hint & SCROLL_CAPTURE_HINT_EXCLUDE) != 0) { return; } + boolean rectIsVisible = true; + + // Apply clipBounds if present. + if (mClipBounds != null) { + rectIsVisible = localVisibleRect.intersect(mClipBounds); + } + if (!rectIsVisible) { + return; + } // Get a callback provided by the framework, library or application. ScrollCaptureCallback callback = (mListenerInfo == null) ? null : mListenerInfo.mScrollCaptureCallback; - // Try internal support for standard scrolling containers. + // Try framework support for standard scrolling containers. if (callback == null) { callback = createScrollCaptureCallbackInternal(localVisibleRect, windowOffset); } @@ -30142,7 +30157,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Add to the list for consideration Point offset = new Point(windowOffset.x, windowOffset.y); Rect rect = new Rect(localVisibleRect); - targets.add(new ScrollCaptureTarget(this, rect, offset, callback)); + targets.accept(new ScrollCaptureTarget(this, rect, offset, callback)); } } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 37bea5821e42..38a59373554c 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -77,7 +77,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Queue; +import java.util.function.Consumer; import java.util.function.Predicate; /** @@ -7463,92 +7463,73 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * Offsets the given rectangle in parent's local coordinates into child's coordinate space - * and clips the result to the child View's bounds, padding and clipRect if appropriate. If the - * resulting rectangle is not empty, the request is forwarded to the child. - * <p> - * Note: This method does not account for any static View transformations which may be - * applied to the child view. - * - * @param child the child to dispatch to - * @param localVisibleRect the visible (clipped) area of this ViewGroup, in local coordinates - * @param windowOffset the offset of localVisibleRect within the window - * @param targets a queue to collect located targets - */ - private void dispatchTransformedScrollCaptureSearch(View child, Rect localVisibleRect, - Point windowOffset, Queue<ScrollCaptureTarget> targets) { - - // copy local visible rect for modification and dispatch - final Rect childVisibleRect = getTempRect(); - childVisibleRect.set(localVisibleRect); - - // transform to child coords - final Point childWindowOffset = getTempPoint(); - childWindowOffset.set(windowOffset.x, windowOffset.y); - - final int dx = child.mLeft - mScrollX; - final int dy = child.mTop - mScrollY; - - childVisibleRect.offset(-dx, -dy); - childWindowOffset.offset(dx, dy); - - boolean rectIsVisible = true; - final int width = mRight - mLeft; - final int height = mBottom - mTop; - - // Clip to child bounds - if (getClipChildren()) { - rectIsVisible = childVisibleRect.intersect(0, 0, child.getWidth(), child.getHeight()); - } - - // Clip to child padding. - if (rectIsVisible && (child instanceof ViewGroup) - && ((ViewGroup) child).getClipToPadding()) { - rectIsVisible = childVisibleRect.intersect( - child.mPaddingLeft, child.mPaddingTop, - child.getWidth() - child.mPaddingRight, - child.getHeight() - child.mPaddingBottom); - } - // Clip to child clipBounds. - if (rectIsVisible && child.mClipBounds != null) { - rectIsVisible = childVisibleRect.intersect(child.mClipBounds); - } - if (rectIsVisible) { - child.dispatchScrollCaptureSearch(childVisibleRect, childWindowOffset, targets); - } - } - - /** * Handle the scroll capture search request by checking this view if applicable, then to each * child view. * * @param localVisibleRect the visible area of this ViewGroup in local coordinates, according to * the parent * @param windowOffset the offset of this view within the window - * @param targets the collected list of scroll capture targets - * - * @hide + * @param targets accepts potential scroll capture targets; {@link Consumer#accept + * results.accept} may be called zero or more times on the calling + * thread before onScrollCaptureSearch returns */ @Override public void dispatchScrollCaptureSearch( @NonNull Rect localVisibleRect, @NonNull Point windowOffset, - @NonNull Queue<ScrollCaptureTarget> targets) { + @NonNull Consumer<ScrollCaptureTarget> targets) { + + // copy local visible rect for modification and dispatch + final Rect rect = getTempRect(); + rect.set(localVisibleRect); + + if (getClipToPadding()) { + rect.inset(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom); + } // Dispatch to self first. super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets); - // Then dispatch to children, if not excluding descendants. - if ((getScrollCaptureHint() & SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS) == 0) { - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - // Only visible views can be captured. - if (child.getVisibility() != View.VISIBLE) { - continue; - } - // Transform to child coords and dispatch - dispatchTransformedScrollCaptureSearch(child, localVisibleRect, windowOffset, - targets); + // Skip children if descendants excluded. + if ((getScrollCaptureHint() & SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS) != 0) { + return; + } + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + // Only visible views can be captured. + if (child.getVisibility() != View.VISIBLE) { + continue; + } + // Offset the given rectangle (in parent's local coordinates) into child's coordinate + // space and clip the result to the child View's bounds, padding and clipRect as needed. + // If the resulting rectangle is not empty, the request is forwarded to the child. + + // copy local visible rect for modification and dispatch + final Rect childVisibleRect = getTempRect(); + childVisibleRect.set(localVisibleRect); + + // transform to child coords + final Point childWindowOffset = getTempPoint(); + childWindowOffset.set(windowOffset.x, windowOffset.y); + + final int dx = child.mLeft - mScrollX; + final int dy = child.mTop - mScrollY; + + childVisibleRect.offset(-dx, -dy); + childWindowOffset.offset(dx, dy); + + boolean rectIsVisible = true; + + // Clip to child bounds + if (getClipChildren()) { + rectIsVisible = childVisibleRect.intersect(0, 0, child.getWidth(), + child.getHeight()); + } + + // Clip to child padding. + if (rectIsVisible) { + child.dispatchScrollCaptureSearch(childVisibleRect, childWindowOffset, targets); } } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 1abcb15ca7a3..4f6679b2bc97 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -141,6 +141,7 @@ import android.util.AndroidRuntimeException; import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.EventLog; +import android.util.IndentingPrintWriter; import android.util.Log; import android.util.LongArray; import android.util.MergedConfiguration; @@ -202,6 +203,7 @@ import com.android.internal.view.SurfaceCallbackHelper; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; +import java.io.StringWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashSet; @@ -274,6 +276,11 @@ public final class ViewRootImpl implements ViewParent, */ private static final int CONTENT_CAPTURE_ENABLED_FALSE = 2; + /** + * Maximum time to wait for {@link View#dispatchScrollCaptureSearch} to complete. + */ + private static final int SCROLL_CAPTURE_REQUEST_TIMEOUT_MILLIS = 2500; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>(); @@ -668,8 +675,6 @@ public final class ViewRootImpl implements ViewParent, private final InsetsController mInsetsController; private final ImeFocusController mImeFocusController; - private ScrollCaptureConnection mScrollCaptureConnection; - private boolean mIsSurfaceOpaque; private final BackgroundBlurDrawable.Aggregator mBlurRegionAggregator = @@ -683,12 +688,6 @@ public final class ViewRootImpl implements ViewParent, return mImeFocusController; } - /** @return The current {@link ScrollCaptureConnection} for this instance, if any is active. */ - @Nullable - public ScrollCaptureConnection getScrollCaptureConnection() { - return mScrollCaptureConnection; - } - private final GestureExclusionTracker mGestureExclusionTracker = new GestureExclusionTracker(); private IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection; @@ -730,6 +729,8 @@ public final class ViewRootImpl implements ViewParent, private HashSet<ScrollCaptureCallback> mRootScrollCaptureCallbacks; + private long mScrollCaptureRequestTimeout = SCROLL_CAPTURE_REQUEST_TIMEOUT_MILLIS; + /** * Increment this value when the surface has been replaced. */ @@ -817,6 +818,8 @@ public final class ViewRootImpl implements ViewParent, mImeFocusController = new ImeFocusController(this); AudioManager audioManager = mContext.getSystemService(AudioManager.class); mFastScrollSoundEffectsEnabled = audioManager.areNavigationRepeatSoundEffectsEnabled(); + + mScrollCaptureRequestTimeout = SCROLL_CAPTURE_REQUEST_TIMEOUT_MILLIS; } public static void addFirstDrawHandler(Runnable callback) { @@ -9223,9 +9226,9 @@ public final class ViewRootImpl implements ViewParent, * Collect and include any ScrollCaptureCallback instances registered with the window. * * @see #addScrollCaptureCallback(ScrollCaptureCallback) - * @param targets the search queue for targets + * @param results an object to collect the results of the search */ - private void collectRootScrollCaptureTargets(Queue<ScrollCaptureTarget> targets) { + private void collectRootScrollCaptureTargets(ScrollCaptureSearchResults results) { if (mRootScrollCaptureCallbacks == null) { return; } @@ -9233,26 +9236,45 @@ public final class ViewRootImpl implements ViewParent, // Add to the list for consideration Point offset = new Point(mView.getLeft(), mView.getTop()); Rect rect = new Rect(0, 0, mView.getWidth(), mView.getHeight()); - targets.add(new ScrollCaptureTarget(mView, rect, offset, cb)); + results.addTarget(new ScrollCaptureTarget(mView, rect, offset, cb)); } } /** - * Handles an inbound request for scroll capture from the system. If a client is not already - * active, a search will be dispatched through the view tree to locate scrolling content. + * Update the timeout for scroll capture requests. Only affects this view root. + * The default value is {@link #SCROLL_CAPTURE_REQUEST_TIMEOUT_MILLIS}. + * + * @param timeMillis the new timeout in milliseconds + */ + public void setScrollCaptureRequestTimeout(int timeMillis) { + mScrollCaptureRequestTimeout = timeMillis; + } + + /** + * Get the current timeout for scroll capture requests. + * + * @return the timeout in milliseconds + */ + public long getScrollCaptureRequestTimeout() { + return mScrollCaptureRequestTimeout; + } + + /** + * Handles an inbound request for scroll capture from the system. A search will be + * dispatched through the view tree to locate scrolling content. * <p> - * Either {@link IScrollCaptureCallbacks#onConnected(IScrollCaptureConnection, Rect, - * Point)} or {@link IScrollCaptureCallbacks#onUnavailable()} will be returned - * depending on the results of the search. + * A call to {@link IScrollCaptureCallbacks#onScrollCaptureResponse(ScrollCaptureResponse)} + * will follow. * * @param callbacks to receive responses - * @see ScrollCaptureTargetResolver + * @see ScrollCaptureTargetSelector */ public void handleScrollCaptureRequest(@NonNull IScrollCaptureCallbacks callbacks) { - LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + ScrollCaptureSearchResults results = + new ScrollCaptureSearchResults(mContext.getMainExecutor()); // Window (root) level callbacks - collectRootScrollCaptureTargets(targetList); + collectRootScrollCaptureTargets(results); // Search through View-tree View rootView = getView(); @@ -9260,58 +9282,70 @@ public final class ViewRootImpl implements ViewParent, Point point = new Point(); Rect rect = new Rect(0, 0, rootView.getWidth(), rootView.getHeight()); getChildVisibleRect(rootView, rect, point); - rootView.dispatchScrollCaptureSearch(rect, point, targetList); + rootView.dispatchScrollCaptureSearch(rect, point, results::addTarget); } - - // No-op path. Scroll capture not offered for this window. - if (targetList.isEmpty()) { - dispatchScrollCaptureSearchResult(callbacks, null); - return; + Runnable onComplete = () -> dispatchScrollCaptureSearchResult(callbacks, results); + results.setOnCompleteListener(onComplete); + if (!results.isComplete()) { + mHandler.postDelayed(results::finish, getScrollCaptureRequestTimeout()); } - - // Request scrollBounds from each of the targets. - // Continues with the consumer once all responses are consumed, or the timeout expires. - ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetList); - resolver.start(mHandler, 1000, - (selected) -> dispatchScrollCaptureSearchResult(callbacks, selected)); } /** Called by {@link #handleScrollCaptureRequest} when a result is returned */ private void dispatchScrollCaptureSearchResult( @NonNull IScrollCaptureCallbacks callbacks, - @Nullable ScrollCaptureTarget selectedTarget) { + @NonNull ScrollCaptureSearchResults results) { + + ScrollCaptureTarget selectedTarget = results.getTopResult(); + + ScrollCaptureResponse.Builder response = new ScrollCaptureResponse.Builder(); + response.setWindowTitle(getTitle().toString()); + + StringWriter writer = new StringWriter(); + IndentingPrintWriter pw = new IndentingPrintWriter(writer); + results.dump(pw); + pw.flush(); + response.addMessage(writer.toString()); - // If timeout or no eligible targets found. if (selectedTarget == null) { + response.setDescription("No scrollable targets found in window"); try { - if (DEBUG_SCROLL_CAPTURE) { - Log.d(TAG, "scrollCaptureSearch returned no targets available."); - } - callbacks.onUnavailable(); + callbacks.onScrollCaptureResponse(response.build()); } catch (RemoteException e) { - if (DEBUG_SCROLL_CAPTURE) { - Log.w(TAG, "Failed to send scroll capture search result.", e); - } + Log.e(TAG, "Failed to send scroll capture search result", e); } return; } - // Create a client instance and return it to the caller - mScrollCaptureConnection = new ScrollCaptureConnection(selectedTarget, callbacks); + response.setDescription("Connected"); + + // Compute area covered by scrolling content within window + Rect boundsInWindow = new Rect(); + View containingView = selectedTarget.getContainingView(); + containingView.getLocationInWindow(mAttachInfo.mTmpLocation); + boundsInWindow.set(selectedTarget.getScrollBounds()); + boundsInWindow.offset(mAttachInfo.mTmpLocation[0], mAttachInfo.mTmpLocation[1]); + response.setBoundsInWindow(boundsInWindow); + + // Compute the area on screen covered by the window + Rect boundsOnScreen = new Rect(); + mView.getLocationOnScreen(mAttachInfo.mTmpLocation); + boundsOnScreen.set(0, 0, mView.getWidth(), mView.getHeight()); + boundsOnScreen.offset(mAttachInfo.mTmpLocation[0], mAttachInfo.mTmpLocation[1]); + response.setWindowBounds(boundsOnScreen); + + // Create a connection and return it to the caller + ScrollCaptureConnection connection = new ScrollCaptureConnection( + mView.getContext().getMainExecutor(), selectedTarget, callbacks); + response.setConnection(connection); + try { - if (DEBUG_SCROLL_CAPTURE) { - Log.d(TAG, "scrollCaptureSearch returning client: " + getScrollCaptureConnection()); - } - callbacks.onConnected( - mScrollCaptureConnection, - selectedTarget.getScrollBounds(), - selectedTarget.getPositionInWindow()); + callbacks.onScrollCaptureResponse(response.build()); } catch (RemoteException e) { if (DEBUG_SCROLL_CAPTURE) { - Log.w(TAG, "Failed to send scroll capture search result.", e); + Log.w(TAG, "Failed to send scroll capture search response.", e); } - mScrollCaptureConnection.disconnect(); - mScrollCaptureConnection = null; + connection.close(); } } diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 4ecdd78f5a42..221b3346df58 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -2621,7 +2621,6 @@ public abstract class Window { * callback with the root view of the window. * * @param callback the callback to add - * @hide */ public void registerScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) { } @@ -2630,7 +2629,6 @@ public abstract class Window { * Unregisters a {@link ScrollCaptureCallback} previously registered with this window. * * @param callback the callback to remove - * @hide */ public void unregisterScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) { } |
