Merge "Long screenshots framework update and API unhide" into sc-dev
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
core/java/android/view/ScrollCaptureResponse.aidl
Normal file
19
core/java/android/view/ScrollCaptureResponse.aidl
Normal 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;
|
||||
381
core/java/android/view/ScrollCaptureResponse.java
Normal file
381
core/java/android/view/ScrollCaptureResponse.java
Normal 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
|
||||
|
||||
}
|
||||
271
core/java/android/view/ScrollCaptureSearchResults.java
Normal file
271
core/java/android/view/ScrollCaptureSearchResults.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user