Merge "Scroll Capture Framework" into rvc-dev am: 03f9a814a5

Change-Id: Ie371aafcdce9db0a2709eb495539a9e5d29260f1
This commit is contained in:
Mark Renouf
2020-04-21 10:53:05 +00:00
committed by Automerger Merge Worker
34 changed files with 4231 additions and 13 deletions

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

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

View File

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

View File

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

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

View 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;
}
}
}

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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