Merge "Scroll Capture Framework" into rvc-dev am: 03f9a814a5
Change-Id: Ie371aafcdce9db0a2709eb495539a9e5d29260f1
This commit is contained in:
49
core/java/android/view/IScrollCaptureClient.aidl
Normal file
49
core/java/android/view/IScrollCaptureClient.aidl
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.graphics.Rect;
|
||||
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.
|
||||
*
|
||||
* {@hide}
|
||||
*/
|
||||
interface IScrollCaptureClient {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @see android.view.ScrollCaptureCallback#onScrollCaptureRequest
|
||||
*/
|
||||
oneway void requestImage(in Rect captureArea);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
oneway void endCapture();
|
||||
}
|
||||
63
core/java/android/view/IScrollCaptureController.aidl
Normal file
63
core/java/android/view/IScrollCaptureController.aidl
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.view.Surface;
|
||||
|
||||
import android.view.IScrollCaptureClient;
|
||||
|
||||
/**
|
||||
* Interface to a controller passed to the {@link IScrollCaptureClient} which provides the client an
|
||||
* asynchronous callback channel for responses.
|
||||
*
|
||||
* {@hide}
|
||||
*/
|
||||
interface IScrollCaptureController {
|
||||
/**
|
||||
* Scroll capture is available, and a client connect has been returned.
|
||||
*
|
||||
* @param client interface to a ScrollCaptureCallback in the window process
|
||||
* @param scrollAreaInWindow the location of scrolling in global (window) coordinate space
|
||||
*/
|
||||
oneway void onClientConnected(in IScrollCaptureClient client, in Rect scrollBounds,
|
||||
in Point positionInWindow);
|
||||
|
||||
/**
|
||||
* Nothing in the window can be scrolled, scroll capture not offered.
|
||||
*/
|
||||
oneway void onClientUnavailable();
|
||||
|
||||
/**
|
||||
* Notifies the system that the client has confirmed the request and is ready to begin providing
|
||||
* image requests.
|
||||
*/
|
||||
oneway void onCaptureStarted();
|
||||
|
||||
/**
|
||||
* Received a response from a capture request.
|
||||
*/
|
||||
oneway void onCaptureBufferSent(long frameNumber, 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.
|
||||
*/
|
||||
oneway void onConnectionClosed();
|
||||
}
|
||||
@@ -21,15 +21,16 @@ import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.MergedConfiguration;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.DragEvent;
|
||||
import android.view.InsetsSourceControl;
|
||||
import android.view.InsetsState;
|
||||
import android.view.IScrollCaptureController;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.InsetsState;
|
||||
import android.view.InsetsSourceControl;
|
||||
|
||||
import com.android.internal.os.IResultReceiver;
|
||||
import android.util.MergedConfiguration;
|
||||
|
||||
/**
|
||||
* API back to a client window that the Window Manager uses to inform it of
|
||||
@@ -139,4 +140,11 @@ oneway interface IWindow {
|
||||
* Tell the window that it is either gaining or losing pointer capture.
|
||||
*/
|
||||
void dispatchPointerCaptureChanged(boolean hasCapture);
|
||||
|
||||
/**
|
||||
* Called when Scroll Capture support is requested for a window.
|
||||
*
|
||||
* @param controller the controller to receive responses
|
||||
*/
|
||||
void requestScrollCapture(in IScrollCaptureController controller);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import android.view.IDisplayFoldListener;
|
||||
import android.view.IDisplayWindowRotationController;
|
||||
import android.view.IOnKeyguardExitResult;
|
||||
import android.view.IPinnedStackListener;
|
||||
import android.view.IScrollCaptureController;
|
||||
import android.view.RemoteAnimationAdapter;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.ISystemGestureExclusionListener;
|
||||
@@ -749,4 +750,18 @@ interface IWindowManager
|
||||
* @param flags see definition in SurfaceTracing.cpp
|
||||
*/
|
||||
void setLayerTracingFlags(int flags);
|
||||
|
||||
/**
|
||||
* Forwards a scroll capture request to the appropriate window, if available.
|
||||
*
|
||||
* @param displayId the id of the display to target
|
||||
* @param behindClient token for a window, used to filter the search to windows behind it, or
|
||||
* {@code null} to accept a window at any zOrder
|
||||
* @param taskId specifies the id of a task the result must belong to, or -1 to ignore task ids
|
||||
* @param controller the controller to receive results, a call to either
|
||||
* {@link IScrollCaptureController#onClientConnected} or
|
||||
* {@link IScrollCaptureController#onClientUnavailable}.
|
||||
*/
|
||||
void requestScrollCapture(int displayId, IBinder behindClient, int taskId,
|
||||
IScrollCaptureController controller);
|
||||
}
|
||||
|
||||
151
core/java/android/view/ScrollCaptureCallback.java
Normal file
151
core/java/android/view/ScrollCaptureCallback.java
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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.NonNull;
|
||||
import android.annotation.UiThread;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A ScrollCaptureCallback is responsible for providing rendered snapshots of scrolling content for
|
||||
* the scroll capture system. A single callback is responsible for providing support to a single
|
||||
* scrolling UI element. At request time, the system will select the best candidate from among all
|
||||
* callbacks registered within the window.
|
||||
* <p>
|
||||
* A callback is assigned to a View using {@link View#setScrollCaptureCallback}, or to the window as
|
||||
* {@link Window#addScrollCaptureCallback}. The point where the callback is registered defines the
|
||||
* frame of reference for the bounds measurements used.
|
||||
* <p>
|
||||
* <b>Terminology</b>
|
||||
* <dl>
|
||||
* <dt>Containing View</dt>
|
||||
* <dd>The view on which this callback is attached, or the root view of the window if the callback
|
||||
* is assigned directly to a window.</dd>
|
||||
*
|
||||
* <dt>Scroll Bounds</dt>
|
||||
* <dd>A rectangle which describes an area within the containing view where scrolling content may
|
||||
* be positioned. This may be the Containing View bounds itself, or any rectangle within.
|
||||
* Requested by {@link #onScrollCaptureSearch}.</dd>
|
||||
*
|
||||
* <dt>Scroll Delta</dt>
|
||||
* <dd>The distance the scroll position has moved since capture started. Implementations are
|
||||
* responsible for tracking changes in vertical scroll position during capture. This is required to
|
||||
* map the capture area to the correct location, given the current scroll position.
|
||||
*
|
||||
* <dt>Capture Area</dt>
|
||||
* <dd>A rectangle which describes the area to capture, relative to scroll bounds. The vertical
|
||||
* position remains relative to the starting scroll position and any movement since ("Scroll Delta")
|
||||
* should be subtracted to locate the correct local position, and scrolled into view as necessary.
|
||||
* </dd>
|
||||
* </dl>
|
||||
*
|
||||
* @see View#setScrollCaptureHint(int)
|
||||
* @see View#setScrollCaptureCallback(ScrollCaptureCallback)
|
||||
* @see Window#addScrollCaptureCallback(ScrollCaptureCallback)
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@UiThread
|
||||
public interface ScrollCaptureCallback {
|
||||
|
||||
/**
|
||||
* The system is searching for the appropriate scrolling container to capture and would like to
|
||||
* know the size and position of scrolling content handled by this callback.
|
||||
* <p>
|
||||
* Implementations should inset {@code containingViewBounds} to cover only the area within the
|
||||
* containing view where scrolling content may be positioned. This should cover only the content
|
||||
* which tracks with scrolling movement.
|
||||
* <p>
|
||||
* Return the updated rectangle to {@code resultConsumer}. If for any reason the scrolling
|
||||
* content is not available to capture, a {@code null} rectangle may be returned, and this view
|
||||
* will be excluded as the target for this request.
|
||||
* <p>
|
||||
* Responses received after XXXms will be discarded.
|
||||
* <p>
|
||||
* TODO: finalize timeout
|
||||
*
|
||||
* @param onReady consumer for the updated rectangle
|
||||
*/
|
||||
void onScrollCaptureSearch(@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.
|
||||
*/
|
||||
void onScrollCaptureStart(@NonNull ScrollCaptureSession session, @NonNull Runnable onReady);
|
||||
|
||||
/**
|
||||
* An image capture has been requested from the scrolling content.
|
||||
* <p>
|
||||
* <code>captureArea</code> contains the bounds of the image requested, relative to the
|
||||
* rectangle provided by {@link ScrollCaptureCallback#onScrollCaptureSearch}, referred to as
|
||||
* {@code scrollBounds}.
|
||||
* here.
|
||||
* <p>
|
||||
* A series of requests will step by a constant vertical amount relative to {@code
|
||||
* scrollBounds}, moving through the scrolling range of content, above and below the current
|
||||
* visible area. The rectangle's vertical position will not account for any scrolling movement
|
||||
* since capture started. Implementations therefore must track any scroll position changes and
|
||||
* subtract this distance from requests.
|
||||
* <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
|
||||
* <p>
|
||||
*
|
||||
* @param captureArea the area to capture, a rectangle within {@code scrollBounds}
|
||||
*/
|
||||
void onScrollCaptureImageRequest(
|
||||
@NonNull ScrollCaptureSession session, @NonNull Rect captureArea);
|
||||
|
||||
/**
|
||||
* Signals that capture has ended. Implementations should release any temporary resources or
|
||||
* references to objects in use during the capture. Any resources obtained from the session are
|
||||
* now invalid and attempts to use them after this point may throw an exception.
|
||||
* <p>
|
||||
* The window should be returned as much as possible to its original state when capture started.
|
||||
* At a minimum, the content should be scrolled to its original position.
|
||||
* <p>
|
||||
* <code>onReady</code> should be called when the window should be made visible and
|
||||
* interactive. The system will wait up to XXXms for this call before proceeding.
|
||||
* <p>
|
||||
* TODO: finalize timeout
|
||||
*
|
||||
* @param onReady a callback to inform the system that the application has completed any
|
||||
* cleanup and is ready to become visible
|
||||
*/
|
||||
void onScrollCaptureEnd(@NonNull Runnable onReady);
|
||||
}
|
||||
|
||||
312
core/java/android/view/ScrollCaptureClient.java
Normal file
312
core/java/android/view/ScrollCaptureClient.java
Normal file
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* 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 static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.UiThread;
|
||||
import android.annotation.WorkerThread;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Handler;
|
||||
import android.os.RemoteException;
|
||||
import android.util.CloseGuard;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* A client of the system providing Scroll Capture capability on behalf of a Window.
|
||||
* <p>
|
||||
* An instance is created to wrap the selected {@link ScrollCaptureCallback}.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class ScrollCaptureClient extends IScrollCaptureClient.Stub {
|
||||
|
||||
private static final String TAG = "ScrollCaptureClient";
|
||||
private static final int DEFAULT_TIMEOUT = 1000;
|
||||
|
||||
private final Handler mHandler;
|
||||
private ScrollCaptureTarget mSelectedTarget;
|
||||
private int mTimeoutMillis = DEFAULT_TIMEOUT;
|
||||
|
||||
protected Surface mSurface;
|
||||
private IScrollCaptureController mController;
|
||||
|
||||
private final Rect mScrollBounds;
|
||||
private final Point mPositionInWindow;
|
||||
private final CloseGuard mCloseGuard;
|
||||
|
||||
// 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;
|
||||
|
||||
/**
|
||||
* Constructs a ScrollCaptureClient.
|
||||
*
|
||||
* @param selectedTarget the target the client is controlling
|
||||
* @param controller the callbacks to reply to system requests
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public ScrollCaptureClient(
|
||||
@NonNull ScrollCaptureTarget selectedTarget,
|
||||
@NonNull IScrollCaptureController controller) {
|
||||
requireNonNull(selectedTarget, "<selectedTarget> must non-null");
|
||||
requireNonNull(controller, "<controller> must non-null");
|
||||
final Rect scrollBounds = requireNonNull(selectedTarget.getScrollBounds(),
|
||||
"target.getScrollBounds() must be non-null to construct a client");
|
||||
|
||||
mSelectedTarget = selectedTarget;
|
||||
mHandler = selectedTarget.getContainingView().getHandler();
|
||||
mScrollBounds = new Rect(scrollBounds);
|
||||
mPositionInWindow = new Point(selectedTarget.getPositionInWindow());
|
||||
|
||||
mController = controller;
|
||||
mCloseGuard = new CloseGuard();
|
||||
mCloseGuard.open("close");
|
||||
|
||||
selectedTarget.getContainingView().addOnAttachStateChangeListener(
|
||||
new View.OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v) {
|
||||
selectedTarget.getContainingView().removeOnAttachStateChangeListener(this);
|
||||
endCapture();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setTimeoutMillis(int timeoutMillis) {
|
||||
mTimeoutMillis = timeoutMillis;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@VisibleForTesting
|
||||
public DelayedAction getTimeoutAction() {
|
||||
return mTimeoutAction;
|
||||
}
|
||||
|
||||
private void checkConnected() {
|
||||
if (mSelectedTarget == null || mController == 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 // IScrollCaptureClient
|
||||
@Override
|
||||
public void startCapture(Surface surface) throws RemoteException {
|
||||
checkConnected();
|
||||
mSurface = surface;
|
||||
scheduleTimeout(mTimeoutMillis, this::onStartCaptureTimeout);
|
||||
mSession = new ScrollCaptureSession(mSurface, mScrollBounds, mPositionInWindow, this);
|
||||
mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureStart(mSession,
|
||||
this::onStartCaptureCompleted));
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void onStartCaptureCompleted() {
|
||||
if (cancelTimeout()) {
|
||||
mHandler.post(() -> {
|
||||
try {
|
||||
mController.onCaptureStarted();
|
||||
} catch (RemoteException e) {
|
||||
doShutdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void onStartCaptureTimeout() {
|
||||
endCapture();
|
||||
}
|
||||
|
||||
@WorkerThread // IScrollCaptureClient
|
||||
@Override
|
||||
public void requestImage(Rect requestRect) {
|
||||
checkConnected();
|
||||
checkStarted();
|
||||
scheduleTimeout(mTimeoutMillis, this::onRequestImageTimeout);
|
||||
// Response is dispatched via ScrollCaptureSession, to onRequestImageCompleted
|
||||
mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureImageRequest(
|
||||
mSession, new Rect(requestRect)));
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void onRequestImageCompleted(long frameNumber, Rect capturedArea) {
|
||||
final Rect finalCapturedArea = new Rect(capturedArea);
|
||||
if (cancelTimeout()) {
|
||||
mHandler.post(() -> {
|
||||
try {
|
||||
mController.onCaptureBufferSent(frameNumber, finalCapturedArea);
|
||||
} catch (RemoteException e) {
|
||||
doShutdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void onRequestImageTimeout() {
|
||||
endCapture();
|
||||
}
|
||||
|
||||
@WorkerThread // IScrollCaptureClient
|
||||
@Override
|
||||
public void endCapture() {
|
||||
if (isStarted()) {
|
||||
scheduleTimeout(mTimeoutMillis, this::onEndCaptureTimeout);
|
||||
mHandler.post(() ->
|
||||
mSelectedTarget.getCallback().onScrollCaptureEnd(this::onEndCaptureCompleted));
|
||||
} else {
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStarted() {
|
||||
return mController != null && mSelectedTarget != null;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void onEndCaptureCompleted() { // onEndCaptureCompleted
|
||||
if (cancelTimeout()) {
|
||||
doShutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void onEndCaptureTimeout() {
|
||||
doShutdown();
|
||||
}
|
||||
|
||||
|
||||
private void doShutdown() {
|
||||
try {
|
||||
if (mController != null) {
|
||||
mController.onConnectionClosed();
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
// Ignore
|
||||
} finally {
|
||||
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();
|
||||
mSession = null;
|
||||
}
|
||||
|
||||
mSelectedTarget = null;
|
||||
mController = null;
|
||||
}
|
||||
|
||||
/** @return a string representation of the state of this client */
|
||||
public String toString() {
|
||||
return "ScrollCaptureClient{"
|
||||
+ ", session=" + mSession
|
||||
+ ", selectedTarget=" + mSelectedTarget
|
||||
+ ", clientCallbacks=" + mController
|
||||
+ "}";
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
mHandler.removeCallbacksAndMessages(mToken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
core/java/android/view/ScrollCaptureSession.java
Normal file
105
core/java/android/view/ScrollCaptureSession.java
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.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
|
||||
*/
|
||||
public class ScrollCaptureSession {
|
||||
|
||||
private final Surface mSurface;
|
||||
private final Rect mScrollBounds;
|
||||
private final Point mPositionInWindow;
|
||||
|
||||
@Nullable
|
||||
private ScrollCaptureClient mClient;
|
||||
|
||||
/** @hide */
|
||||
public ScrollCaptureSession(Surface surface, Rect scrollBounds, Point positionInWindow,
|
||||
ScrollCaptureClient client) {
|
||||
mSurface = surface;
|
||||
mScrollBounds = scrollBounds;
|
||||
mPositionInWindow = positionInWindow;
|
||||
mClient = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return the surface for transferring image buffers
|
||||
* @throws IllegalStateException if the session has been closed
|
||||
*/
|
||||
@NonNull
|
||||
public Surface getSurface() {
|
||||
return mSurface;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code scroll bounds}, as provided by
|
||||
* {@link ScrollCaptureCallback#onScrollCaptureSearch}.
|
||||
*
|
||||
* @return the area of scrolling content within the containing view
|
||||
*/
|
||||
@NonNull
|
||||
public Rect getScrollBounds() {
|
||||
return mScrollBounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset of {@code scroll bounds} within the window.
|
||||
*
|
||||
* @return the area of scrolling content within the containing view
|
||||
*/
|
||||
@NonNull
|
||||
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 (mClient != null) {
|
||||
mClient.onRequestImageCompleted(frameNumber, capturedArea);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public void disconnect() {
|
||||
mClient = null;
|
||||
if (mSurface.isValid()) {
|
||||
mSurface.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
135
core/java/android/view/ScrollCaptureTarget.java
Normal file
135
core/java/android/view/ScrollCaptureTarget.java
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.UiThread;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import com.android.internal.util.FastMath;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
private final ScrollCaptureCallback mCallback;
|
||||
private final Rect mLocalVisibleRect;
|
||||
private final Point mPositionInWindow;
|
||||
private final int mHint;
|
||||
private Rect mScrollBounds;
|
||||
|
||||
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) {
|
||||
mContainingView = scrollTarget;
|
||||
mHint = mContainingView.getScrollCaptureHint();
|
||||
mCallback = callback;
|
||||
mLocalVisibleRect = localVisibleRect;
|
||||
mPositionInWindow = positionInWindow;
|
||||
}
|
||||
|
||||
/** @return the hint that the {@code containing view} had during the scroll capture search */
|
||||
@View.ScrollCaptureHint
|
||||
public int getHint() {
|
||||
return mHint;
|
||||
}
|
||||
|
||||
/** @return the {@link ScrollCaptureCallback} for this target */
|
||||
@NonNull
|
||||
public ScrollCaptureCallback getCallback() {
|
||||
return mCallback;
|
||||
}
|
||||
|
||||
/** @return the {@code containing view} for this {@link ScrollCaptureCallback callback} */
|
||||
@NonNull
|
||||
public View getContainingView() {
|
||||
return mContainingView;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return the visible bounds of the {@code containing view} in view-local coordinates
|
||||
*/
|
||||
@NonNull
|
||||
public Rect getLocalVisibleRect() {
|
||||
return mLocalVisibleRect;
|
||||
}
|
||||
|
||||
/** @return the position of the {@code containing view} within the window */
|
||||
@NonNull
|
||||
public Point getPositionInWindow() {
|
||||
return mPositionInWindow;
|
||||
}
|
||||
|
||||
/** @return the {@code scroll bounds} for this {@link ScrollCaptureCallback callback} */
|
||||
@Nullable
|
||||
public Rect getScrollBounds() {
|
||||
return mScrollBounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scroll bounds rect to the intersection of provided rect and the current bounds of
|
||||
* the {@code containing view}.
|
||||
*/
|
||||
public void setScrollBounds(@Nullable Rect scrollBounds) {
|
||||
mScrollBounds = Rect.copyOrNull(scrollBounds);
|
||||
if (mScrollBounds == null) {
|
||||
return;
|
||||
}
|
||||
if (!mScrollBounds.intersect(0, 0,
|
||||
mContainingView.getWidth(), mContainingView.getHeight())) {
|
||||
mScrollBounds.setEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
private static void zero(float[] pointArray) {
|
||||
pointArray[0] = 0;
|
||||
pointArray[1] = 0;
|
||||
}
|
||||
|
||||
private static void roundIntoPoint(Point pointObj, float[] pointArray) {
|
||||
pointObj.x = FastMath.round(pointArray[0]);
|
||||
pointObj.y = FastMath.round(pointArray[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the value of {@link #mLocalVisibleRect} and {@link #mPositionInWindow} based on the
|
||||
* current state of the {@code containing view}.
|
||||
*/
|
||||
@UiThread
|
||||
public void updatePositionInWindow() {
|
||||
mMatrixViewLocalToWindow.reset();
|
||||
mContainingView.transformMatrixToGlobal(mMatrixViewLocalToWindow);
|
||||
|
||||
zero(mTmpFloatArr);
|
||||
mMatrixViewLocalToWindow.mapPoints(mTmpFloatArr);
|
||||
roundIntoPoint(mPositionInWindow, mTmpFloatArr);
|
||||
}
|
||||
|
||||
}
|
||||
387
core/java/android/view/ScrollCaptureTargetResolver.java
Normal file
387
core/java/android/view/ScrollCaptureTargetResolver.java
Normal file
@@ -0,0 +1,387 @@
|
||||
/*
|
||||
* 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 static final boolean DEBUG = true;
|
||||
|
||||
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) {
|
||||
Log.d(TAG, "chooseTarget: " + a + " or " + b);
|
||||
// Nothing plus nothing is still nothing.
|
||||
if (a == null && b == null) {
|
||||
Log.d(TAG, "chooseTarget: (both null) return " + null);
|
||||
return null;
|
||||
}
|
||||
// Prefer non-null.
|
||||
if (a == null || b == null) {
|
||||
ScrollCaptureTarget c = (a == null) ? b : a;
|
||||
Log.d(TAG, "chooseTarget: (other is null) return " + c);
|
||||
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}.
|
||||
* @param uiHandler the UI thread handler for the view tree
|
||||
* @see #start(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.elapsedRealtime() >= mDeadlineMillis;
|
||||
if (finish) {
|
||||
System.err.println("We think we're done, or timed out");
|
||||
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(() -> run(timeLimitMillis, resultConsumer));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void run(long timeLimitMillis, Consumer<ScrollCaptureTarget> resultConsumer) {
|
||||
checkThread();
|
||||
|
||||
mPendingBoundsRequests = mTargets.size();
|
||||
for (ScrollCaptureTarget target : mTargets) {
|
||||
queryTarget(target);
|
||||
}
|
||||
mDeadlineMillis = SystemClock.elapsedRealtime() + mTimeLimitMillis;
|
||||
mHandler.postAtTime(mTimeoutRunnable, mDeadlineMillis);
|
||||
}
|
||||
|
||||
private final Runnable mTimeoutRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
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.elapsedRealtime() >= mDeadlineMillis;
|
||||
|
||||
final View containingView = target.getContainingView();
|
||||
if (!nullOrEmpty(scrollBounds) && containingView.isAggregatedVisible()) {
|
||||
target.updatePositionInWindow();
|
||||
target.setScrollBounds(scrollBounds);
|
||||
supplyResult(target);
|
||||
}
|
||||
|
||||
System.err.println("mPendingBoundsRequests: " + mPendingBoundsRequests);
|
||||
System.err.println("mDeadlineMillis: " + mDeadlineMillis);
|
||||
System.err.println("SystemClock.elapsedRealtime(): " + SystemClock.elapsedRealtime());
|
||||
|
||||
if (!mFinished) {
|
||||
// Reschedule the timeout.
|
||||
System.err.println(
|
||||
"We think we're NOT done yet and will check back at " + mDeadlineMillis);
|
||||
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;
|
||||
}
|
||||
|
||||
private static int findRelation(@NonNull View a, @NonNull View b) {
|
||||
if (a == b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ViewParent parentA = a.getParent();
|
||||
ViewParent parentB = b.getParent();
|
||||
|
||||
while (parentA != null || parentB != null) {
|
||||
if (parentA == parentB) {
|
||||
return 0;
|
||||
}
|
||||
if (parentA == b) {
|
||||
return 1; // A is descendant of B
|
||||
}
|
||||
if (parentB == a) {
|
||||
return -1; // B is descendant of A
|
||||
}
|
||||
if (parentA != null) {
|
||||
parentA = parentA.getParent();
|
||||
}
|
||||
if (parentB != null) {
|
||||
parentB = parentB.getParent();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param target the target consumer
|
||||
*/
|
||||
static class SingletonConsumer<T> implements Consumer<T> {
|
||||
final AtomicReference<Consumer<T>> mAtomicRef;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,7 @@ import android.widget.ScrollBarDrawable;
|
||||
|
||||
import com.android.internal.R;
|
||||
import com.android.internal.util.FrameworkStatsLog;
|
||||
import com.android.internal.view.ScrollCaptureInternal;
|
||||
import com.android.internal.view.TooltipPopup;
|
||||
import com.android.internal.view.menu.MenuBuilder;
|
||||
import com.android.internal.widget.ScrollBarUtils;
|
||||
@@ -167,6 +168,7 @@ 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.atomic.AtomicInteger;
|
||||
import java.util.function.Predicate;
|
||||
@@ -1311,7 +1313,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
*/
|
||||
public static final int AUTOFILL_TYPE_LIST = 3;
|
||||
|
||||
|
||||
/**
|
||||
* Autofill type for a field that contains a date, which is represented by a long representing
|
||||
* the number of milliseconds since the standard base time known as "the epoch", namely
|
||||
@@ -1441,6 +1442,58 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
*/
|
||||
public static final int IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS = 0x8;
|
||||
|
||||
/** {@hide} */
|
||||
@IntDef(flag = true, prefix = {"SCROLL_CAPTURE_HINT_"},
|
||||
value = {
|
||||
SCROLL_CAPTURE_HINT_AUTO,
|
||||
SCROLL_CAPTURE_HINT_EXCLUDE,
|
||||
SCROLL_CAPTURE_HINT_INCLUDE,
|
||||
SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface ScrollCaptureHint {}
|
||||
|
||||
/**
|
||||
* The content of this view will be considered for scroll capture if scrolling is possible.
|
||||
*
|
||||
* @see #getScrollCaptureHint()
|
||||
* @see #setScrollCaptureHint(int)
|
||||
* @hide
|
||||
*/
|
||||
public static final int SCROLL_CAPTURE_HINT_AUTO = 0;
|
||||
|
||||
/**
|
||||
* Explicitly exclcude this view as a potential scroll capture target. The system will not
|
||||
* consider it. Mutually exclusive with {@link #SCROLL_CAPTURE_HINT_INCLUDE}, which this flag
|
||||
* takes precedence over.
|
||||
*
|
||||
* @see #getScrollCaptureHint()
|
||||
* @see #setScrollCaptureHint(int)
|
||||
* @hide
|
||||
*/
|
||||
public static final int SCROLL_CAPTURE_HINT_EXCLUDE = 0x1;
|
||||
|
||||
/**
|
||||
* Explicitly include this view as a potential scroll capture target. When locating a scroll
|
||||
* capture target, this view will be prioritized before others without this flag. Mutually
|
||||
* exclusive with {@link #SCROLL_CAPTURE_HINT_EXCLUDE}, which takes precedence.
|
||||
*
|
||||
* @see #getScrollCaptureHint()
|
||||
* @see #setScrollCaptureHint(int)
|
||||
* @hide
|
||||
*/
|
||||
public static final int SCROLL_CAPTURE_HINT_INCLUDE = 0x2;
|
||||
|
||||
/**
|
||||
* Explicitly exclude all children of this view as potential scroll capture targets. This view
|
||||
* is unaffected. Note: Excluded children are not considered, regardless of {@link
|
||||
* #SCROLL_CAPTURE_HINT_INCLUDE}.
|
||||
*
|
||||
* @see #getScrollCaptureHint()
|
||||
* @see #setScrollCaptureHint(int)
|
||||
* @hide
|
||||
*/
|
||||
public static final int SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS = 0x4;
|
||||
|
||||
/**
|
||||
* This view is enabled. Interpretation varies by subclass.
|
||||
@@ -3430,6 +3483,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
* 11 PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK
|
||||
* 1 PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS
|
||||
* 1 PFLAG4_AUTOFILL_HIDE_HIGHLIGHT
|
||||
* 11 PFLAG4_SCROLL_CAPTURE_HINT_MASK
|
||||
* |-------|-------|-------|-------|
|
||||
*/
|
||||
|
||||
@@ -3477,6 +3531,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
*/
|
||||
private static final int PFLAG4_AUTOFILL_HIDE_HIGHLIGHT = 0x200;
|
||||
|
||||
/**
|
||||
* Shift for the bits in {@link #mPrivateFlags4} related to scroll capture.
|
||||
*/
|
||||
static final int PFLAG4_SCROLL_CAPTURE_HINT_SHIFT = 10;
|
||||
|
||||
static final int PFLAG4_SCROLL_CAPTURE_HINT_MASK = (SCROLL_CAPTURE_HINT_INCLUDE
|
||||
| SCROLL_CAPTURE_HINT_EXCLUDE | SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS)
|
||||
<< PFLAG4_SCROLL_CAPTURE_HINT_SHIFT;
|
||||
|
||||
/* End of masks for mPrivateFlags4 */
|
||||
|
||||
/** @hide */
|
||||
@@ -4690,6 +4753,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
* Used to track {@link #mSystemGestureExclusionRects}
|
||||
*/
|
||||
public RenderNode.PositionUpdateListener mPositionUpdateListener;
|
||||
|
||||
/**
|
||||
* Allows the application to implement custom scroll capture support.
|
||||
*/
|
||||
ScrollCaptureCallback mScrollCaptureCallback;
|
||||
}
|
||||
|
||||
@UnsupportedAppUsage
|
||||
@@ -5941,6 +6009,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
case R.styleable.View_forceDarkAllowed:
|
||||
mRenderNode.setForceDarkAllowed(a.getBoolean(attr, true));
|
||||
break;
|
||||
case R.styleable.View_scrollCaptureHint:
|
||||
setScrollCaptureHint((a.getInt(attr, SCROLL_CAPTURE_HINT_AUTO)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29090,6 +29161,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
*/
|
||||
int mLeashedParentAccessibilityViewId;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
ScrollCaptureInternal mScrollCaptureInternal;
|
||||
|
||||
/**
|
||||
* Creates a new set of attachment information with the specified
|
||||
* events handler and thread.
|
||||
@@ -29150,6 +29226,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
ScrollCaptureInternal getScrollCaptureInternal() {
|
||||
if (mScrollCaptureInternal != null) {
|
||||
mScrollCaptureInternal = new ScrollCaptureInternal();
|
||||
}
|
||||
return mScrollCaptureInternal;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29683,6 +29767,104 @@ 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() {
|
||||
return (mPrivateFlags4 & PFLAG4_SCROLL_CAPTURE_HINT_MASK)
|
||||
>> PFLAG4_SCROLL_CAPTURE_HINT_SHIFT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scroll capture hint for this View. These flags affect the search for a potential
|
||||
* scroll capture targets.
|
||||
*
|
||||
* @param hint the scrollCaptureHint flags value to set
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public void setScrollCaptureHint(@ScrollCaptureHint int hint) {
|
||||
mPrivateFlags4 &= ~PFLAG4_SCROLL_CAPTURE_HINT_MASK;
|
||||
mPrivateFlags4 |= ((hint << PFLAG4_SCROLL_CAPTURE_HINT_SHIFT)
|
||||
& PFLAG4_SCROLL_CAPTURE_HINT_MASK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the callback to receive scroll capture requests. This component is the adapter between
|
||||
* the scroll capture API and application UI code. If no callback is set, the system may provide
|
||||
* an implementation. Any value provided here will take precedence over a system version.
|
||||
* <p>
|
||||
* This view will be ignored when {@link #SCROLL_CAPTURE_HINT_EXCLUDE} is set in its {@link
|
||||
* #setScrollCaptureHint(int) scrollCaptureHint}, regardless whether a callback has been set.
|
||||
* <p>
|
||||
* It is recommended to set the scroll capture hint {@link #SCROLL_CAPTURE_HINT_INCLUDE} when
|
||||
* 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) {
|
||||
getListenerInfo().mScrollCaptureCallback = callback;
|
||||
}
|
||||
|
||||
/** {@hide} */
|
||||
@Nullable
|
||||
public ScrollCaptureCallback createScrollCaptureCallbackInternal(@NonNull Rect localVisibleRect,
|
||||
@NonNull Point windowOffset) {
|
||||
if (mAttachInfo == null) {
|
||||
return null;
|
||||
}
|
||||
if (mAttachInfo.mScrollCaptureInternal == null) {
|
||||
mAttachInfo.mScrollCaptureInternal = new ScrollCaptureInternal();
|
||||
}
|
||||
return mAttachInfo.mScrollCaptureInternal.requestCallback(this, localVisibleRect,
|
||||
windowOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when scroll capture is requested, to search for appropriate content to scroll. If
|
||||
* applicable, this view adds itself to the provided list for consideration, subject to the
|
||||
* flags set by {@link #setScrollCaptureHint}.
|
||||
*
|
||||
* @param localVisibleRect the local visible rect of this view
|
||||
* @param windowOffset the offset of localVisibleRect within the window
|
||||
* @param targets a queue which collects potential targets
|
||||
*
|
||||
* @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) {
|
||||
int hint = getScrollCaptureHint();
|
||||
if ((hint & SCROLL_CAPTURE_HINT_EXCLUDE) != 0) {
|
||||
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.
|
||||
if (callback == null) {
|
||||
callback = createScrollCaptureCallbackInternal(localVisibleRect, windowOffset);
|
||||
}
|
||||
|
||||
// If found, then add it to the list.
|
||||
if (callback != null) {
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump all private flags in readable format, useful for documentation and
|
||||
* sanity checking.
|
||||
|
||||
@@ -40,6 +40,7 @@ import android.graphics.Color;
|
||||
import android.graphics.Insets;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
@@ -75,6 +76,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.Predicate;
|
||||
|
||||
/**
|
||||
@@ -188,7 +190,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
|
||||
private PointF mLocalPoint;
|
||||
|
||||
// Lazily-created holder for point computations.
|
||||
private float[] mTempPoint;
|
||||
private float[] mTempPosition;
|
||||
|
||||
// Lazily-created holder for point computations.
|
||||
private Point mTempPoint;
|
||||
|
||||
// Lazily created Rect for dispatch to children
|
||||
private Rect mTempRect;
|
||||
|
||||
// Lazily created int[2] for dispatch to children
|
||||
private int[] mTempLocation;
|
||||
|
||||
// Layout animation
|
||||
private LayoutAnimationController mLayoutAnimationController;
|
||||
@@ -1860,7 +1871,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
|
||||
final float tx = mCurrentDragStartEvent.mX;
|
||||
final float ty = mCurrentDragStartEvent.mY;
|
||||
|
||||
final float[] point = getTempPoint();
|
||||
final float[] point = getTempLocationF();
|
||||
point[0] = tx;
|
||||
point[1] = ty;
|
||||
transformPointToViewLocal(point, child);
|
||||
@@ -2932,9 +2943,23 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
|
||||
}
|
||||
}
|
||||
|
||||
private float[] getTempPoint() {
|
||||
private Rect getTempRect() {
|
||||
if (mTempRect == null) {
|
||||
mTempRect = new Rect();
|
||||
}
|
||||
return mTempRect;
|
||||
}
|
||||
|
||||
private float[] getTempLocationF() {
|
||||
if (mTempPosition == null) {
|
||||
mTempPosition = new float[2];
|
||||
}
|
||||
return mTempPosition;
|
||||
}
|
||||
|
||||
private Point getTempPoint() {
|
||||
if (mTempPoint == null) {
|
||||
mTempPoint = new float[2];
|
||||
mTempPoint = new Point();
|
||||
}
|
||||
return mTempPoint;
|
||||
}
|
||||
@@ -2948,7 +2973,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
|
||||
@UnsupportedAppUsage
|
||||
protected boolean isTransformedTouchPointInView(float x, float y, View child,
|
||||
PointF outLocalPoint) {
|
||||
final float[] point = getTempPoint();
|
||||
final float[] point = getTempLocationF();
|
||||
point[0] = x;
|
||||
point[1] = y;
|
||||
transformPointToViewLocal(point, child);
|
||||
@@ -4568,7 +4593,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
|
||||
final boolean nonActionable = !child.isClickable() && !child.isLongClickable();
|
||||
final boolean duplicatesState = (child.mViewFlags & DUPLICATE_PARENT_STATE) != 0;
|
||||
if (nonActionable || duplicatesState) {
|
||||
final float[] point = getTempPoint();
|
||||
final float[] point = getTempLocationF();
|
||||
point[0] = x;
|
||||
point[1] = y;
|
||||
transformPointToViewLocal(point, child);
|
||||
@@ -7353,6 +7378,97 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Offsets the given rectangle in parent's local coordinates into child's coordinate space
|
||||
* and clips the result to the child View's bounds, padding and clipRect if appropriate. If the
|
||||
* resulting rectangle is not empty, the request is forwarded to the child.
|
||||
* <p>
|
||||
* Note: This method does not account for any static View transformations which may be
|
||||
* applied to the child view.
|
||||
*
|
||||
* @param child the child to dispatch to
|
||||
* @param localVisibleRect the visible (clipped) area of this ViewGroup, in local coordinates
|
||||
* @param windowOffset the offset of localVisibleRect within the window
|
||||
* @param targets a queue to collect located targets
|
||||
*/
|
||||
private void dispatchTransformedScrollCaptureSearch(View child, Rect localVisibleRect,
|
||||
Point windowOffset, Queue<ScrollCaptureTarget> targets) {
|
||||
|
||||
// copy local visible rect for modification and dispatch
|
||||
final Rect childVisibleRect = getTempRect();
|
||||
childVisibleRect.set(localVisibleRect);
|
||||
|
||||
// transform to child coords
|
||||
final Point childWindowOffset = getTempPoint();
|
||||
childWindowOffset.set(windowOffset.x, windowOffset.y);
|
||||
|
||||
final int dx = child.mLeft - mScrollX;
|
||||
final int dy = child.mTop - mScrollY;
|
||||
|
||||
childVisibleRect.offset(-dx, -dy);
|
||||
childWindowOffset.offset(dx, dy);
|
||||
|
||||
boolean rectIsVisible = true;
|
||||
final int width = mRight - mLeft;
|
||||
final int height = mBottom - mTop;
|
||||
|
||||
// Clip to child bounds
|
||||
if (getClipChildren()) {
|
||||
rectIsVisible = childVisibleRect.intersect(0, 0, child.getWidth(), child.getHeight());
|
||||
}
|
||||
|
||||
// Clip to child padding.
|
||||
if (rectIsVisible && (child instanceof ViewGroup)
|
||||
&& ((ViewGroup) child).getClipToPadding()) {
|
||||
rectIsVisible = childVisibleRect.intersect(
|
||||
child.mPaddingLeft, child.mPaddingTop,
|
||||
child.getWidth() - child.mPaddingRight,
|
||||
child.getHeight() - child.mPaddingBottom);
|
||||
}
|
||||
// Clip to child clipBounds.
|
||||
if (rectIsVisible && child.mClipBounds != null) {
|
||||
rectIsVisible = childVisibleRect.intersect(child.mClipBounds);
|
||||
}
|
||||
if (rectIsVisible) {
|
||||
child.dispatchScrollCaptureSearch(childVisibleRect, childWindowOffset, targets);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the scroll capture search request by checking this view if applicable, then to each
|
||||
* child view.
|
||||
*
|
||||
* @param localVisibleRect the visible area of this ViewGroup in local coordinates, according to
|
||||
* the parent
|
||||
* @param windowOffset the offset of this view within the window
|
||||
* @param targets the collected list of scroll capture targets
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public void dispatchScrollCaptureSearch(
|
||||
@NonNull Rect localVisibleRect, @NonNull Point windowOffset,
|
||||
@NonNull Queue<ScrollCaptureTarget> targets) {
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the animation listener to which layout animation events are
|
||||
* sent.
|
||||
|
||||
@@ -206,6 +206,7 @@ public final class ViewRootImpl implements ViewParent,
|
||||
private static final boolean DEBUG_INPUT_STAGES = false || LOCAL_LOGV;
|
||||
private static final boolean DEBUG_KEEP_SCREEN_ON = false || LOCAL_LOGV;
|
||||
private static final boolean DEBUG_CONTENT_CAPTURE = false || LOCAL_LOGV;
|
||||
private static final boolean DEBUG_SCROLL_CAPTURE = false || LOCAL_LOGV;
|
||||
|
||||
/**
|
||||
* Set to false if we do not want to use the multi threaded renderer even though
|
||||
@@ -653,6 +654,8 @@ public final class ViewRootImpl implements ViewParent,
|
||||
private final InsetsController mInsetsController;
|
||||
private final ImeFocusController mImeFocusController;
|
||||
|
||||
private ScrollCaptureClient mScrollCaptureClient;
|
||||
|
||||
/**
|
||||
* @return {@link ImeFocusController} for this instance.
|
||||
*/
|
||||
@@ -661,6 +664,11 @@ public final class ViewRootImpl implements ViewParent,
|
||||
return mImeFocusController;
|
||||
}
|
||||
|
||||
/** @return The current {@link ScrollCaptureClient} for this instance, if any is active. */
|
||||
@Nullable
|
||||
public ScrollCaptureClient getScrollCaptureClient() {
|
||||
return mScrollCaptureClient;
|
||||
}
|
||||
|
||||
private final GestureExclusionTracker mGestureExclusionTracker = new GestureExclusionTracker();
|
||||
|
||||
@@ -694,6 +702,8 @@ public final class ViewRootImpl implements ViewParent,
|
||||
// draw returns.
|
||||
private SurfaceControl.Transaction mRtBLASTSyncTransaction = new SurfaceControl.Transaction();
|
||||
|
||||
private HashSet<ScrollCaptureCallback> mRootScrollCaptureCallbacks;
|
||||
|
||||
private String mTag = TAG;
|
||||
|
||||
public ViewRootImpl(Context context, Display display) {
|
||||
@@ -4778,6 +4788,7 @@ public final class ViewRootImpl implements ViewParent,
|
||||
private static final int MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED = 33;
|
||||
private static final int MSG_SHOW_INSETS = 34;
|
||||
private static final int MSG_HIDE_INSETS = 35;
|
||||
private static final int MSG_REQUEST_SCROLL_CAPTURE = 36;
|
||||
|
||||
|
||||
final class ViewRootHandler extends Handler {
|
||||
@@ -5080,6 +5091,9 @@ public final class ViewRootImpl implements ViewParent,
|
||||
case MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED: {
|
||||
updateLocationInParentDisplay(msg.arg1, msg.arg2);
|
||||
} break;
|
||||
case MSG_REQUEST_SCROLL_CAPTURE:
|
||||
handleScrollCaptureRequest((IScrollCaptureController) msg.obj);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8789,6 +8803,131 @@ public final class ViewRootImpl implements ViewParent,
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a scroll capture callback to this window.
|
||||
*
|
||||
* @param callback the callback to add
|
||||
*/
|
||||
public void addScrollCaptureCallback(ScrollCaptureCallback callback) {
|
||||
if (mRootScrollCaptureCallbacks == null) {
|
||||
mRootScrollCaptureCallbacks = new HashSet<>();
|
||||
}
|
||||
mRootScrollCaptureCallbacks.add(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a scroll capture callback from this window.
|
||||
*
|
||||
* @param callback the callback to remove
|
||||
*/
|
||||
public void removeScrollCaptureCallback(ScrollCaptureCallback callback) {
|
||||
if (mRootScrollCaptureCallbacks != null) {
|
||||
mRootScrollCaptureCallbacks.remove(callback);
|
||||
if (mRootScrollCaptureCallbacks.isEmpty()) {
|
||||
mRootScrollCaptureCallbacks = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a scroll capture request to the view hierarchy on the ui thread.
|
||||
*
|
||||
* @param controller the controller to receive replies
|
||||
*/
|
||||
public void dispatchScrollCaptureRequest(@NonNull IScrollCaptureController controller) {
|
||||
mHandler.obtainMessage(MSG_REQUEST_SCROLL_CAPTURE, controller).sendToTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect and include any ScrollCaptureCallback instances registered with the window.
|
||||
*
|
||||
* @see #addScrollCaptureCallback(ScrollCaptureCallback)
|
||||
* @param targets the search queue for targets
|
||||
*/
|
||||
private void collectRootScrollCaptureTargets(Queue<ScrollCaptureTarget> targets) {
|
||||
for (ScrollCaptureCallback cb : mRootScrollCaptureCallbacks) {
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* <p>
|
||||
* Either {@link IScrollCaptureController#onClientConnected(IScrollCaptureClient, Rect,
|
||||
* Point)} or {@link IScrollCaptureController#onClientUnavailable()} will be returned
|
||||
* depending on the results of the search.
|
||||
*
|
||||
* @param controller the interface to the system controller
|
||||
* @see ScrollCaptureTargetResolver
|
||||
*/
|
||||
private void handleScrollCaptureRequest(@NonNull IScrollCaptureController controller) {
|
||||
LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
|
||||
|
||||
// Window (root) level callbacks
|
||||
collectRootScrollCaptureTargets(targetList);
|
||||
|
||||
// Search through View-tree
|
||||
View rootView = getView();
|
||||
Point point = new Point();
|
||||
Rect rect = new Rect(0, 0, rootView.getWidth(), rootView.getHeight());
|
||||
getChildVisibleRect(rootView, rect, point);
|
||||
rootView.dispatchScrollCaptureSearch(rect, point, targetList);
|
||||
|
||||
// No-op path. Scroll capture not offered for this window.
|
||||
if (targetList.isEmpty()) {
|
||||
dispatchScrollCaptureSearchResult(controller, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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(controller, selected));
|
||||
}
|
||||
|
||||
/** Called by {@link #handleScrollCaptureRequest} when a result is returned */
|
||||
private void dispatchScrollCaptureSearchResult(
|
||||
@NonNull IScrollCaptureController controller,
|
||||
@Nullable ScrollCaptureTarget selectedTarget) {
|
||||
|
||||
// If timeout or no eligible targets found.
|
||||
if (selectedTarget == null) {
|
||||
try {
|
||||
if (DEBUG_SCROLL_CAPTURE) {
|
||||
Log.d(TAG, "scrollCaptureSearch returned no targets available.");
|
||||
}
|
||||
controller.onClientUnavailable();
|
||||
} catch (RemoteException e) {
|
||||
if (DEBUG_SCROLL_CAPTURE) {
|
||||
Log.w(TAG, "Failed to notify controller of scroll capture search result.", e);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a client instance and return it to the caller
|
||||
mScrollCaptureClient = new ScrollCaptureClient(selectedTarget, controller);
|
||||
try {
|
||||
if (DEBUG_SCROLL_CAPTURE) {
|
||||
Log.d(TAG, "scrollCaptureSearch returning client: " + getScrollCaptureClient());
|
||||
}
|
||||
controller.onClientConnected(
|
||||
mScrollCaptureClient,
|
||||
selectedTarget.getScrollBounds(),
|
||||
selectedTarget.getPositionInWindow());
|
||||
} catch (RemoteException e) {
|
||||
if (DEBUG_SCROLL_CAPTURE) {
|
||||
Log.w(TAG, "Failed to notify controller of scroll capture search result.", e);
|
||||
}
|
||||
mScrollCaptureClient.disconnect();
|
||||
mScrollCaptureClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void reportNextDraw() {
|
||||
if (mReportNextDraw == false) {
|
||||
@@ -9091,6 +9230,13 @@ public final class ViewRootImpl implements ViewParent,
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestScrollCapture(IScrollCaptureController controller) {
|
||||
final ViewRootImpl viewAncestor = mViewAncestor.get();
|
||||
if (viewAncestor != null) {
|
||||
viewAncestor.dispatchScrollCaptureRequest(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class CalledFromWrongThreadException extends AndroidRuntimeException {
|
||||
|
||||
@@ -2535,6 +2535,33 @@ public abstract class Window {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* System request to begin scroll capture.
|
||||
*
|
||||
* @param controller the controller to receive responses
|
||||
* @hide
|
||||
*/
|
||||
public void requestScrollCapture(IScrollCaptureController controller) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link ScrollCaptureCallback} with the root of this window.
|
||||
*
|
||||
* @param callback the callback to add
|
||||
* @hide
|
||||
*/
|
||||
public void addScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a {@link ScrollCaptureCallback} previously registered with this window.
|
||||
*
|
||||
* @param callback the callback to remove
|
||||
* @hide
|
||||
*/
|
||||
public void removeScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public void setTheme(int resId) {
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import android.util.MergedConfiguration;
|
||||
import android.view.IWindowSession;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ import android.util.TypedValue;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.Gravity;
|
||||
import android.view.IRotationWatcher.Stub;
|
||||
import android.view.IScrollCaptureController;
|
||||
import android.view.IWindowManager;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputEvent;
|
||||
@@ -89,6 +90,7 @@ import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScrollCaptureCallback;
|
||||
import android.view.SearchEvent;
|
||||
import android.view.SurfaceHolder.Callback2;
|
||||
import android.view.View;
|
||||
@@ -3916,4 +3918,35 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
|
||||
: null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* System request to begin scroll capture.
|
||||
*
|
||||
* @param controller the controller to receive responses
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public void requestScrollCapture(IScrollCaptureController controller) {
|
||||
getViewRootImpl().dispatchScrollCaptureRequest(controller);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler providing scrolling capture support for window content.
|
||||
*
|
||||
* @param callback the callback to add
|
||||
*/
|
||||
@Override
|
||||
public void addScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
|
||||
getViewRootImpl().addScrollCaptureCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the given {@link ScrollCaptureCallback}.
|
||||
*
|
||||
* @param callback the callback to remove
|
||||
*/
|
||||
@Override
|
||||
public void removeScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
|
||||
getViewRootImpl().removeScrollCaptureCallback(callback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.os.RemoteException;
|
||||
import android.util.MergedConfiguration;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.DragEvent;
|
||||
import android.view.IScrollCaptureController;
|
||||
import android.view.IWindow;
|
||||
import android.view.IWindowSession;
|
||||
import android.view.InsetsSourceControl;
|
||||
@@ -169,4 +170,13 @@ public class BaseIWindow extends IWindow.Stub {
|
||||
@Override
|
||||
public void dispatchPointerCaptureChanged(boolean hasCapture) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestScrollCapture(IScrollCaptureController controller) {
|
||||
try {
|
||||
controller.onClientUnavailable();
|
||||
} catch (RemoteException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
117
core/java/com/android/internal/view/ScrollCaptureInternal.java
Normal file
117
core/java/com/android/internal/view/ScrollCaptureInternal.java
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 com.android.internal.view;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.view.ScrollCaptureCallback;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
/**
|
||||
* Provides built-in framework level Scroll Capture support for standard scrolling Views.
|
||||
*/
|
||||
public class ScrollCaptureInternal {
|
||||
private static final String TAG = "ScrollCaptureInternal";
|
||||
|
||||
private static final int UP = -1;
|
||||
private static final int DOWN = 1;
|
||||
|
||||
/**
|
||||
* Not a ViewGroup, or cannot scroll according to View APIs.
|
||||
*/
|
||||
public static final int TYPE_FIXED = 0;
|
||||
|
||||
/**
|
||||
* Slides a single child view using mScrollX/mScrollY.
|
||||
*/
|
||||
public static final int TYPE_SCROLLING = 1;
|
||||
|
||||
/**
|
||||
* Slides child views through the viewport by translating their layout positions with {@link
|
||||
* View#offsetTopAndBottom(int)}. Manages Child view lifecycle, creating as needed and
|
||||
* binding views to data from an adapter. Views are reused whenever possible.
|
||||
*/
|
||||
public static final int TYPE_RECYCLING = 2;
|
||||
|
||||
/**
|
||||
* Performs tests on the given View and determines:
|
||||
* 1. If scrolling is possible
|
||||
* 2. What mechanisms are used for scrolling.
|
||||
* <p>
|
||||
* This needs to be fast and not alloc memory. It's called on everything in the tree not marked
|
||||
* as excluded during scroll capture search.
|
||||
*/
|
||||
public static int detectScrollingType(View view) {
|
||||
// Must be a ViewGroup
|
||||
if (!(view instanceof ViewGroup)) {
|
||||
return TYPE_FIXED;
|
||||
}
|
||||
// Confirm that it can scroll.
|
||||
if (!(view.canScrollVertically(DOWN) || view.canScrollVertically(UP))) {
|
||||
// Nothing to scroll here, move along.
|
||||
return TYPE_FIXED;
|
||||
}
|
||||
// ScrollViews accept only a single child.
|
||||
if (((ViewGroup) view).getChildCount() > 1) {
|
||||
return TYPE_RECYCLING;
|
||||
}
|
||||
//Because recycling containers don't use scrollY, a non-zero value means Scroll view.
|
||||
if (view.getScrollY() != 0) {
|
||||
return TYPE_SCROLLING;
|
||||
}
|
||||
// Since scrollY cannot be negative, this means a Recycling view.
|
||||
if (view.canScrollVertically(UP)) {
|
||||
return TYPE_RECYCLING;
|
||||
}
|
||||
// canScrollVertically(UP) == false, getScrollY() == 0, getChildCount() == 1.
|
||||
|
||||
// For Recycling containers, this should be a no-op (RecyclerView logs a warning)
|
||||
view.scrollTo(view.getScrollX(), 1);
|
||||
|
||||
// A scrolling container would have moved by 1px.
|
||||
if (view.getScrollY() == 1) {
|
||||
view.scrollTo(view.getScrollX(), 0);
|
||||
return TYPE_SCROLLING;
|
||||
}
|
||||
return TYPE_RECYCLING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a scroll capture callback for the given view if possible.
|
||||
*
|
||||
* @param view the view to capture
|
||||
* @param localVisibleRect the visible area of the given view in local coordinates, as supplied
|
||||
* by the view parent
|
||||
* @param positionInWindow the offset of localVisibleRect within the window
|
||||
*
|
||||
* @return a new callback or null if the View isn't supported
|
||||
*/
|
||||
@Nullable
|
||||
public ScrollCaptureCallback requestCallback(View view, Rect localVisibleRect,
|
||||
Point positionInWindow) {
|
||||
// Nothing to see here yet.
|
||||
int i = detectScrollingType(view);
|
||||
switch (i) {
|
||||
case TYPE_SCROLLING:
|
||||
return new ScrollCaptureViewSupport<>((ViewGroup) view,
|
||||
new ScrollViewCaptureHelper());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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 com.android.internal.view;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
|
||||
interface ScrollCaptureViewHelper<V extends View> {
|
||||
int UP = -1;
|
||||
int DOWN = 1;
|
||||
|
||||
/**
|
||||
* Verifies that the view is still visible and scrollable. If true is returned here, expect a
|
||||
* call to {@link #onComputeScrollBounds(View)} to follow.
|
||||
*
|
||||
* @param view the view being captured
|
||||
* @return true if the callback should respond to a request with scroll bounds
|
||||
*/
|
||||
default boolean onAcceptSession(@Nullable V view) {
|
||||
return view != null && view.isVisibleToUser()
|
||||
&& (view.canScrollVertically(UP) || view.canScrollVertically(DOWN));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a scroll capture request for a view, adjust the provided rect to cover the scrollable
|
||||
* content area. The default implementation returns the padded content area of {@code view}.
|
||||
*
|
||||
* @param view the view being captured
|
||||
*/
|
||||
default Rect onComputeScrollBounds(@Nullable V view) {
|
||||
return new Rect(view.getPaddingLeft(), view.getPaddingTop(),
|
||||
view.getWidth() - view.getPaddingRight(),
|
||||
view.getHeight() - view.getPaddingBottom());
|
||||
}
|
||||
/**
|
||||
* Adjust the target for capture.
|
||||
* <p>
|
||||
* Do not touch anything that may change layout positions or sizes on screen. Anything else may
|
||||
* be adjusted as long as it can be reversed in {@link #onPrepareForEnd(View)}.
|
||||
*
|
||||
* @param view the view being captured
|
||||
* @param scrollBounds the bounds within {@code view} where content scrolls
|
||||
*/
|
||||
void onPrepareForStart(@NonNull V view, Rect scrollBounds);
|
||||
|
||||
/**
|
||||
* Map the request onto the screen.
|
||||
* <p>
|
||||
* Given a rect describing the area to capture, relative to scrollBounds, take actions
|
||||
* necessary to bring the content within the rectangle into the visible area of the view if
|
||||
* needed and return the resulting rectangle describing the position and bounds of the area
|
||||
* which is visible.
|
||||
*
|
||||
* @param scrollBounds the area in which scrolling content moves, local to the {@code containing
|
||||
* view}
|
||||
* @param requestRect the area relative to {@code scrollBounds} which describes the location of
|
||||
* content to capture for the request
|
||||
* @return the visible area within scrollBounds of the requested rectangle, return {@code null}
|
||||
* in the case of an unrecoverable error condition, to abort the capture process
|
||||
*/
|
||||
Rect onScrollRequested(@NonNull V view, Rect scrollBounds, Rect requestRect);
|
||||
|
||||
/**
|
||||
* Restore the target after capture.
|
||||
* <p>
|
||||
* Put back anything that was changed in {@link #onPrepareForStart(View, Rect)}.
|
||||
*
|
||||
* @param view the view being captured
|
||||
*/
|
||||
void onPrepareForEnd(@NonNull V view);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* 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 com.android.internal.view;
|
||||
|
||||
import android.graphics.HardwareRenderer;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.RecordingCanvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.RenderNode;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.ScrollCaptureCallback;
|
||||
import android.view.ScrollCaptureSession;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Provides a ScrollCaptureCallback implementation for to handle arbitrary View-based scrolling
|
||||
* containers.
|
||||
* <p>
|
||||
* To use this class, supply the target view and an implementation of {@ScrollCaptureViewHelper}
|
||||
* to the callback.
|
||||
*
|
||||
* @param <V> the specific View subclass handled
|
||||
* @hide
|
||||
*/
|
||||
public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCallback {
|
||||
|
||||
private final WeakReference<V> mWeakView;
|
||||
private final ScrollCaptureViewHelper<V> mViewHelper;
|
||||
private ViewRenderer mRenderer;
|
||||
private Handler mUiHandler;
|
||||
private boolean mStarted;
|
||||
private boolean mEnded;
|
||||
|
||||
static <V extends View> ScrollCaptureCallback createCallback(V view,
|
||||
ScrollCaptureViewHelper<V> impl) {
|
||||
return new ScrollCaptureViewSupport<>(view, impl);
|
||||
}
|
||||
|
||||
ScrollCaptureViewSupport(V containingView, ScrollCaptureViewHelper<V> viewHelper) {
|
||||
mWeakView = new WeakReference<>(containingView);
|
||||
mRenderer = new ViewRenderer();
|
||||
mUiHandler = containingView.getHandler();
|
||||
mViewHelper = viewHelper;
|
||||
}
|
||||
|
||||
// Base implementation of ScrollCaptureCallback
|
||||
|
||||
@Override
|
||||
public final void onScrollCaptureSearch(Consumer<Rect> onReady) {
|
||||
V view = mWeakView.get();
|
||||
mStarted = false;
|
||||
mEnded = false;
|
||||
|
||||
if (view != null && view.isVisibleToUser() && mViewHelper.onAcceptSession(view)) {
|
||||
onReady.accept(mViewHelper.onComputeScrollBounds(view));
|
||||
return;
|
||||
}
|
||||
onReady.accept(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onScrollCaptureStart(ScrollCaptureSession session, Runnable onReady) {
|
||||
V view = mWeakView.get();
|
||||
mEnded = false;
|
||||
mStarted = true;
|
||||
|
||||
// Note: If somehow the view is already gone or detached, the first call to
|
||||
// {@code onScrollCaptureImageRequest} will return an error and request the session to
|
||||
// end.
|
||||
if (view != null && view.isVisibleToUser()) {
|
||||
mRenderer.setSurface(session.getSurface());
|
||||
mViewHelper.onPrepareForStart(view, session.getScrollBounds());
|
||||
}
|
||||
onReady.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onScrollCaptureImageRequest(ScrollCaptureSession session, Rect requestRect) {
|
||||
V view = mWeakView.get();
|
||||
if (view == null || !view.isVisibleToUser()) {
|
||||
// Signal to the controller that we have a problem and can't continue.
|
||||
session.notifyBufferSent(0, null);
|
||||
return;
|
||||
}
|
||||
Rect captureArea = mViewHelper.onScrollRequested(view, session.getScrollBounds(),
|
||||
requestRect);
|
||||
mRenderer.renderFrame(view, captureArea, mUiHandler,
|
||||
() -> session.notifyBufferSent(0, captureArea));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onScrollCaptureEnd(Runnable onReady) {
|
||||
V view = mWeakView.get();
|
||||
if (mStarted && !mEnded) {
|
||||
mViewHelper.onPrepareForEnd(view);
|
||||
/* empty */
|
||||
mEnded = true;
|
||||
mRenderer.trimMemory();
|
||||
mRenderer.setSurface(null);
|
||||
}
|
||||
onReady.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper class which assists in rendering sections of the view hierarchy relative to a
|
||||
* given view. Used by framework implementations of ScrollCaptureHandler to render and dispatch
|
||||
* image requests.
|
||||
*/
|
||||
static final class ViewRenderer {
|
||||
// alpha, "reasonable default" from Javadoc
|
||||
private static final float AMBIENT_SHADOW_ALPHA = 0.039f;
|
||||
private static final float SPOT_SHADOW_ALPHA = 0.039f;
|
||||
|
||||
// Default values:
|
||||
// lightX = (screen.width() / 2) - windowLeft
|
||||
// lightY = 0 - windowTop
|
||||
// lightZ = 600dp
|
||||
// lightRadius = 800dp
|
||||
private static final float LIGHT_Z_DP = 400;
|
||||
private static final float LIGHT_RADIUS_DP = 800;
|
||||
private static final String TAG = "ViewRenderer";
|
||||
|
||||
private HardwareRenderer mRenderer;
|
||||
private RenderNode mRootRenderNode;
|
||||
private final RectF mTempRectF = new RectF();
|
||||
private final Rect mSourceRect = new Rect();
|
||||
private final Rect mTempRect = new Rect();
|
||||
private final Matrix mTempMatrix = new Matrix();
|
||||
private final int[] mTempLocation = new int[2];
|
||||
private long mLastRenderedSourceDrawingId = -1;
|
||||
|
||||
|
||||
ViewRenderer() {
|
||||
mRenderer = new HardwareRenderer();
|
||||
mRootRenderNode = new RenderNode("ScrollCaptureRoot");
|
||||
mRenderer.setContentRoot(mRootRenderNode);
|
||||
|
||||
// TODO: Figure out a way to flip this on when we are sure the source window is opaque
|
||||
mRenderer.setOpaque(false);
|
||||
}
|
||||
|
||||
public void setSurface(Surface surface) {
|
||||
mRenderer.setSurface(surface);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache invalidation check. If the source view is the same as the previous call (which is
|
||||
* mostly always the case, then we can skip setting up lighting on each call (for now)
|
||||
*
|
||||
* @return true if the view changed, false if the view was previously rendered by this class
|
||||
*/
|
||||
private boolean updateForView(View source) {
|
||||
if (mLastRenderedSourceDrawingId == source.getUniqueDrawingId()) {
|
||||
return false;
|
||||
}
|
||||
mLastRenderedSourceDrawingId = source.getUniqueDrawingId();
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: may need to adjust lightY based on the virtual canvas position to get
|
||||
// consistent shadow positions across the whole capture. Or possibly just
|
||||
// pull lightZ way back to make shadows more uniform.
|
||||
private void setupLighting(View mSource) {
|
||||
mLastRenderedSourceDrawingId = mSource.getUniqueDrawingId();
|
||||
DisplayMetrics metrics = mSource.getResources().getDisplayMetrics();
|
||||
mSource.getLocationOnScreen(mTempLocation);
|
||||
final float lightX = metrics.widthPixels / 2f - mTempLocation[0];
|
||||
final float lightY = metrics.heightPixels - mTempLocation[1];
|
||||
final int lightZ = (int) (LIGHT_Z_DP * metrics.density);
|
||||
final int lightRadius = (int) (LIGHT_RADIUS_DP * metrics.density);
|
||||
|
||||
// Enable shadows for elevation/Z
|
||||
mRenderer.setLightSourceGeometry(lightX, lightY, lightZ, lightRadius);
|
||||
mRenderer.setLightSourceAlpha(AMBIENT_SHADOW_ALPHA, SPOT_SHADOW_ALPHA);
|
||||
|
||||
}
|
||||
|
||||
public void renderFrame(View localReference, Rect sourceRect, Handler handler,
|
||||
Runnable onFrameCommitted) {
|
||||
if (updateForView(localReference)) {
|
||||
setupLighting(localReference);
|
||||
}
|
||||
buildRootDisplayList(localReference, sourceRect);
|
||||
HardwareRenderer.FrameRenderRequest request = mRenderer.createRenderRequest();
|
||||
request.setVsyncTime(SystemClock.elapsedRealtimeNanos());
|
||||
request.setFrameCommitCallback(handler::post, onFrameCommitted);
|
||||
request.setWaitForPresent(true);
|
||||
request.syncAndDraw();
|
||||
}
|
||||
|
||||
public void trimMemory() {
|
||||
mRenderer.clearContent();
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
mRenderer.destroy();
|
||||
}
|
||||
|
||||
private void transformToRoot(View local, Rect localRect, Rect outRect) {
|
||||
mTempMatrix.reset();
|
||||
local.transformMatrixToGlobal(mTempMatrix);
|
||||
mTempRectF.set(localRect);
|
||||
mTempMatrix.mapRect(mTempRectF);
|
||||
mTempRectF.round(outRect);
|
||||
}
|
||||
|
||||
private void buildRootDisplayList(View source, Rect localSourceRect) {
|
||||
final View captureSource = source.getRootView();
|
||||
transformToRoot(source, localSourceRect, mTempRect);
|
||||
mRootRenderNode.setPosition(0, 0, mTempRect.width(), mTempRect.height());
|
||||
RecordingCanvas canvas = mRootRenderNode.beginRecording(mTempRect.width(),
|
||||
mTempRect.height());
|
||||
canvas.translate(-mTempRect.left, -mTempRect.top);
|
||||
canvas.drawRenderNode(captureSource.updateDisplayListIfDirty());
|
||||
mRootRenderNode.endRecording();
|
||||
}
|
||||
}
|
||||
}
|
||||
167
core/java/com/android/internal/view/ScrollViewCaptureHelper.java
Normal file
167
core/java/com/android/internal/view/ScrollViewCaptureHelper.java
Normal file
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* 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 com.android.internal.view;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
|
||||
/**
|
||||
* ScrollCapture for ScrollView and <i>ScrollView-like</i> ViewGroups.
|
||||
* <p>
|
||||
* Requirements for proper operation:
|
||||
* <ul>
|
||||
* <li>contains at most 1 child.
|
||||
* <li>scrolls to absolute positions with {@link View#scrollTo(int, int)}.
|
||||
* <li>has a finite, known content height and scrolling range
|
||||
* <li>correctly implements {@link View#canScrollVertically(int)}
|
||||
* <li>correctly implements {@link ViewParent#requestChildRectangleOnScreen(View,
|
||||
* Rect, boolean)}
|
||||
* </ul>
|
||||
*/
|
||||
public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGroup> {
|
||||
private int mStartScrollY;
|
||||
private boolean mScrollBarEnabled;
|
||||
private int mOverScrollMode;
|
||||
|
||||
/** @see ScrollCaptureViewHelper#onPrepareForStart(View, Rect) */
|
||||
public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) {
|
||||
mStartScrollY = view.getScrollY();
|
||||
mOverScrollMode = view.getOverScrollMode();
|
||||
if (mOverScrollMode != View.OVER_SCROLL_NEVER) {
|
||||
view.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||
}
|
||||
mScrollBarEnabled = view.isVerticalScrollBarEnabled();
|
||||
if (mScrollBarEnabled) {
|
||||
view.setVerticalScrollBarEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
/** @see ScrollCaptureViewHelper#onScrollRequested(View, Rect, Rect) */
|
||||
public Rect onScrollRequested(@NonNull ViewGroup view, Rect scrollBounds, Rect requestRect) {
|
||||
final View contentView = view.getChildAt(0); // returns null, does not throw IOOBE
|
||||
if (contentView == null) {
|
||||
return null;
|
||||
}
|
||||
/*
|
||||
+---------+ <----+ Content [25,25 - 275,1025] (w=250,h=1000)
|
||||
| |
|
||||
...|.........|... startScrollY=100
|
||||
| |
|
||||
+--+---------+---+ <--+ Container View [0,0 - 300,300] (scrollY=200)
|
||||
| . . |
|
||||
--- | . +-----+ <------+ Scroll Bounds [50,50 - 250,250] (200x200)
|
||||
^ | . | | . | (Local to Container View, fixed/un-scrolled)
|
||||
| | . | | . |
|
||||
| | . | | . |
|
||||
| | . +-----+ . |
|
||||
| | . . |
|
||||
| +--+---------+---+
|
||||
| | |
|
||||
-+- | +-----+ |
|
||||
| |#####| | <--+ Requested Bounds [0,300 - 200,400] (200x100)
|
||||
| +-----+ | (Local to Scroll Bounds, fixed/un-scrolled)
|
||||
| |
|
||||
+---------+
|
||||
|
||||
Container View (ScrollView) [0,0 - 300,300] (scrollY = 200)
|
||||
\__ Content [25,25 - 275,1025] (250x1000) (contentView)
|
||||
\__ Scroll Bounds[50,50 - 250,250] (w=200,h=200)
|
||||
\__ Requested Bounds[0,300 - 200,400] (200x100)
|
||||
*/
|
||||
|
||||
// 0) adjust the requestRect to account for scroll change since start
|
||||
//
|
||||
// Scroll Bounds[50,50 - 250,250] (w=200,h=200)
|
||||
// \__ Requested Bounds[0,200 - 200,300] (200x100)
|
||||
|
||||
// (y-100) (scrollY - mStartScrollY)
|
||||
int scrollDelta = view.getScrollY() - mStartScrollY;
|
||||
|
||||
// 1) Translate request rect to make it relative to container view
|
||||
//
|
||||
// Container View [0,0 - 300,300] (scrollY=200)
|
||||
// \__ Requested Bounds[50,250 - 250,350] (w=250, h=100)
|
||||
|
||||
// (x+50,y+50)
|
||||
Rect requestedContainerBounds = new Rect(requestRect);
|
||||
requestedContainerBounds.offset(0, -scrollDelta);
|
||||
requestedContainerBounds.offset(scrollBounds.left, scrollBounds.top);
|
||||
|
||||
// 2) Translate from container to contentView relative (applying container scrollY)
|
||||
//
|
||||
// Container View [0,0 - 300,300] (scrollY=200)
|
||||
// \__ Content [25,25 - 275,1025] (250x1000) (contentView)
|
||||
// \__ Requested Bounds[25,425 - 200,525] (w=250, h=100)
|
||||
|
||||
// (x-25,y+175) (scrollY - content.top)
|
||||
Rect requestedContentBounds = new Rect(requestedContainerBounds);
|
||||
requestedContentBounds.offset(
|
||||
view.getScrollX() - contentView.getLeft(),
|
||||
view.getScrollY() - contentView.getTop());
|
||||
|
||||
|
||||
|
||||
// requestRect is now local to contentView as requestedContentBounds
|
||||
// contentView (and each parent in turn if possible) will be scrolled
|
||||
// (if necessary) to make all of requestedContent visible, (if possible!)
|
||||
contentView.requestRectangleOnScreen(new Rect(requestedContentBounds), true);
|
||||
|
||||
// update new offset between starting and current scroll position
|
||||
scrollDelta = view.getScrollY() - mStartScrollY;
|
||||
|
||||
|
||||
// TODO: adjust to avoid occlusions/minimize scroll changes
|
||||
|
||||
Point offset = new Point();
|
||||
final Rect capturedRect = new Rect(requestedContentBounds); // empty
|
||||
if (!view.getChildVisibleRect(contentView, capturedRect, offset)) {
|
||||
capturedRect.setEmpty();
|
||||
return capturedRect;
|
||||
}
|
||||
// Transform back from global to content-view local
|
||||
capturedRect.offset(-offset.x, -offset.y);
|
||||
|
||||
// Then back to container view
|
||||
capturedRect.offset(
|
||||
contentView.getLeft() - view.getScrollX(),
|
||||
contentView.getTop() - view.getScrollY());
|
||||
|
||||
|
||||
// And back to relative to scrollBounds
|
||||
capturedRect.offset(-scrollBounds.left, -scrollBounds.top);
|
||||
|
||||
// Apply scrollDelta again to return to make capturedRect relative to scrollBounds at
|
||||
// the scroll position at start of capture.
|
||||
capturedRect.offset(0, scrollDelta);
|
||||
return capturedRect;
|
||||
}
|
||||
|
||||
/** @see ScrollCaptureViewHelper#onPrepareForEnd(View) */
|
||||
public void onPrepareForEnd(@NonNull ViewGroup view) {
|
||||
view.scrollTo(0, mStartScrollY);
|
||||
if (mOverScrollMode != View.OVER_SCROLL_NEVER) {
|
||||
view.setOverScrollMode(mOverScrollMode);
|
||||
}
|
||||
if (mScrollBarEnabled) {
|
||||
view.setVerticalScrollBarEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2527,6 +2527,21 @@
|
||||
<flag name="noExcludeDescendants" value="0x8" />
|
||||
</attr>
|
||||
|
||||
<!-- Hints the Android System whether the this View should be considered a scroll capture target. -->
|
||||
<attr name="scrollCaptureHint">
|
||||
<!-- Let the Android System determine if the view can be a scroll capture target. -->
|
||||
<flag name="auto" value="0" />
|
||||
<!-- Hint the Android System that this view is a likely target. If capable, it will
|
||||
be ranked above other views without this flag. -->
|
||||
<flag name="include" value="0x1" />
|
||||
<!-- Hint the Android System that this view should never be considered a scroll capture
|
||||
target. -->
|
||||
<flag name="exclude" value="0x2" />
|
||||
<!-- Hint the Android System that this view's children should not be examined and should
|
||||
be excluded as a scroll capture target. -->
|
||||
<flag name="excludeDescendants" value="0x4" />
|
||||
</attr>
|
||||
|
||||
<!-- Boolean that controls whether a view can take focus while in touch mode.
|
||||
If this is true for a view, that view can gain focus when clicked on, and can keep
|
||||
focus if another view is clicked on that doesn't have this attribute set to true. -->
|
||||
|
||||
@@ -3020,6 +3020,8 @@
|
||||
<public name="preserveLegacyExternalStorage" />
|
||||
<public name="mimeGroup" />
|
||||
<public name="gwpAsanMode" />
|
||||
<!-- @hide -->
|
||||
<public name="scrollCaptureHint" />
|
||||
</public-group>
|
||||
|
||||
<public-group type="drawable" first-id="0x010800b5">
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
/*
|
||||
* 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 static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
import static androidx.test.InstrumentationRegistry.getTargetContext;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
/**
|
||||
* Tests of {@link ScrollCaptureClient}.
|
||||
*/
|
||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ScrollCaptureClientTest {
|
||||
|
||||
private final Point mPositionInWindow = new Point(1, 2);
|
||||
private final Rect mLocalVisibleRect = new Rect(2, 3, 4, 5);
|
||||
private final Rect mScrollBounds = new Rect(3, 4, 5, 6);
|
||||
|
||||
private Handler mHandler;
|
||||
private ScrollCaptureTarget mTarget1;
|
||||
|
||||
@Mock
|
||||
private Surface mSurface;
|
||||
@Mock
|
||||
private IScrollCaptureController mClientCallbacks;
|
||||
@Mock
|
||||
private View mMockView1;
|
||||
@Mock
|
||||
private ScrollCaptureCallback mCallback1;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mHandler = new Handler(getTargetContext().getMainLooper());
|
||||
|
||||
when(mMockView1.getHandler()).thenReturn(mHandler);
|
||||
when(mMockView1.getScrollCaptureHint()).thenReturn(View.SCROLL_CAPTURE_HINT_INCLUDE);
|
||||
|
||||
mTarget1 = new ScrollCaptureTarget(
|
||||
mMockView1, mLocalVisibleRect, mPositionInWindow, mCallback1);
|
||||
mTarget1.setScrollBounds(mScrollBounds);
|
||||
}
|
||||
|
||||
/** Test the DelayedAction timeout helper class works as expected. */
|
||||
@Test
|
||||
public void testDelayedAction() {
|
||||
Runnable action = Mockito.mock(Runnable.class);
|
||||
ScrollCaptureClient.DelayedAction delayed =
|
||||
new ScrollCaptureClient.DelayedAction(mHandler, 100, action);
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException ex) {
|
||||
/* ignore */
|
||||
}
|
||||
getInstrumentation().waitForIdleSync();
|
||||
assertFalse(delayed.cancel());
|
||||
assertFalse(delayed.timeoutNow());
|
||||
verify(action, times(1)).run();
|
||||
}
|
||||
|
||||
/** Test the DelayedAction cancel() */
|
||||
@Test
|
||||
public void testDelayedAction_cancel() {
|
||||
Runnable action = Mockito.mock(Runnable.class);
|
||||
ScrollCaptureClient.DelayedAction delayed =
|
||||
new ScrollCaptureClient.DelayedAction(mHandler, 100, action);
|
||||
try {
|
||||
Thread.sleep(50);
|
||||
} catch (InterruptedException ex) {
|
||||
/* ignore */
|
||||
}
|
||||
assertTrue(delayed.cancel());
|
||||
assertFalse(delayed.timeoutNow());
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException ex) {
|
||||
/* ignore */
|
||||
}
|
||||
getInstrumentation().waitForIdleSync();
|
||||
verify(action, never()).run();
|
||||
}
|
||||
|
||||
/** Test the DelayedAction timeoutNow() - for testing only */
|
||||
@Test
|
||||
public void testDelayedAction_timeoutNow() {
|
||||
Runnable action = Mockito.mock(Runnable.class);
|
||||
ScrollCaptureClient.DelayedAction delayed =
|
||||
new ScrollCaptureClient.DelayedAction(mHandler, 100, action);
|
||||
try {
|
||||
Thread.sleep(50);
|
||||
} catch (InterruptedException ex) {
|
||||
/* ignore */
|
||||
}
|
||||
assertTrue(delayed.timeoutNow());
|
||||
assertFalse(delayed.cancel());
|
||||
getInstrumentation().waitForIdleSync();
|
||||
verify(action, times(1)).run();
|
||||
}
|
||||
|
||||
/** Test creating a client with valid info */
|
||||
@Test
|
||||
public void testConstruction() {
|
||||
new ScrollCaptureClient(mTarget1, mClientCallbacks);
|
||||
}
|
||||
|
||||
/** Test creating a client fails if arguments are not valid. */
|
||||
@Test
|
||||
public void testConstruction_requiresScrollBounds() {
|
||||
try {
|
||||
mTarget1.setScrollBounds(null);
|
||||
new ScrollCaptureClient(mTarget1, mClientCallbacks);
|
||||
fail("An exception was expected.");
|
||||
} catch (RuntimeException ex) {
|
||||
// Ignore, expected.
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static Answer<Void> runRunnable(int arg) {
|
||||
return invocation -> {
|
||||
Runnable r = invocation.getArgument(arg);
|
||||
r.run();
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static Answer<Void> reportBufferSent(int sessionArg, long frameNum, Rect capturedArea) {
|
||||
return invocation -> {
|
||||
ScrollCaptureSession session = invocation.getArgument(sessionArg);
|
||||
session.notifyBufferSent(frameNum, capturedArea);
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
/** @see ScrollCaptureClient#startCapture(Surface) */
|
||||
@Test
|
||||
public void testStartCapture() throws Exception {
|
||||
final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
|
||||
|
||||
// Have the session start accepted immediately
|
||||
doAnswer(runRunnable(1)).when(mCallback1)
|
||||
.onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class));
|
||||
client.startCapture(mSurface);
|
||||
getInstrumentation().waitForIdleSync();
|
||||
|
||||
verify(mCallback1, times(1))
|
||||
.onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class));
|
||||
verify(mClientCallbacks, times(1)).onCaptureStarted();
|
||||
verifyNoMoreInteractions(mClientCallbacks);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartCaptureTimeout() throws Exception {
|
||||
final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
|
||||
client.startCapture(mSurface);
|
||||
|
||||
// Force timeout to fire
|
||||
client.getTimeoutAction().timeoutNow();
|
||||
|
||||
getInstrumentation().waitForIdleSync();
|
||||
verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class));
|
||||
}
|
||||
|
||||
private void startClient(ScrollCaptureClient client) throws Exception {
|
||||
doAnswer(runRunnable(1)).when(mCallback1)
|
||||
.onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class));
|
||||
client.startCapture(mSurface);
|
||||
getInstrumentation().waitForIdleSync();
|
||||
reset(mCallback1, mClientCallbacks);
|
||||
}
|
||||
|
||||
/** @see ScrollCaptureClient#requestImage(Rect) */
|
||||
@Test
|
||||
public void testRequestImage() throws Exception {
|
||||
final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
|
||||
startClient(client);
|
||||
|
||||
// Stub the callback to complete the request immediately
|
||||
doAnswer(reportBufferSent(/* sessionArg */ 0, /* frameNum */ 1L, new Rect(1, 2, 3, 4)))
|
||||
.when(mCallback1)
|
||||
.onScrollCaptureImageRequest(any(ScrollCaptureSession.class), any(Rect.class));
|
||||
|
||||
// Make the inbound binder call
|
||||
client.requestImage(new Rect(1, 2, 3, 4));
|
||||
|
||||
// Wait for handler thread dispatch
|
||||
getInstrumentation().waitForIdleSync();
|
||||
verify(mCallback1, times(1)).onScrollCaptureImageRequest(
|
||||
any(ScrollCaptureSession.class), eq(new Rect(1, 2, 3, 4)));
|
||||
|
||||
// Wait for binder thread dispatch
|
||||
getInstrumentation().waitForIdleSync();
|
||||
verify(mClientCallbacks, times(1)).onCaptureBufferSent(eq(1L), eq(new Rect(1, 2, 3, 4)));
|
||||
|
||||
verifyNoMoreInteractions(mCallback1, mClientCallbacks);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestImageTimeout() throws Exception {
|
||||
final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
|
||||
startClient(client);
|
||||
|
||||
// Make the inbound binder call
|
||||
client.requestImage(new Rect(1, 2, 3, 4));
|
||||
|
||||
// Wait for handler thread dispatch
|
||||
getInstrumentation().waitForIdleSync();
|
||||
verify(mCallback1, times(1)).onScrollCaptureImageRequest(
|
||||
any(ScrollCaptureSession.class), eq(new Rect(1, 2, 3, 4)));
|
||||
|
||||
// Force timeout to fire
|
||||
client.getTimeoutAction().timeoutNow();
|
||||
getInstrumentation().waitForIdleSync();
|
||||
|
||||
// (callback not stubbed, does nothing)
|
||||
// Timeout triggers request to end capture
|
||||
verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class));
|
||||
verifyNoMoreInteractions(mCallback1, mClientCallbacks);
|
||||
}
|
||||
|
||||
/** @see ScrollCaptureClient#endCapture() */
|
||||
@Test
|
||||
public void testEndCapture() throws Exception {
|
||||
final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
|
||||
startClient(client);
|
||||
|
||||
// Stub the callback to complete the request immediately
|
||||
doAnswer(runRunnable(0))
|
||||
.when(mCallback1)
|
||||
.onScrollCaptureEnd(any(Runnable.class));
|
||||
|
||||
// Make the inbound binder call
|
||||
client.endCapture();
|
||||
|
||||
// Wait for handler thread dispatch
|
||||
getInstrumentation().waitForIdleSync();
|
||||
verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class));
|
||||
|
||||
// Wait for binder thread dispatch
|
||||
getInstrumentation().waitForIdleSync();
|
||||
verify(mClientCallbacks, times(1)).onConnectionClosed();
|
||||
|
||||
verifyNoMoreInteractions(mCallback1, mClientCallbacks);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEndCaptureTimeout() throws Exception {
|
||||
final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
|
||||
startClient(client);
|
||||
|
||||
// Make the inbound binder call
|
||||
client.endCapture();
|
||||
|
||||
// Wait for handler thread dispatch
|
||||
getInstrumentation().waitForIdleSync();
|
||||
verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class));
|
||||
|
||||
// Force timeout to fire
|
||||
client.getTimeoutAction().timeoutNow();
|
||||
|
||||
// Wait for binder thread dispatch
|
||||
getInstrumentation().waitForIdleSync();
|
||||
verify(mClientCallbacks, times(1)).onConnectionClosed();
|
||||
|
||||
verifyNoMoreInteractions(mCallback1, mClientCallbacks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
/*
|
||||
* 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 static androidx.test.InstrumentationRegistry.getTargetContext;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Tests of {@link ScrollCaptureTargetResolver}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ScrollCaptureTargetResolverTest {
|
||||
|
||||
private static final long TEST_TIMEOUT_MS = 2000;
|
||||
private static final long RESOLVER_TIMEOUT_MS = 1000;
|
||||
|
||||
private Handler mHandler;
|
||||
private TargetConsumer mTargetConsumer;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mTargetConsumer = new TargetConsumer();
|
||||
mHandler = new Handler(getTargetContext().getMainLooper());
|
||||
}
|
||||
|
||||
@Test(timeout = TEST_TIMEOUT_MS)
|
||||
public void testEmptyQueue() throws InterruptedException {
|
||||
ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(new LinkedList<>());
|
||||
resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
|
||||
|
||||
// Test only
|
||||
resolver.waitForResult();
|
||||
|
||||
ScrollCaptureTarget result = mTargetConsumer.getLastValue();
|
||||
assertNull("Expected null due to empty queue", result);
|
||||
}
|
||||
|
||||
@Test(timeout = TEST_TIMEOUT_MS)
|
||||
public void testNoValidTargets() throws InterruptedException {
|
||||
LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
|
||||
|
||||
// Supplies scrollBounds = null
|
||||
FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback();
|
||||
callback1.setScrollBounds(null);
|
||||
ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50),
|
||||
new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
|
||||
// Supplies scrollBounds = empty rect
|
||||
FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
|
||||
callback2.setScrollBounds(new Rect());
|
||||
ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50),
|
||||
new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE);
|
||||
|
||||
targetQueue.add(target1);
|
||||
targetQueue.add(target2);
|
||||
|
||||
ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
|
||||
resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
|
||||
|
||||
// Test only
|
||||
resolver.waitForResult();
|
||||
|
||||
ScrollCaptureTarget result = mTargetConsumer.getLastValue();
|
||||
assertNull("Expected null due to no valid targets", result);
|
||||
}
|
||||
|
||||
@Test(timeout = TEST_TIMEOUT_MS)
|
||||
public void testSingleTarget() throws InterruptedException {
|
||||
FakeScrollCaptureCallback callback = new FakeScrollCaptureCallback();
|
||||
ScrollCaptureTarget target = createTarget(callback,
|
||||
new Rect(20, 30, 40, 50), new Point(10, 10),
|
||||
View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
callback.setScrollBounds(new Rect(2, 2, 18, 18));
|
||||
|
||||
LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
|
||||
targetQueue.add(target);
|
||||
ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
|
||||
resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
|
||||
|
||||
// Test only
|
||||
resolver.waitForResult();
|
||||
|
||||
ScrollCaptureTarget result = mTargetConsumer.getLastValue();
|
||||
assertSame("Excepted the same target as a result", target, result);
|
||||
assertEquals("result has wrong scroll bounds",
|
||||
new Rect(2, 2, 18, 18), result.getScrollBounds());
|
||||
}
|
||||
|
||||
@Test(timeout = TEST_TIMEOUT_MS)
|
||||
public void testSingleTarget_backgroundThread() throws InterruptedException {
|
||||
BackgroundTestCallback callback1 = new BackgroundTestCallback();
|
||||
ScrollCaptureTarget target1 = createTarget(callback1,
|
||||
new Rect(20, 30, 40, 50), new Point(10, 10),
|
||||
View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
callback1.setDelay(100);
|
||||
callback1.setScrollBounds(new Rect(2, 2, 18, 18));
|
||||
|
||||
LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
|
||||
targetQueue.add(target1);
|
||||
|
||||
ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
|
||||
resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
|
||||
|
||||
// Test only
|
||||
resolver.waitForResult();
|
||||
|
||||
ScrollCaptureTarget result = mTargetConsumer.getLastValue();
|
||||
assertSame("Excepted the single target1 as a result", target1, result);
|
||||
assertEquals("Result has wrong scroll bounds",
|
||||
new Rect(2, 2, 18, 18), result.getScrollBounds());
|
||||
}
|
||||
|
||||
@Test(timeout = TEST_TIMEOUT_MS)
|
||||
public void testPreferNonEmptyBounds() throws InterruptedException {
|
||||
LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
|
||||
|
||||
FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback();
|
||||
callback1.setScrollBounds(new Rect());
|
||||
ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50),
|
||||
new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
|
||||
FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
|
||||
callback2.setScrollBounds(new Rect(0, 0, 20, 20));
|
||||
ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50),
|
||||
new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE);
|
||||
|
||||
FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback();
|
||||
callback3.setScrollBounds(null);
|
||||
ScrollCaptureTarget target3 = createTarget(callback3, new Rect(20, 30, 40, 50),
|
||||
new Point(0, 40), View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
|
||||
targetQueue.add(target1);
|
||||
targetQueue.add(target2); // scrollBounds not null or empty()
|
||||
targetQueue.add(target3);
|
||||
|
||||
ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
|
||||
resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
|
||||
resolver.waitForResult();
|
||||
|
||||
ScrollCaptureTarget result = mTargetConsumer.getLastValue();
|
||||
assertEquals("Expected " + target2 + " as a result", target2, result);
|
||||
assertEquals("result has wrong scroll bounds",
|
||||
new Rect(0, 0, 20, 20), result.getScrollBounds());
|
||||
}
|
||||
|
||||
@Test(timeout = TEST_TIMEOUT_MS)
|
||||
public void testPreferHintInclude() throws InterruptedException {
|
||||
LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
|
||||
|
||||
FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback();
|
||||
callback1.setScrollBounds(new Rect(0, 0, 20, 20));
|
||||
ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50),
|
||||
new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
|
||||
FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
|
||||
callback2.setScrollBounds(new Rect(1, 1, 19, 19));
|
||||
ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50),
|
||||
new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE);
|
||||
|
||||
FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback();
|
||||
callback3.setScrollBounds(new Rect(2, 2, 18, 18));
|
||||
ScrollCaptureTarget target3 = createTarget(callback3, new Rect(20, 30, 40, 50),
|
||||
new Point(0, 40), View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
|
||||
targetQueue.add(target1);
|
||||
targetQueue.add(target2); // * INCLUDE > AUTO
|
||||
targetQueue.add(target3);
|
||||
|
||||
ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
|
||||
resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
|
||||
|
||||
resolver.waitForResult();
|
||||
|
||||
ScrollCaptureTarget result = mTargetConsumer.getLastValue();
|
||||
assertEquals("input = " + targetQueue + " Expected " + target2
|
||||
+ " as the result, due to hint=INCLUDE", target2, result);
|
||||
assertEquals("result has wrong scroll bounds",
|
||||
new Rect(1, 1, 19, 19), result.getScrollBounds());
|
||||
}
|
||||
|
||||
@Test(timeout = TEST_TIMEOUT_MS)
|
||||
public void testDescendantPreferred() throws InterruptedException {
|
||||
LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
|
||||
|
||||
ViewGroup targetView1 = new FakeRootView(getTargetContext(), 0, 0, 60, 60); // 60x60
|
||||
ViewGroup targetView2 = new FakeRootView(getTargetContext(), 20, 30, 40, 50); // 20x20
|
||||
ViewGroup targetView3 = new FakeRootView(getTargetContext(), 5, 5, 15, 15); // 10x10
|
||||
|
||||
targetView1.addView(targetView2);
|
||||
targetView2.addView(targetView3);
|
||||
|
||||
// Create first target with an unrelated parent
|
||||
FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback();
|
||||
callback1.setScrollBounds(new Rect(0, 0, 60, 60));
|
||||
ScrollCaptureTarget target1 = createTargetWithView(targetView1, callback1,
|
||||
new Rect(0, 0, 60, 60),
|
||||
new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
|
||||
// Create second target associated with a view within parent2
|
||||
FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
|
||||
callback2.setScrollBounds(new Rect(0, 0, 20, 20));
|
||||
ScrollCaptureTarget target2 = createTargetWithView(targetView2, callback2,
|
||||
new Rect(0, 0, 20, 20),
|
||||
new Point(20, 30), View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
|
||||
// Create third target associated with a view within parent3
|
||||
FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback();
|
||||
callback3.setScrollBounds(new Rect(0, 0, 15, 15));
|
||||
ScrollCaptureTarget target3 = createTargetWithView(targetView3, callback3,
|
||||
new Rect(0, 0, 15, 15),
|
||||
new Point(25, 35), View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
|
||||
targetQueue.add(target1); // auto, 60x60
|
||||
targetQueue.add(target2); // auto, 20x20
|
||||
targetQueue.add(target3); // auto, 15x15 <- innermost scrollable
|
||||
|
||||
ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
|
||||
resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
|
||||
|
||||
// Test only
|
||||
resolver.waitForResult();
|
||||
|
||||
ScrollCaptureTarget result = mTargetConsumer.getLastValue();
|
||||
assertSame("Expected target3 as the result, due to relation", target3, result);
|
||||
assertEquals("result has wrong scroll bounds",
|
||||
new Rect(0, 0, 15, 15), result.getScrollBounds());
|
||||
}
|
||||
|
||||
/**
|
||||
* If a timeout expires, late results are ignored.
|
||||
*/
|
||||
@Test(timeout = TEST_TIMEOUT_MS)
|
||||
public void testTimeout() throws InterruptedException {
|
||||
LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
|
||||
|
||||
// callback 1, 10x10, hint=AUTO, responds immediately from bg thread
|
||||
BackgroundTestCallback callback1 = new BackgroundTestCallback();
|
||||
callback1.setScrollBounds(new Rect(5, 5, 15, 15));
|
||||
ScrollCaptureTarget target1 = createTarget(
|
||||
callback1, new Rect(20, 30, 40, 50), new Point(10, 10),
|
||||
View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
targetQueue.add(target1);
|
||||
|
||||
// callback 2, 20x20, hint=AUTO, responds after 5s from bg thread
|
||||
BackgroundTestCallback callback2 = new BackgroundTestCallback();
|
||||
callback2.setScrollBounds(new Rect(0, 0, 20, 20));
|
||||
callback2.setDelay(5000);
|
||||
ScrollCaptureTarget target2 = createTarget(
|
||||
callback2, new Rect(20, 30, 40, 50), new Point(10, 10),
|
||||
View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
targetQueue.add(target2);
|
||||
|
||||
// callback 3, 20x20, hint=INCLUDE, responds after 10s from bg thread
|
||||
BackgroundTestCallback callback3 = new BackgroundTestCallback();
|
||||
callback3.setScrollBounds(new Rect(0, 0, 20, 20));
|
||||
callback3.setDelay(10000);
|
||||
ScrollCaptureTarget target3 = createTarget(
|
||||
callback3, new Rect(20, 30, 40, 50), new Point(10, 10),
|
||||
View.SCROLL_CAPTURE_HINT_INCLUDE);
|
||||
targetQueue.add(target3);
|
||||
|
||||
// callback 1 will be received
|
||||
// callback 2 & 3 will be ignored due to timeout
|
||||
|
||||
ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
|
||||
resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
|
||||
|
||||
resolver.waitForResult();
|
||||
|
||||
ScrollCaptureTarget result = mTargetConsumer.getLastValue();
|
||||
assertSame("Expected target1 as the result, due to timeouts of others", target1, result);
|
||||
assertEquals("result has wrong scroll bounds",
|
||||
new Rect(5, 5, 15, 15), result.getScrollBounds());
|
||||
assertEquals("callback1 should have been called",
|
||||
1, callback1.getOnScrollCaptureSearchCount());
|
||||
assertEquals("callback2 should have been called",
|
||||
1, callback2.getOnScrollCaptureSearchCount());
|
||||
assertEquals("callback3 should have been called",
|
||||
1, callback3.getOnScrollCaptureSearchCount());
|
||||
}
|
||||
|
||||
@Test(timeout = TEST_TIMEOUT_MS)
|
||||
public void testWithCallbackMultipleReplies() throws InterruptedException {
|
||||
// Calls response methods 3 times each
|
||||
RepeatingCaptureCallback callback1 = new RepeatingCaptureCallback(3);
|
||||
callback1.setScrollBounds(new Rect(2, 2, 18, 18));
|
||||
ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50),
|
||||
new Point(10, 10), View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
|
||||
FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
|
||||
callback2.setScrollBounds(new Rect(0, 0, 20, 20));
|
||||
ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50),
|
||||
new Point(10, 10), View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
|
||||
LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
|
||||
targetQueue.add(target1);
|
||||
targetQueue.add(target2);
|
||||
|
||||
ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
|
||||
resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
|
||||
|
||||
resolver.waitForResult();
|
||||
|
||||
ScrollCaptureTarget result = mTargetConsumer.getLastValue();
|
||||
assertSame("Expected target2 as the result, due to hint=INCLUDE", target2, result);
|
||||
assertEquals("result has wrong scroll bounds",
|
||||
new Rect(0, 0, 20, 20), result.getScrollBounds());
|
||||
assertEquals("callback1 should have been called once",
|
||||
1, callback1.getOnScrollCaptureSearchCount());
|
||||
assertEquals("callback2 should have been called once",
|
||||
1, callback2.getOnScrollCaptureSearchCount());
|
||||
}
|
||||
|
||||
private static class TargetConsumer implements Consumer<ScrollCaptureTarget> {
|
||||
volatile ScrollCaptureTarget mResult;
|
||||
int mAcceptCount;
|
||||
|
||||
ScrollCaptureTarget getLastValue() {
|
||||
return mResult;
|
||||
}
|
||||
|
||||
int acceptCount() {
|
||||
return mAcceptCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(@Nullable ScrollCaptureTarget t) {
|
||||
mAcceptCount++;
|
||||
mResult = t;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupTargetView(View view, Rect localVisibleRect, int scrollCaptureHint) {
|
||||
view.setScrollCaptureHint(scrollCaptureHint);
|
||||
view.onVisibilityAggregated(true);
|
||||
// Treat any offset as padding, outset localVisibleRect on all sides and use this as
|
||||
// child bounds
|
||||
Rect bounds = new Rect(localVisibleRect);
|
||||
bounds.inset(-bounds.left, -bounds.top, bounds.left, bounds.top);
|
||||
view.layout(bounds.left, bounds.top, bounds.right, bounds.bottom);
|
||||
view.onVisibilityAggregated(true);
|
||||
}
|
||||
|
||||
private ScrollCaptureTarget createTarget(ScrollCaptureCallback callback, Rect localVisibleRect,
|
||||
Point positionInWindow, int scrollCaptureHint) {
|
||||
View mockView = new View(getTargetContext());
|
||||
return createTargetWithView(mockView, callback, localVisibleRect, positionInWindow,
|
||||
scrollCaptureHint);
|
||||
}
|
||||
|
||||
private ScrollCaptureTarget createTargetWithView(View view, ScrollCaptureCallback callback,
|
||||
Rect localVisibleRect, Point positionInWindow, int scrollCaptureHint) {
|
||||
setupTargetView(view, localVisibleRect, scrollCaptureHint);
|
||||
return new ScrollCaptureTarget(view, localVisibleRect, positionInWindow, callback);
|
||||
}
|
||||
|
||||
|
||||
static class FakeRootView extends ViewGroup implements ViewParent {
|
||||
FakeRootView(Context context, int l, int t, int r, int b) {
|
||||
super(context);
|
||||
layout(l, t, r, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
}
|
||||
}
|
||||
|
||||
static class FakeScrollCaptureCallback implements ScrollCaptureCallback {
|
||||
private Rect mScrollBounds;
|
||||
private long mDelayMillis;
|
||||
private int mOnScrollCaptureSearchCount;
|
||||
|
||||
public int getOnScrollCaptureSearchCount() {
|
||||
return mOnScrollCaptureSearchCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollCaptureSearch(Consumer<Rect> onReady) {
|
||||
mOnScrollCaptureSearchCount++;
|
||||
run(() -> {
|
||||
Rect b = getScrollBounds();
|
||||
onReady.accept(b);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollCaptureStart(ScrollCaptureSession session, Runnable onReady) {
|
||||
run(onReady);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollCaptureImageRequest(ScrollCaptureSession session, Rect captureArea) {
|
||||
run(() -> session.notifyBufferSent(0, captureArea));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollCaptureEnd(Runnable onReady) {
|
||||
run(onReady);
|
||||
}
|
||||
|
||||
public void setScrollBounds(@Nullable Rect scrollBounds) {
|
||||
mScrollBounds = scrollBounds;
|
||||
}
|
||||
|
||||
public void setDelay(long delayMillis) {
|
||||
mDelayMillis = delayMillis;
|
||||
}
|
||||
|
||||
protected Rect getScrollBounds() {
|
||||
return mScrollBounds;
|
||||
}
|
||||
|
||||
protected void run(Runnable r) {
|
||||
delay();
|
||||
r.run();
|
||||
}
|
||||
|
||||
protected void delay() {
|
||||
if (mDelayMillis > 0) {
|
||||
try {
|
||||
Thread.sleep(mDelayMillis);
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class RepeatingCaptureCallback extends FakeScrollCaptureCallback {
|
||||
private int mRepeatCount;
|
||||
|
||||
RepeatingCaptureCallback(int repeatCount) {
|
||||
mRepeatCount = repeatCount;
|
||||
}
|
||||
|
||||
protected void run(Runnable r) {
|
||||
delay();
|
||||
for (int i = 0; i < mRepeatCount; i++) {
|
||||
r.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Response to async calls on an arbitrary background thread */
|
||||
static class BackgroundTestCallback extends FakeScrollCaptureCallback {
|
||||
static int sCount = 0;
|
||||
private void runOnBackgroundThread(Runnable r) {
|
||||
final Runnable target = () -> {
|
||||
delay();
|
||||
r.run();
|
||||
};
|
||||
Thread t = new Thread(target);
|
||||
synchronized (BackgroundTestCallback.this) {
|
||||
sCount++;
|
||||
}
|
||||
t.setName("Background-Thread-" + sCount);
|
||||
t.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run(Runnable r) {
|
||||
runOnBackgroundThread(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
/*
|
||||
* 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 static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.testng.AssertJUnit.assertSame;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
|
||||
import androidx.test.filters.FlakyTest;
|
||||
import androidx.test.filters.MediumTest;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
/**
|
||||
* Exercises Scroll Capture search in {@link ViewGroup}.
|
||||
*/
|
||||
@Presubmit
|
||||
@SmallTest
|
||||
@FlakyTest(detail = "promote once confirmed flake-free")
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ViewGroupScrollCaptureTest {
|
||||
|
||||
@Mock
|
||||
ScrollCaptureCallback mMockCallback;
|
||||
@Mock
|
||||
ScrollCaptureCallback mMockCallback2;
|
||||
|
||||
/** Make sure the hint flags are saved and loaded correctly. */
|
||||
@Test
|
||||
public void testSetScrollCaptureHint() throws Exception {
|
||||
final Context context = getInstrumentation().getContext();
|
||||
final MockViewGroup viewGroup = new MockViewGroup(context);
|
||||
|
||||
assertNotNull(viewGroup);
|
||||
assertEquals("Default scroll capture hint flags should be [SCROLL_CAPTURE_HINT_AUTO]",
|
||||
ViewGroup.SCROLL_CAPTURE_HINT_AUTO, viewGroup.getScrollCaptureHint());
|
||||
|
||||
viewGroup.setScrollCaptureHint(View.SCROLL_CAPTURE_HINT_INCLUDE);
|
||||
assertEquals("The scroll capture hint was not stored correctly.",
|
||||
ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE, viewGroup.getScrollCaptureHint());
|
||||
|
||||
viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE);
|
||||
assertEquals("The scroll capture hint was not stored correctly.",
|
||||
ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE, viewGroup.getScrollCaptureHint());
|
||||
|
||||
viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
|
||||
assertEquals("The scroll capture hint was not stored correctly.",
|
||||
ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS,
|
||||
viewGroup.getScrollCaptureHint());
|
||||
|
||||
viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE
|
||||
| ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
|
||||
assertEquals("The scroll capture hint was not stored correctly.",
|
||||
ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE
|
||||
| ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS,
|
||||
viewGroup.getScrollCaptureHint());
|
||||
|
||||
viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE
|
||||
| ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
|
||||
assertEquals("The scroll capture hint was not stored correctly.",
|
||||
ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE
|
||||
| ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS,
|
||||
viewGroup.getScrollCaptureHint());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a ViewGroup with 'scrollCaptureHint=auto', but no ScrollCaptureCallback set dispatches
|
||||
* correctly. Verifies that the framework helper is called. Verifies a that non-null callback
|
||||
* return results in an expected target in the results.
|
||||
*/
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testDispatchScrollCaptureSearch_noCallback_hintAuto() throws Exception {
|
||||
final Context context = getInstrumentation().getContext();
|
||||
final MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200);
|
||||
|
||||
// When system internal scroll capture is requested, this callback is returned.
|
||||
viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback);
|
||||
|
||||
Rect localVisibleRect = new Rect(0, 0, 200, 200);
|
||||
Point windowOffset = new Point();
|
||||
LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
|
||||
|
||||
// Dispatch
|
||||
viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
|
||||
|
||||
// Verify the system checked for fallback support
|
||||
viewGroup.assertDispatchScrollCaptureCount(1);
|
||||
viewGroup.assertLastDispatchScrollCaptureArgs(localVisibleRect, windowOffset);
|
||||
|
||||
// Verify the target is as expected.
|
||||
assertEquals(1, targetList.size());
|
||||
ScrollCaptureTarget target = targetList.get(0);
|
||||
assertSame("Target has the wrong callback", mMockCallback, target.getCallback());
|
||||
assertSame("Target has the wrong View", viewGroup, target.getContainingView());
|
||||
assertEquals("Target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO,
|
||||
target.getContainingView().getScrollCaptureHint());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a ViewGroup with 'scrollCaptureHint=exclude' is ignored. The Framework helper is
|
||||
* stubbed to return a callback. Verifies that the framework helper is not called (because of
|
||||
* exclude), and no scroll capture target is added to the results.
|
||||
*/
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testDispatchScrollCaptureSearch_noCallback_hintExclude() throws Exception {
|
||||
final Context context = getInstrumentation().getContext();
|
||||
final MockViewGroup viewGroup =
|
||||
new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_EXCLUDE);
|
||||
|
||||
// When system internal scroll capture is requested, this callback is returned.
|
||||
viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback);
|
||||
|
||||
Rect localVisibleRect = new Rect(0, 0, 200, 200);
|
||||
Point windowOffset = new Point();
|
||||
LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
|
||||
|
||||
// Dispatch
|
||||
viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
|
||||
|
||||
// Verify the results.
|
||||
assertEquals("Target list size should be zero.", 0, targetList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a ViewGroup with 'scrollCaptureHint=auto', and a scroll capture callback set
|
||||
* dispatches as expected. Also verifies that the system fallback support is not called, and the
|
||||
* the returned target is constructed correctly.
|
||||
*/
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testDispatchScrollCaptureSearch_withCallback_hintAuto() throws Exception {
|
||||
final Context context = getInstrumentation().getContext();
|
||||
MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200);
|
||||
|
||||
// With an already provided scroll capture callback
|
||||
viewGroup.setScrollCaptureCallback(mMockCallback);
|
||||
|
||||
// When system internal scroll capture is requested, this callback is returned.
|
||||
viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback);
|
||||
|
||||
Rect localVisibleRect = new Rect(0, 0, 200, 200);
|
||||
Point windowOffset = new Point();
|
||||
LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
|
||||
|
||||
// Dispatch to the ViewGroup
|
||||
viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
|
||||
|
||||
// Confirm that framework support was not requested,
|
||||
// because this view already had a callback set.
|
||||
viewGroup.assertCreateScrollCaptureCallbackInternalCount(0);
|
||||
|
||||
// Verify the target is as expected.
|
||||
assertEquals(1, targetList.size());
|
||||
ScrollCaptureTarget target = targetList.get(0);
|
||||
assertSame("Target has the wrong callback", mMockCallback, target.getCallback());
|
||||
assertSame("Target has the wrong View", viewGroup, target.getContainingView());
|
||||
assertEquals("Target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO,
|
||||
target.getContainingView().getScrollCaptureHint());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a ViewGroup with a callback set, but 'scrollCaptureHint=exclude' is ignored. The
|
||||
* exclude flag takes precedence. Verifies that the framework helper is not called (because of
|
||||
* exclude, and a callback being set), and no scroll capture target is added to the results.
|
||||
*/
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testDispatchScrollCaptureSearch_withCallback_hintExclude() throws Exception {
|
||||
final Context context = getInstrumentation().getContext();
|
||||
MockViewGroup viewGroup =
|
||||
new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_EXCLUDE);
|
||||
// With an already provided scroll capture callback
|
||||
viewGroup.setScrollCaptureCallback(mMockCallback);
|
||||
|
||||
Rect localVisibleRect = new Rect(0, 0, 200, 200);
|
||||
Point windowOffset = new Point();
|
||||
LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
|
||||
|
||||
// Dispatch to the ViewGroup itself
|
||||
viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
|
||||
|
||||
// Confirm that framework support was not requested, because this view is excluded.
|
||||
// (And because this view has a callback set.)
|
||||
viewGroup.assertCreateScrollCaptureCallbackInternalCount(0);
|
||||
|
||||
// Has callback, but hint=excluded, so excluded.
|
||||
assertTrue(targetList.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test scroll capture search dispatch to child views.
|
||||
* <p>
|
||||
* Verifies computation of child visible bounds.
|
||||
* TODO: with scrollX / scrollY, split up into discrete tests
|
||||
*/
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testDispatchScrollCaptureSearch_toChildren() throws Exception {
|
||||
final Context context = getInstrumentation().getContext();
|
||||
final MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200);
|
||||
|
||||
Rect localVisibleRect = new Rect(25, 50, 175, 150);
|
||||
Point windowOffset = new Point(0, 0);
|
||||
|
||||
// visible area
|
||||
// |<- l=25, |
|
||||
// | r=175 ->|
|
||||
// +--------------------------+
|
||||
// | view1 (0, 0, 200, 25) |
|
||||
// +---------------+----------+
|
||||
// | | |
|
||||
// | view2 | view4 | --+
|
||||
// | (0, 25, | (inv) | | visible area
|
||||
// | 150, 100)| | |
|
||||
// +---------------+----------+ | t=50, b=150
|
||||
// | view3 | view5 | |
|
||||
// | (0, 100 |(150, 100 | --+
|
||||
// | 200, 200) | 200, 200)|
|
||||
// | | |
|
||||
// | | |
|
||||
// +---------------+----------+ (200,200)
|
||||
|
||||
// View 1 is clipped and not visible.
|
||||
final MockView view1 = new MockView(context, 0, 0, 200, 25);
|
||||
viewGroup.addView(view1);
|
||||
|
||||
// View 2 is partially visible.
|
||||
final MockView view2 = new MockView(context, 0, 25, 150, 100);
|
||||
viewGroup.addView(view2);
|
||||
|
||||
// View 3 is partially visible.
|
||||
// Pretend View3 can scroll by having framework provide fallback support
|
||||
final MockView view3 = new MockView(context, 0, 100, 200, 200);
|
||||
// When system internal scroll capture is requested for this view, return this callback.
|
||||
view3.setScrollCaptureCallbackInternalForTest(mMockCallback);
|
||||
viewGroup.addView(view3);
|
||||
|
||||
// View 4 is invisible and should be ignored.
|
||||
final MockView view4 = new MockView(context, 150, 25, 200, 100, View.INVISIBLE);
|
||||
viewGroup.addView(view4);
|
||||
|
||||
// View 4 is invisible and should be ignored.
|
||||
final MockView view5 = new MockView(context, 150, 100, 200, 200);
|
||||
// When system internal scroll capture is requested for this view, return this callback.
|
||||
view5.setScrollCaptureCallback(mMockCallback2);
|
||||
view5.setScrollCaptureHint(View.SCROLL_CAPTURE_HINT_INCLUDE);
|
||||
viewGroup.addView(view5);
|
||||
|
||||
// Where targets are added
|
||||
final LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
|
||||
|
||||
// Dispatch to the ViewGroup
|
||||
viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
|
||||
|
||||
// View 1 is entirely clipped by the parent and not visible, dispatch
|
||||
// skips this view entirely.
|
||||
view1.assertDispatchScrollCaptureSearchCount(0);
|
||||
view1.assertCreateScrollCaptureCallbackInternalCount(0);
|
||||
|
||||
// View 2, verify the computed localVisibleRect and windowOffset are correctly transformed
|
||||
// to the child coordinate space
|
||||
view2.assertDispatchScrollCaptureSearchCount(1);
|
||||
view2.assertDispatchScrollCaptureSearchLastArgs(
|
||||
new Rect(25, 25, 150, 75), new Point(0, 25));
|
||||
// No callback set, so the framework is asked for support
|
||||
view2.assertCreateScrollCaptureCallbackInternalCount(1);
|
||||
|
||||
// View 3, verify the computed localVisibleRect and windowOffset are correctly transformed
|
||||
// to the child coordinate space
|
||||
view3.assertDispatchScrollCaptureSearchCount(1);
|
||||
view3.assertDispatchScrollCaptureSearchLastArgs(
|
||||
new Rect(25, 0, 175, 50), new Point(0, 100));
|
||||
// No callback set, so the framework is asked for support
|
||||
view3.assertCreateScrollCaptureCallbackInternalCount(1);
|
||||
|
||||
// view4 is invisible, so it should be skipped entirely.
|
||||
view4.assertDispatchScrollCaptureSearchCount(0);
|
||||
view4.assertCreateScrollCaptureCallbackInternalCount(0);
|
||||
|
||||
// view5 is partially visible
|
||||
view5.assertDispatchScrollCaptureSearchCount(1);
|
||||
view5.assertDispatchScrollCaptureSearchLastArgs(
|
||||
new Rect(0, 0, 25, 50), new Point(150, 100));
|
||||
// view5 has a callback set on it, so internal framework support should not be consulted.
|
||||
view5.assertCreateScrollCaptureCallbackInternalCount(0);
|
||||
|
||||
// 2 views should have been returned, view3 & view5
|
||||
assertEquals(2, targetList.size());
|
||||
|
||||
ScrollCaptureTarget target = targetList.get(0);
|
||||
assertSame("First target has the wrong View", view3, target.getContainingView());
|
||||
assertSame("First target has the wrong callback", mMockCallback, target.getCallback());
|
||||
assertEquals("First target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO,
|
||||
target.getContainingView().getScrollCaptureHint());
|
||||
|
||||
target = targetList.get(1);
|
||||
assertSame("Second target has the wrong View", view5, target.getContainingView());
|
||||
assertSame("Second target has the wrong callback", mMockCallback2, target.getCallback());
|
||||
assertEquals("Second target hint is incorrect", View.SCROLL_CAPTURE_HINT_INCLUDE,
|
||||
target.getContainingView().getScrollCaptureHint());
|
||||
}
|
||||
|
||||
public static final class MockView extends View {
|
||||
private ScrollCaptureCallback mInternalCallback;
|
||||
|
||||
private int mDispatchScrollCaptureSearchNumCalls;
|
||||
private Rect mDispatchScrollCaptureSearchLastLocalVisibleRect;
|
||||
private Point mDispatchScrollCaptureSearchLastWindowOffset;
|
||||
private int mCreateScrollCaptureCallbackInternalCount;
|
||||
|
||||
MockView(Context context) {
|
||||
this(context, /* left */ 0, /* top */0, /* right */ 0, /* bottom */0);
|
||||
}
|
||||
|
||||
MockView(Context context, int left, int top, int right, int bottom) {
|
||||
this(context, left, top, right, bottom, View.VISIBLE);
|
||||
}
|
||||
|
||||
MockView(Context context, int left, int top, int right, int bottom, int visibility) {
|
||||
super(context);
|
||||
setVisibility(visibility);
|
||||
setFrame(left, top, right, bottom);
|
||||
}
|
||||
|
||||
public void setScrollCaptureCallbackInternalForTest(ScrollCaptureCallback internal) {
|
||||
mInternalCallback = internal;
|
||||
}
|
||||
|
||||
void assertDispatchScrollCaptureSearchCount(int count) {
|
||||
assertEquals("Unexpected number of calls to dispatchScrollCaptureSearch",
|
||||
count, mDispatchScrollCaptureSearchNumCalls);
|
||||
}
|
||||
|
||||
void assertDispatchScrollCaptureSearchLastArgs(Rect localVisibleRect, Point windowOffset) {
|
||||
assertEquals("arg localVisibleRect was incorrect.",
|
||||
localVisibleRect, mDispatchScrollCaptureSearchLastLocalVisibleRect);
|
||||
assertEquals("arg windowOffset was incorrect.",
|
||||
windowOffset, mDispatchScrollCaptureSearchLastWindowOffset);
|
||||
}
|
||||
|
||||
void assertCreateScrollCaptureCallbackInternalCount(int count) {
|
||||
assertEquals("Unexpected number of calls to createScrollCaptureCallackInternal",
|
||||
count, mCreateScrollCaptureCallbackInternalCount);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
mDispatchScrollCaptureSearchNumCalls = 0;
|
||||
mDispatchScrollCaptureSearchLastWindowOffset = null;
|
||||
mDispatchScrollCaptureSearchLastLocalVisibleRect = null;
|
||||
mCreateScrollCaptureCallbackInternalCount = 0;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchScrollCaptureSearch(Rect localVisibleRect, Point windowOffset,
|
||||
Queue<ScrollCaptureTarget> targets) {
|
||||
mDispatchScrollCaptureSearchNumCalls++;
|
||||
mDispatchScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect);
|
||||
mDispatchScrollCaptureSearchLastWindowOffset = new Point(windowOffset);
|
||||
super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ScrollCaptureCallback createScrollCaptureCallbackInternal(Rect localVisibleRect,
|
||||
Point offsetInWindow) {
|
||||
mCreateScrollCaptureCallbackInternalCount++;
|
||||
return mInternalCallback;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MockViewGroup extends ViewGroup {
|
||||
private ScrollCaptureCallback mInternalCallback;
|
||||
private int mDispatchScrollCaptureSearchNumCalls;
|
||||
private Rect mDispatchScrollCaptureSearchLastLocalVisibleRect;
|
||||
private Point mDispatchScrollCaptureSearchLastWindowOffset;
|
||||
private int mCreateScrollCaptureCallbackInternalCount;
|
||||
|
||||
|
||||
MockViewGroup(Context context) {
|
||||
this(context, /* left */ 0, /* top */0, /* right */ 0, /* bottom */0);
|
||||
}
|
||||
|
||||
MockViewGroup(Context context, int left, int top, int right, int bottom) {
|
||||
this(context, left, top, right, bottom, View.SCROLL_CAPTURE_HINT_AUTO);
|
||||
}
|
||||
|
||||
MockViewGroup(Context context, int left, int top, int right, int bottom,
|
||||
int scrollCaptureHint) {
|
||||
super(context);
|
||||
setScrollCaptureHint(scrollCaptureHint);
|
||||
setFrame(left, top, right, bottom);
|
||||
}
|
||||
|
||||
public void setScrollCaptureCallbackInternalForTest(ScrollCaptureCallback internal) {
|
||||
mInternalCallback = internal;
|
||||
}
|
||||
|
||||
void assertDispatchScrollCaptureSearchCount(int count) {
|
||||
assertEquals("Unexpected number of calls to dispatchScrollCaptureSearch",
|
||||
count, mDispatchScrollCaptureSearchNumCalls);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ScrollCaptureCallback createScrollCaptureCallbackInternal(Rect localVisibleRect,
|
||||
Point offsetInWindow) {
|
||||
mCreateScrollCaptureCallbackInternalCount++;
|
||||
return mInternalCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
// We don't layout this view.
|
||||
}
|
||||
|
||||
void assertDispatchScrollCaptureCount(int count) {
|
||||
assertEquals(count, mDispatchScrollCaptureSearchNumCalls);
|
||||
}
|
||||
|
||||
void assertLastDispatchScrollCaptureArgs(Rect localVisibleRect, Point windowOffset) {
|
||||
assertEquals("arg localVisibleRect to dispatchScrollCaptureCallback was incorrect.",
|
||||
localVisibleRect, mDispatchScrollCaptureSearchLastLocalVisibleRect);
|
||||
assertEquals("arg windowOffset to dispatchScrollCaptureCallback was incorrect.",
|
||||
windowOffset, mDispatchScrollCaptureSearchLastWindowOffset);
|
||||
}
|
||||
void assertCreateScrollCaptureCallbackInternalCount(int count) {
|
||||
assertEquals("Unexpected number of calls to createScrollCaptureCallackInternal",
|
||||
count, mCreateScrollCaptureCallbackInternalCount);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
mDispatchScrollCaptureSearchNumCalls = 0;
|
||||
mDispatchScrollCaptureSearchLastWindowOffset = null;
|
||||
mDispatchScrollCaptureSearchLastLocalVisibleRect = null;
|
||||
mCreateScrollCaptureCallbackInternalCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchScrollCaptureSearch(Rect localVisibleRect, Point windowOffset,
|
||||
Queue<ScrollCaptureTarget> targets) {
|
||||
mDispatchScrollCaptureSearchNumCalls++;
|
||||
mDispatchScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect);
|
||||
mDispatchScrollCaptureSearchLastWindowOffset = new Point(windowOffset);
|
||||
super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* 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 com.android.internal.view;
|
||||
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getContext;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.annotation.UiThreadTest;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class ScrollViewCaptureHelperTest {
|
||||
|
||||
private FrameLayout mParent;
|
||||
private ScrollView mTarget;
|
||||
private LinearLayout mContent;
|
||||
private WindowManager mWm;
|
||||
|
||||
private WindowManager.LayoutParams mWindowLayoutParams;
|
||||
|
||||
private static final int CHILD_VIEWS = 12;
|
||||
public static final int CHILD_VIEW_HEIGHT = 300;
|
||||
|
||||
private static final int WINDOW_WIDTH = 800;
|
||||
private static final int WINDOW_HEIGHT = 1200;
|
||||
|
||||
private static final int CAPTURE_HEIGHT = 600;
|
||||
|
||||
private Random mRandom;
|
||||
|
||||
private static float sDensity;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUpClass() {
|
||||
sDensity = getContext().getResources().getDisplayMetrics().density;
|
||||
}
|
||||
|
||||
@Before
|
||||
@UiThreadTest
|
||||
public void setUp() {
|
||||
mRandom = new Random();
|
||||
mParent = new FrameLayout(getContext());
|
||||
|
||||
mTarget = new ScrollView(getContext());
|
||||
mParent.addView(mTarget, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
|
||||
|
||||
mContent = new LinearLayout(getContext());
|
||||
mContent.setOrientation(LinearLayout.VERTICAL);
|
||||
mTarget.addView(mContent, new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
|
||||
|
||||
for (int i = 0; i < CHILD_VIEWS; i++) {
|
||||
TextView view = new TextView(getContext());
|
||||
view.setText("Child #" + i);
|
||||
view.setTextColor(Color.WHITE);
|
||||
view.setTextSize(30f);
|
||||
view.setBackgroundColor(Color.rgb(mRandom.nextFloat(), mRandom.nextFloat(),
|
||||
mRandom.nextFloat()));
|
||||
mContent.addView(view, new ViewGroup.LayoutParams(MATCH_PARENT, CHILD_VIEW_HEIGHT));
|
||||
}
|
||||
|
||||
// Window -> Parent -> Target -> Content
|
||||
|
||||
mWm = getContext().getSystemService(WindowManager.class);
|
||||
|
||||
// Setup the window that we are going to use
|
||||
mWindowLayoutParams = new WindowManager.LayoutParams(WINDOW_WIDTH, WINDOW_HEIGHT,
|
||||
TYPE_APPLICATION_OVERLAY, FLAG_NOT_TOUCHABLE, PixelFormat.OPAQUE);
|
||||
mWindowLayoutParams.setTitle("ScrollViewCaptureHelper");
|
||||
mWindowLayoutParams.gravity = Gravity.CENTER;
|
||||
mWm.addView(mParent, mWindowLayoutParams);
|
||||
}
|
||||
|
||||
@After
|
||||
@UiThreadTest
|
||||
public void tearDown() {
|
||||
mWm.removeViewImmediate(mParent);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onPrepareForStart() {
|
||||
ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
|
||||
Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
|
||||
svc.onPrepareForStart(mTarget, scrollBounds);
|
||||
}
|
||||
|
||||
static void assertEmpty(Rect r) {
|
||||
if (r != null && !r.isEmpty()) {
|
||||
fail("Not true that " + r + " is empty");
|
||||
}
|
||||
}
|
||||
|
||||
static void assertContains(Rect parent, Rect child) {
|
||||
if (!parent.contains(child)) {
|
||||
fail("Not true that " + parent + " contains " + child);
|
||||
}
|
||||
}
|
||||
|
||||
static void assertRectEquals(Rect parent, Rect child) {
|
||||
if (!parent.equals(child)) {
|
||||
fail("Not true that " + parent + " is equal to " + child);
|
||||
}
|
||||
}
|
||||
|
||||
static Rect getVisibleRect(View v) {
|
||||
Rect r = new Rect(0, 0, v.getWidth(), v.getHeight());
|
||||
v.getLocalVisibleRect(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
static int assertScrollToY(View v, int scrollY) {
|
||||
v.scrollTo(0, scrollY);
|
||||
int dest = v.getScrollY();
|
||||
assertEquals(scrollY, dest);
|
||||
return scrollY;
|
||||
}
|
||||
|
||||
|
||||
static void assertCapturedAreaCompletelyVisible(int startScrollY, Rect requestRect,
|
||||
Rect localVisibleNow) {
|
||||
Rect captured = new Rect(localVisibleNow);
|
||||
captured.offset(0, -startScrollY); // make relative
|
||||
|
||||
if (!captured.contains(requestRect)) {
|
||||
fail("Not true that all of " + requestRect + " is contained by " + captured);
|
||||
}
|
||||
}
|
||||
static void assertCapturedAreaPartiallyVisible(int startScrollY, Rect requestRect,
|
||||
Rect localVisibleNow) {
|
||||
Rect captured = new Rect(localVisibleNow);
|
||||
captured.offset(0, -startScrollY); // make relative
|
||||
|
||||
if (!Rect.intersects(captured, requestRect)) {
|
||||
fail("Not true that any of " + requestRect + " intersects " + captured);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_up_fromTop() {
|
||||
final int startScrollY = assertScrollToY(mTarget, 0);
|
||||
|
||||
ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
|
||||
Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
|
||||
svc.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
assertTrue(scrollBounds.height() > CAPTURE_HEIGHT);
|
||||
|
||||
Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
|
||||
|
||||
Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
|
||||
|
||||
// The result is an empty rectangle and no scrolling, since it
|
||||
// is not possible to physically scroll further up to make the
|
||||
// requested area visible at all (it doesn't exist).
|
||||
assertEmpty(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_down_fromTop() {
|
||||
final int startScrollY = assertScrollToY(mTarget, 0);
|
||||
|
||||
|
||||
ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
|
||||
Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
|
||||
svc.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
assertTrue(scrollBounds.height() > CAPTURE_HEIGHT);
|
||||
|
||||
// Capture between y = +1200 to +1500 pixels BELOW current top
|
||||
Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
|
||||
WINDOW_HEIGHT + CAPTURE_HEIGHT);
|
||||
|
||||
Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
|
||||
assertRectEquals(request, result);
|
||||
|
||||
assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_up_fromMiddle() {
|
||||
final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT);
|
||||
|
||||
ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
|
||||
Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
|
||||
svc.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
|
||||
|
||||
|
||||
Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
|
||||
|
||||
assertRectEquals(request, result);
|
||||
|
||||
assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_down_fromMiddle() {
|
||||
final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT);
|
||||
|
||||
ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
|
||||
Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
|
||||
svc.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
|
||||
WINDOW_HEIGHT + CAPTURE_HEIGHT);
|
||||
|
||||
Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
|
||||
assertRectEquals(request, result);
|
||||
|
||||
assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_up_fromBottom() {
|
||||
final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2);
|
||||
|
||||
ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
|
||||
Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
|
||||
svc.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
|
||||
|
||||
Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
|
||||
assertRectEquals(request, result);
|
||||
|
||||
assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_down_fromBottom() {
|
||||
final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2);
|
||||
|
||||
ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
|
||||
Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
|
||||
svc.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
|
||||
WINDOW_HEIGHT + CAPTURE_HEIGHT);
|
||||
|
||||
Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
|
||||
|
||||
// The result is an empty rectangle and no scrolling, since it
|
||||
// is not possible to physically scroll further down to make the
|
||||
// requested area visible at all (it doesn't exist).
|
||||
assertEmpty(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_offTopEdge() {
|
||||
final int startScrollY = assertScrollToY(mTarget, 0);
|
||||
|
||||
ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
|
||||
Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
|
||||
svc.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
// Create a request which lands halfway off the top of the content
|
||||
//from -1500 to -900, (starting at 1200 = -300 to +300 within the content)
|
||||
int top = 0;
|
||||
Rect request = new Rect(
|
||||
0, top - (CAPTURE_HEIGHT / 2),
|
||||
scrollBounds.width(), top + (CAPTURE_HEIGHT / 2));
|
||||
|
||||
Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
|
||||
// The result is a partial result
|
||||
Rect expectedResult = new Rect(request);
|
||||
expectedResult.top += 300; // top half clipped
|
||||
assertRectEquals(expectedResult, result);
|
||||
assertCapturedAreaPartiallyVisible(startScrollY, request, getVisibleRect(mContent));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_offBottomEdge() {
|
||||
final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2); // 2400
|
||||
|
||||
ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
|
||||
Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
|
||||
svc.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
// Create a request which lands halfway off the bottom of the content
|
||||
//from 600 to to 1200, (starting at 2400 = 3000 to 3600 within the content)
|
||||
|
||||
int bottom = WINDOW_HEIGHT;
|
||||
Rect request = new Rect(
|
||||
0, bottom - (CAPTURE_HEIGHT / 2),
|
||||
scrollBounds.width(), bottom + (CAPTURE_HEIGHT / 2));
|
||||
|
||||
Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
|
||||
|
||||
Rect expectedResult = new Rect(request);
|
||||
expectedResult.bottom -= 300; // bottom half clipped
|
||||
assertRectEquals(expectedResult, result);
|
||||
assertCapturedAreaPartiallyVisible(startScrollY, request, getVisibleRect(mContent));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onPrepareForEnd() {
|
||||
ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
|
||||
svc.onPrepareForEnd(mTarget);
|
||||
}
|
||||
}
|
||||
@@ -283,6 +283,12 @@
|
||||
"group": "WM_DEBUG_APP_TRANSITIONS",
|
||||
"at": "com\/android\/server\/wm\/ActivityRecord.java"
|
||||
},
|
||||
"-1517908912": {
|
||||
"message": "requestScrollCapture: caught exception dispatching to window.token=%s",
|
||||
"level": "WARN",
|
||||
"group": "WM_ERROR",
|
||||
"at": "com\/android\/server\/wm\/WindowManagerService.java"
|
||||
},
|
||||
"-1515151503": {
|
||||
"message": ">>> OPEN TRANSACTION removeReplacedWindows",
|
||||
"level": "INFO",
|
||||
@@ -1441,6 +1447,12 @@
|
||||
"group": "WM_DEBUG_RECENTS_ANIMATIONS",
|
||||
"at": "com\/android\/server\/wm\/RecentsAnimation.java"
|
||||
},
|
||||
"646981048": {
|
||||
"message": "Invalid displayId for requestScrollCapture: %d",
|
||||
"level": "ERROR",
|
||||
"group": "WM_ERROR",
|
||||
"at": "com\/android\/server\/wm\/WindowManagerService.java"
|
||||
},
|
||||
"662572728": {
|
||||
"message": "Attempted to add a toast window with bad token %s. Aborting.",
|
||||
"level": "WARN",
|
||||
@@ -1597,6 +1609,12 @@
|
||||
"group": "WM_ERROR",
|
||||
"at": "com\/android\/server\/wm\/WindowManagerService.java"
|
||||
},
|
||||
"1046922686": {
|
||||
"message": "requestScrollCapture: caught exception dispatching callback: %s",
|
||||
"level": "WARN",
|
||||
"group": "WM_ERROR",
|
||||
"at": "com\/android\/server\/wm\/WindowManagerService.java"
|
||||
},
|
||||
"1051545910": {
|
||||
"message": "Exit animation finished in %s: remove=%b",
|
||||
"level": "VERBOSE",
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 com.android.systemui.screenshot;
|
||||
|
||||
import android.os.IBinder;
|
||||
import android.view.IWindowManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Stub
|
||||
*/
|
||||
public class ScrollCaptureController {
|
||||
|
||||
public static final int STATUS_A = 0;
|
||||
public static final int STATUS_B = 1;
|
||||
|
||||
private final IWindowManager mWindowManagerService;
|
||||
private StatusListener mListener;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param windowManagerService
|
||||
*/
|
||||
@Inject
|
||||
public ScrollCaptureController(IWindowManager windowManagerService) {
|
||||
mWindowManagerService = windowManagerService;
|
||||
}
|
||||
|
||||
interface StatusListener {
|
||||
void onScrollCaptureStatus(boolean available);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param window
|
||||
* @param listener
|
||||
*/
|
||||
public void getStatus(IBinder window, StatusListener listener) {
|
||||
mListener = listener;
|
||||
// try {
|
||||
// mWindowManagerService.requestScrollCapture(window, new ClientCallbacks());
|
||||
// } catch (RemoteException e) {
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import android.util.SparseArray;
|
||||
import android.view.Display;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.DragEvent;
|
||||
import android.view.IScrollCaptureController;
|
||||
import android.view.IWindow;
|
||||
import android.view.IWindowManager;
|
||||
import android.view.IWindowSession;
|
||||
@@ -352,5 +353,14 @@ public class SystemWindows {
|
||||
|
||||
@Override
|
||||
public void dispatchPointerCaptureChanged(boolean hasCapture) {}
|
||||
|
||||
@Override
|
||||
public void requestScrollCapture(IScrollCaptureController controller) {
|
||||
try {
|
||||
controller.onClientUnavailable();
|
||||
} catch (RemoteException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5450,6 +5450,46 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
|
||||
return mWmService.mDisplayManagerInternal.getDisplayPosition(getDisplayId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the appropriate target window for scroll capture. The search progresses top to
|
||||
* bottom.
|
||||
* If {@code searchBehind} is non-null, the search will only consider windows behind this one.
|
||||
* If a valid taskId is specified, the target window must belong to the given task.
|
||||
*
|
||||
* @param searchBehind a window used to filter the search to windows behind it, or null to begin
|
||||
* the search at the top window of the display
|
||||
* @param taskId specifies the id of a task the result must belong to or
|
||||
* {@link android.app.ActivityTaskManager#INVALID_TASK_ID INVALID_TASK_ID}
|
||||
* to match any window
|
||||
* @return the located window or null if none could be found matching criteria
|
||||
*/
|
||||
@Nullable
|
||||
WindowState findScrollCaptureTargetWindow(@Nullable WindowState searchBehind, int taskId) {
|
||||
return getWindow(new Predicate<WindowState>() {
|
||||
boolean behindTopWindow = (searchBehind == null); // optional filter
|
||||
@Override
|
||||
public boolean test(WindowState nextWindow) {
|
||||
// Skip through all windows until we pass topWindow (if specified)
|
||||
if (!behindTopWindow) {
|
||||
if (nextWindow == searchBehind) {
|
||||
behindTopWindow = true;
|
||||
}
|
||||
return false; /* continue */
|
||||
}
|
||||
if (taskId != INVALID_TASK_ID) {
|
||||
Task task = nextWindow.getTask();
|
||||
if (task == null || !task.isTaskId(taskId)) {
|
||||
return false; /* continue */
|
||||
}
|
||||
}
|
||||
if (!nextWindow.canReceiveKeys()) {
|
||||
return false; /* continue */
|
||||
}
|
||||
return true; /* stop */
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class RemoteInsetsControlTarget implements InsetsControlTarget {
|
||||
private final IDisplayWindowInsetsController mRemoteInsetsController;
|
||||
|
||||
|
||||
@@ -219,6 +219,7 @@ import android.view.IOnKeyguardExitResult;
|
||||
import android.view.IPinnedStackListener;
|
||||
import android.view.IRecentsAnimationRunner;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.IScrollCaptureController;
|
||||
import android.view.ISystemGestureExclusionListener;
|
||||
import android.view.IWallpaperVisibilityListener;
|
||||
import android.view.IWindow;
|
||||
@@ -6837,6 +6838,58 @@ public class WindowManagerService extends IWindowManager.Stub
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards a scroll capture request to the appropriate window, if available.
|
||||
*
|
||||
* @param displayId the display for the request
|
||||
* @param behindClient token for a window, used to filter the search to windows behind it
|
||||
* @param taskId specifies the id of a task the result must belong to or -1 to ignore task ids
|
||||
* @param controller the controller to receive results; a call to either
|
||||
* {@link IScrollCaptureController#onClientConnected} or
|
||||
* {@link IScrollCaptureController#onClientUnavailable}.
|
||||
*/
|
||||
public void requestScrollCapture(int displayId, @Nullable IBinder behindClient, int taskId,
|
||||
IScrollCaptureController controller) {
|
||||
if (!checkCallingPermission(READ_FRAME_BUFFER, "requestScrollCapture()")) {
|
||||
throw new SecurityException("Requires READ_FRAME_BUFFER permission");
|
||||
}
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
synchronized (mGlobalLock) {
|
||||
DisplayContent dc = mRoot.getDisplayContent(displayId);
|
||||
if (dc == null) {
|
||||
ProtoLog.e(WM_ERROR,
|
||||
"Invalid displayId for requestScrollCapture: %d", displayId);
|
||||
controller.onClientUnavailable();
|
||||
return;
|
||||
}
|
||||
WindowState topWindow = null;
|
||||
if (behindClient != null) {
|
||||
topWindow = windowForClientLocked(null, behindClient, /* throwOnError*/ true);
|
||||
}
|
||||
WindowState targetWindow = dc.findScrollCaptureTargetWindow(topWindow, taskId);
|
||||
if (targetWindow == null) {
|
||||
controller.onClientUnavailable();
|
||||
return;
|
||||
}
|
||||
// Forward to the window for handling.
|
||||
try {
|
||||
targetWindow.mClient.requestScrollCapture(controller);
|
||||
} catch (RemoteException e) {
|
||||
ProtoLog.w(WM_ERROR,
|
||||
"requestScrollCapture: caught exception dispatching to window."
|
||||
+ "token=%s", targetWindow.mClient.asBinder());
|
||||
controller.onClientUnavailable();
|
||||
}
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
ProtoLog.w(WM_ERROR,
|
||||
"requestScrollCapture: caught exception dispatching callback: %s", e);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dontOverrideDisplayInfo(int displayId) {
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
|
||||
@@ -41,6 +41,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
|
||||
@@ -74,6 +75,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ActivityTaskManager;
|
||||
import android.app.WindowConfiguration;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
@@ -1207,6 +1209,31 @@ public class DisplayContentTests extends WindowTestsBase {
|
||||
assertNull(taskDisplayArea.getOrCreateRootHomeTask());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindScrollCaptureTargetWindow_behindWindow() {
|
||||
DisplayContent display = createNewDisplay();
|
||||
ActivityStack stack = createTaskStackOnDisplay(display);
|
||||
Task task = createTaskInStack(stack, 0 /* userId */);
|
||||
WindowState activityWindow = createAppWindow(task, TYPE_APPLICATION, "App Window");
|
||||
WindowState behindWindow = createWindow(null, TYPE_SCREENSHOT, display, "Screenshot");
|
||||
|
||||
WindowState result = display.findScrollCaptureTargetWindow(behindWindow,
|
||||
ActivityTaskManager.INVALID_TASK_ID);
|
||||
assertEquals(activityWindow, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindScrollCaptureTargetWindow_taskId() {
|
||||
DisplayContent display = createNewDisplay();
|
||||
ActivityStack stack = createTaskStackOnDisplay(display);
|
||||
Task task = createTaskInStack(stack, 0 /* userId */);
|
||||
WindowState window = createAppWindow(task, TYPE_APPLICATION, "App Window");
|
||||
WindowState behindWindow = createWindow(null, TYPE_SCREENSHOT, display, "Screenshot");
|
||||
|
||||
WindowState result = display.findScrollCaptureTargetWindow(null, task.mTaskId);
|
||||
assertEquals(window, result);
|
||||
}
|
||||
|
||||
private boolean isOptionsPanelAtRight(int displayId) {
|
||||
return (mWm.getPreferredOptionsPanelGravity(displayId) & Gravity.RIGHT) == Gravity.RIGHT;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.os.RemoteException;
|
||||
import android.util.MergedConfiguration;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.DragEvent;
|
||||
import android.view.IScrollCaptureController;
|
||||
import android.view.IWindow;
|
||||
import android.view.InsetsSourceControl;
|
||||
import android.view.InsetsState;
|
||||
@@ -112,6 +113,10 @@ public class TestIWindow extends IWindow.Stub {
|
||||
public void dispatchPointerCaptureChanged(boolean hasCapture) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestScrollCapture(IScrollCaptureController controller) throws RemoteException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showInsets(int types, boolean fromIme) throws RemoteException {
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ class WindowTestsBase extends SystemServiceTestsBase {
|
||||
WindowState createAppWindow(Task task, int type, String name) {
|
||||
synchronized (mWm.mGlobalLock) {
|
||||
final ActivityRecord activity =
|
||||
WindowTestUtils.createTestActivityRecord(mDisplayContent);
|
||||
WindowTestUtils.createTestActivityRecord(task.getDisplayContent());
|
||||
task.addChild(activity, 0);
|
||||
return createWindow(null, type, activity, name);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user