summaryrefslogtreecommitdiff
path: root/core/java/android
diff options
context:
space:
mode:
authorMark Renouf <mrenouf@google.com>2021-02-26 13:06:47 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2021-02-26 13:06:47 +0000
commit20bd3d4935bdd00a1603cd14b03d1529e694125d (patch)
treef8a6ab8b7c63a9577da26759bf281b3be55a115d /core/java/android
parentf39926455750b2a1354ac0497d18522e0d24690a (diff)
parent8f1b73e1839803c9d8665846e50a3010697fe8b9 (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.aidl36
-rw-r--r--core/java/android/view/IScrollCaptureConnection.aidl34
-rw-r--r--core/java/android/view/ScrollCaptureCallback.java103
-rw-r--r--core/java/android/view/ScrollCaptureConnection.java373
-rw-r--r--core/java/android/view/ScrollCaptureResponse.aidl19
-rw-r--r--core/java/android/view/ScrollCaptureResponse.java381
-rw-r--r--core/java/android/view/ScrollCaptureSearchResults.java271
-rw-r--r--core/java/android/view/ScrollCaptureSession.java56
-rw-r--r--core/java/android/view/ScrollCaptureTarget.java50
-rw-r--r--core/java/android/view/ScrollCaptureTargetResolver.java337
-rw-r--r--core/java/android/view/View.java53
-rw-r--r--core/java/android/view/ViewGroup.java127
-rw-r--r--core/java/android/view/ViewRootImpl.java138
-rw-r--r--core/java/android/view/Window.java2
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) {
}