Merge "Long screenshots framework update and API unhide" into sc-dev

This commit is contained in:
Mark Renouf
2021-02-26 13:06:47 +00:00
committed by Android (Google) Code Review
30 changed files with 2194 additions and 1788 deletions

View File

@@ -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();
}

View File

@@ -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.
*/
oneway void startCapture(in Surface surface);
/**
* Request the client capture an image within the provided rectangle.
* Informs the target that it has been selected for scroll capture.
*
* @see android.view.ScrollCaptureCallback#onScrollCaptureRequest
* @param surface a return channel for image buffers
*
* @return a cancallation signal which is used cancel the request
*/
oneway void requestImage(in Rect captureArea);
ICancellationSignal startCapture(in Surface surface);
/**
* 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.
* 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 endCapture();
ICancellationSignal requestImage(in Rect captureArea);
/**
* Inform the target that capture has ended.
*
* @return a cancallation signal which is used cancel the request
*/
ICancellationSignal endCapture();
/**
* Closes the connection.
*/
oneway void close();
}

View File

@@ -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.
* 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>
* 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.
* 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>
* Responses received after XXXms will be discarded.
* <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.
* 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>
* 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.
* 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>
* 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.
* <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.
* 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>
* <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.
* <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

View File

@@ -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();
}
});
mConnected = true;
}
@VisibleForTesting
public void setTimeoutMillis(int timeoutMillis) {
mTimeoutMillis = timeoutMillis;
}
@VisibleForTesting
public DelayedAction getTimeoutAction() {
return mTimeoutAction;
}
private void checkConnected() {
if (mSelectedTarget == null || mCallbacks == null) {
throw new IllegalStateException("This client has been disconnected.");
}
}
private void checkStarted() {
if (mSession == null) {
throw new IllegalStateException("Capture session has not been started!");
}
}
@WorkerThread // IScrollCaptureConnection
@BinderThread
@Override
public void startCapture(Surface surface) throws RemoteException {
public ICancellationSignal 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));
if (!surface.isValid()) {
throw new RemoteException(new IllegalArgumentException("surface must be valid"));
}
ICancellationSignal cancellation = CancellationSignal.createTransport();
mCancellation = CancellationSignal.fromTransport(cancellation);
mSession = new ScrollCaptureSession(surface, mScrollBounds, mPositionInWindow);
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)));
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
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();
}
});
}
}
@UiThread
private void onRequestImageTimeout() {
endCapture();
}
@WorkerThread // IScrollCaptureConnection
@Override
public void endCapture() {
if (isStarted()) {
scheduleTimeout(mTimeoutMillis, this::onEndCaptureTimeout);
mHandler.post(() ->
mSelectedTarget.getCallback().onScrollCaptureEnd(this::onEndCaptureCompleted));
} else {
disconnect();
}
}
private boolean isStarted() {
return mCallbacks != null && mSelectedTarget != null;
}
@UiThread
private void onEndCaptureCompleted() { // onEndCaptureCompleted
if (cancelTimeout()) {
doShutdown();
}
}
@UiThread
private void onEndCaptureTimeout() {
doShutdown();
}
private void doShutdown() {
void onImageRequestCompleted(Rect capturedArea) {
try {
if (mCallbacks != null) {
mCallbacks.onConnectionClosed();
}
mRemote.onImageRequestCompleted(0, capturedArea);
} catch (RemoteException e) {
// Ignore
} finally {
disconnect();
Log.w(TAG, "Shutting down due to error: ", e);
close();
}
}
@BinderThread
@Override
public ICancellationSignal endCapture() throws RemoteException {
checkConnected();
checkStarted();
ICancellationSignal cancellation = CancellationSignal.createTransport();
mCancellation = CancellationSignal.fromTransport(cancellation);
Runnable listener =
SafeCallback.create(mCancellation, mUiThread, this::onEndCaptureCompleted);
// -> UiThread
mUiThread.execute(() -> mLocal.onScrollCaptureEnd(listener));
return cancellation;
}
@UiThread
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.");
// -> 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();
}
}
mSelectedTarget = null;
mCallbacks = null;
public boolean isConnected() {
return mConnected;
}
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;
}
private void scheduleTimeout(long timeoutMillis, Runnable action) {
if (mTimeoutAction != null) {
mTimeoutAction.cancel();
}
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;
public CancellationSignal getCancellation() {
return mCancellation;
}
@VisibleForTesting
public DelayedAction(Handler handler, long timeoutMillis, Runnable action) {
mHandler = handler;
mAction = action;
mHandler.postDelayed(this::onTimeout, mToken, timeoutMillis);
}
private boolean onTimeout() {
if (mCompleted.compareAndSet(false, true)) {
mAction.run();
return true;
protected void finalize() throws Throwable {
try {
if (mCloseGuard != null) {
mCloseGuard.warnIfOpen();
}
return false;
close();
} finally {
super.finalize();
}
}
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;
}
/**
* 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();
}
/**
* 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;
// 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;
}
mHandler.removeCallbacksAndMessages(mToken);
return true;
mExecuted = true;
if (mSignal.isCanceled()) {
return;
}
T target = mTargetRef.get();
if (target == null) {
return;
}
mExecutor.execute(() -> targetConsumer.accept(target));
}
static Runnable create(CancellationSignal signal, Executor executor, Runnable target) {
return new RunnableCallback(signal, executor, target);
}
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));
}
}
}

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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()));
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
@@ -30109,6 +30099,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
windowOffset);
}
/**
* 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
@@ -30116,23 +30122,32 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*
* @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));
}
}

View File

@@ -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;
/**
@@ -7462,62 +7462,6 @@ 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.
@@ -7525,30 +7469,67 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
* @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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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) {
}