Merge "Long screenshot fixes for apps that return less than the requested rect" into sc-dev am: 18650530b3

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15002380

Change-Id: I253a8f117db447f0195ef8e2e30952e8fe606038
This commit is contained in:
Matt Casey
2021-06-23 17:15:19 +00:00
committed by Automerger Merge Worker
6 changed files with 102 additions and 113 deletions

View File

@@ -20,6 +20,7 @@ import android.graphics.Bitmap;
import android.graphics.HardwareRenderer;
import android.graphics.RecordingCanvas;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.RenderNode;
import android.graphics.drawable.Drawable;
import android.os.Handler;
@@ -46,7 +47,6 @@ class ImageTileSet {
private static final String TAG = "ImageTileSet";
private CallbackRegistry<OnBoundsChangedListener, ImageTileSet, Rect> mOnBoundsListeners;
private CallbackRegistry<OnContentChangedListener, ImageTileSet, Rect> mContentListeners;
@Inject
@@ -54,14 +54,6 @@ class ImageTileSet {
mHandler = handler;
}
interface OnBoundsChangedListener {
/**
* Reports an update to the bounding box that contains all active tiles. These are virtual
* (capture) coordinates which can be either negative or positive.
*/
void onBoundsChanged(int left, int top, int right, int bottom);
}
interface OnContentChangedListener {
/**
* Mark as dirty and rebuild display list.
@@ -70,25 +62,9 @@ class ImageTileSet {
}
private final List<ImageTile> mTiles = new ArrayList<>();
private final Rect mBounds = new Rect();
private final Region mRegion = new Region();
private final Handler mHandler;
void addOnBoundsChangedListener(OnBoundsChangedListener listener) {
if (mOnBoundsListeners == null) {
mOnBoundsListeners = new CallbackRegistry<>(
new NotifierCallback<OnBoundsChangedListener, ImageTileSet, Rect>() {
@Override
public void onNotifyCallback(OnBoundsChangedListener callback,
ImageTileSet sender,
int arg, Rect newBounds) {
callback.onBoundsChanged(newBounds.left, newBounds.top, newBounds.right,
newBounds.bottom);
}
});
}
mOnBoundsListeners.add(listener);
}
void addOnContentChangedListener(OnContentChangedListener listener) {
if (mContentListeners == null) {
mContentListeners = new CallbackRegistry<>(
@@ -110,14 +86,8 @@ class ImageTileSet {
mHandler.post(() -> addTile(tile));
return;
}
final Rect newBounds = new Rect(mBounds);
final Rect newRect = tile.getLocation();
mTiles.add(tile);
newBounds.union(newRect);
if (!newBounds.equals(mBounds)) {
mBounds.set(newBounds);
notifyBoundsChanged(mBounds);
}
mRegion.op(tile.getLocation(), mRegion, Region.Op.UNION);
notifyContentChanged();
}
@@ -127,12 +97,6 @@ class ImageTileSet {
}
}
private void notifyBoundsChanged(Rect bounds) {
if (mOnBoundsListeners != null) {
mOnBoundsListeners.notifyCallbacks(this, 0, bounds);
}
}
/**
* Returns a drawable to paint the combined contents of the tiles. Drawable dimensions are
* zero-based and map directly to {@link #getLeft()}, {@link #getTop()}, {@link #getRight()},
@@ -153,6 +117,15 @@ class ImageTileSet {
return mTiles.size();
}
/**
* @return the bounding rect around any gaps in the tiles.
*/
Rect getGaps() {
Region difference = new Region();
difference.op(mRegion.getBounds(), mRegion, Region.Op.DIFFERENCE);
return difference.getBounds();
}
ImageTile get(int i) {
return mTiles.get(i);
}
@@ -182,41 +155,40 @@ class ImageTileSet {
}
int getLeft() {
return mBounds.left;
return mRegion.getBounds().left;
}
int getTop() {
return mBounds.top;
return mRegion.getBounds().top;
}
int getRight() {
return mBounds.right;
return mRegion.getBounds().right;
}
int getBottom() {
return mBounds.bottom;
return mRegion.getBounds().bottom;
}
int getWidth() {
return mBounds.width();
return mRegion.getBounds().width();
}
int getHeight() {
return mBounds.height();
return mRegion.getBounds().height();
}
void clear() {
if (mTiles.isEmpty()) {
return;
}
mBounds.setEmpty();
mRegion.setEmpty();
Iterator<ImageTile> i = mTiles.iterator();
while (i.hasNext()) {
ImageTile next = i.next();
next.close();
i.remove();
}
notifyBoundsChanged(mBounds);
notifyContentChanged();
}
}

View File

@@ -56,6 +56,7 @@ import javax.inject.Inject;
public class ScrollCaptureClient {
private static final int TILE_SIZE_PX_MAX = 4 * (1024 * 1024);
private static final int TILES_PER_PAGE = 2; // increase once b/174571735 is addressed
private static final int MAX_TILES = 30;
@VisibleForTesting
static final int MATCH_ANY_TASK = ActivityTaskManager.INVALID_TASK_ID;
@@ -83,11 +84,12 @@ public class ScrollCaptureClient {
int getMaxTiles();
/**
* @return the maximum combined capture height for this session, in pixels.
* Target pixel height for acquisition this session. Session may yield more or less data
* than this, but acquiring this height is considered sufficient for completion.
*
* @return target height in pixels.
*/
default int getMaxHeight() {
return getMaxTiles() * getTileHeight();
}
int getTargetHeight();
/**
* @return the height of each image tile
@@ -234,11 +236,11 @@ public class ScrollCaptureClient {
private final int mTileWidth;
private Rect mRequestRect;
private boolean mStarted;
private final int mTargetHeight;
private ICancellationSignal mCancellationSignal;
private final Rect mWindowBounds;
private final Rect mBoundsInWindow;
private final int mMaxTiles;
private Completer<Session> mStartCompleter;
private Completer<CaptureResult> mTileRequestCompleter;
@@ -256,7 +258,7 @@ public class ScrollCaptureClient {
mTileWidth = mBoundsInWindow.width();
mTileHeight = pxPerTile / mBoundsInWindow.width();
mMaxTiles = (int) Math.ceil(maxPages * TILES_PER_PAGE);
mTargetHeight = (int) (mBoundsInWindow.height() * maxPages);
if (DEBUG_SCROLL) {
Log.d(TAG, "boundsInWindow: " + mBoundsInWindow);
@@ -285,7 +287,7 @@ public class ScrollCaptureClient {
private void start(Completer<Session> completer) {
mReader = ImageReader.newInstance(mTileWidth, mTileHeight, PixelFormat.RGBA_8888,
mMaxTiles, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
MAX_TILES, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
mStartCompleter = completer;
try {
mCancellationSignal = mConnection.startCapture(mReader.getSurface(), this);
@@ -409,9 +411,14 @@ public class ScrollCaptureClient {
return new Rect(mBoundsInWindow);
}
@Override
public int getTargetHeight() {
return mTargetHeight;
}
@Override
public int getMaxTiles() {
return mMaxTiles;
return MAX_TILES;
}
}
}

View File

@@ -199,22 +199,20 @@ public class ScrollCaptureController {
Log.d(TAG, "onCaptureResult: " + result + " scrolling " + (mScrollingUp ? "UP" : "DOWN")
+ " finish on boundary: " + mFinishOnBoundary);
boolean emptyResult = result.captured.height() == 0;
boolean partialResult = !emptyResult
&& result.captured.height() < result.requested.height();
boolean finish = false;
if (emptyResult) {
// Potentially reached a vertical boundary. Extend in the other direction.
if (mFinishOnBoundary) {
Log.d(TAG, "Partial/empty: finished!");
finish = true;
Log.d(TAG, "Empty: finished!");
finishCapture();
return;
} else {
// We hit a boundary, clear the tiles, capture everything in the opposite direction,
// then finish.
mImageTileSet.clear();
mFinishOnBoundary = true;
mScrollingUp = !mScrollingUp;
Log.d(TAG, "Partial/empty: cleared, switch direction to finish");
Log.d(TAG, "Empty: cleared, switch direction to finish");
}
} else {
// Got a non-empty result, but may already have enough bitmap data now
@@ -223,12 +221,14 @@ public class ScrollCaptureController {
Log.d(TAG, "Hit max tiles: finished");
// If we ever hit the max tiles, we've got enough bitmap data to finish
// (even if we weren't sure we'd finish on this pass).
finish = true;
finishCapture();
return;
} else {
if (mScrollingUp && !mFinishOnBoundary) {
// During the initial scroll up, we only want to acquire the portion described
// by IDEAL_PORTION_ABOVE.
if (expectedTiles >= mSession.getMaxTiles() * IDEAL_PORTION_ABOVE) {
if (mImageTileSet.getHeight() + result.captured.height()
>= mSession.getTargetHeight() * IDEAL_PORTION_ABOVE) {
Log.d(TAG, "Hit ideal portion above: clear and switch direction");
// We got enough above the start point, now see how far down it can go.
mImageTileSet.clear();
@@ -246,15 +246,15 @@ public class ScrollCaptureController {
+ " - " + mImageTileSet.getRight() + "," + mImageTileSet.getBottom()
+ " (" + mImageTileSet.getWidth() + "x" + mImageTileSet.getHeight() + ")");
// Stop when "too tall"
if (mImageTileSet.getHeight() > MAX_HEIGHT) {
Log.d(TAG, "Max height reached.");
finish = true;
Rect gapBounds = mImageTileSet.getGaps();
if (!gapBounds.isEmpty()) {
Log.d(TAG, "Found gaps in tileset: " + gapBounds + ", requesting " + gapBounds.top);
requestNextTile(gapBounds.top);
return;
}
if (finish) {
Log.d(TAG, "Stop.");
if (mImageTileSet.getHeight() >= mSession.getTargetHeight()) {
Log.d(TAG, "Target height reached.");
finishCapture();
return;
}
@@ -268,8 +268,8 @@ public class ScrollCaptureController {
: result.requested.bottom;
} else {
nextTop = (mScrollingUp)
? result.captured.top - mSession.getTileHeight()
: result.captured.bottom;
? mImageTileSet.getTop() - mSession.getTileHeight()
: mImageTileSet.getBottom();
}
requestNextTile(nextTop);
}

View File

@@ -57,16 +57,19 @@ class FakeSession implements ScrollCaptureClient.Session {
private int mScrollDelta;
private int mPageHeight;
private int mTargetHeight;
FakeSession(int pageHeight, float maxPages, int tileHeight, int visiblePageTop,
int visiblePageBottom, int availableTop, int availableBottom) {
int visiblePageBottom, int availableTop, int availableBottom,
int maxTiles) {
mPageHeight = pageHeight;
mTileHeight = tileHeight;
mAvailable = new Rect(0, availableTop, getPageWidth(), availableBottom);
mAvailableTop = new Rect(mAvailable);
mAvailableTop.inset(0, 0, 0, pageHeight);
mVisiblePage = new Rect(0, visiblePageTop, getPageWidth(), visiblePageBottom);
mMaxTiles = (int) Math.ceil((pageHeight * maxPages) / mTileHeight);
mTargetHeight = (int) (pageHeight * maxPages);
mMaxTiles = maxTiles;
}
private static Image mockImage() {
@@ -157,6 +160,11 @@ class FakeSession implements ScrollCaptureClient.Session {
return mMaxTiles;
}
@Override
public int getTargetHeight() {
return mTargetHeight;
}
@Override
public int getTileHeight() {
return mTileHeight;

View File

@@ -41,21 +41,6 @@ import org.junit.runner.RunWith;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class FakeSessionTest extends SysuiTestCase {
@Test
public void testMaxTiles() {
FakeSession session = new FakeSession(
/* pageHeight */ 100,
/* maxPages */ 2.25f,
/* tileHeight */ 10,
/* visiblePageTop */ 0,
/* visiblePageBottom */ 100,
/* availableTop */ -250,
/* availableBottom */ 250);
// (pageHeight * maxPages) / tileHeight
assertEquals("getMaxTiles()", 23, session.getMaxTiles());
}
@Test
public void testNonEmptyResult_hasImage() {
FakeSession session = new FakeSession(
@@ -65,7 +50,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 0,
/* visiblePageBottom */ 100,
/* availableTop */ 0,
/* availableBottom */ 100);
/* availableBottom */ 100,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(0));
assertNotNull("result.image", result.image);
assertNotNull("result.image.getHardwareBuffer()", result.image.getHardwareBuffer());
@@ -80,7 +66,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 0,
/* visiblePageBottom */ 100,
/* availableTop */ 0,
/* availableBottom */ 100);
/* availableBottom */ 100,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(-100));
assertNull("result.image", result.image);
}
@@ -94,7 +81,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 0,
/* visiblePageBottom */ 100,
/* availableTop */ -250,
/* availableBottom */ 250);
/* availableBottom */ 250,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(0));
assertEquals("requested top", 0, result.requested.top);
@@ -113,7 +101,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 0,
/* visiblePageBottom */ 100,
/* availableTop */ -250,
/* availableBottom */ 250);
/* availableBottom */ 250,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(90));
assertEquals("requested top", 90, result.requested.top);
@@ -132,7 +121,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 0,
/* visiblePageBottom */ 100,
/* availableTop */ -250,
/* availableBottom */ 250);
/* availableBottom */ 250,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(-100));
assertEquals("requested top", -100, result.requested.top);
@@ -151,7 +141,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 0,
/* visiblePageBottom */ 100,
/* availableTop */ -250,
/* availableBottom */ 250);
/* availableBottom */ 250,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(150));
assertEquals("requested top", 150, result.requested.top);
@@ -170,7 +161,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 0,
/* visiblePageBottom */ 100,
/* availableTop */ -100,
/* availableBottom */ 100);
/* availableBottom */ 100,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(-125));
assertEquals("requested top", -125, result.requested.top);
@@ -189,7 +181,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 0,
/* visiblePageBottom */ 100,
/* availableTop */ -100,
/* availableBottom */ 100);
/* availableBottom */ 100,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(75));
assertEquals("requested top", 75, result.requested.top);
@@ -211,7 +204,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 25, // <<--
/* visiblePageBottom */ 100,
/* availableTop */ -150,
/* availableBottom */ 150);
/* availableBottom */ 150,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(-150));
assertEquals("requested top", -150, result.requested.top);
@@ -233,7 +227,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 0,
/* visiblePageBottom */ 75,
/* availableTop */ -150,
/* availableBottom */ 150);
/* availableBottom */ 150,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(50));
assertEquals("requested top", 50, result.requested.top);
@@ -252,7 +247,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 0,
/* visiblePageBottom */ 100,
/* availableTop */ -100,
/* availableBottom */ 200);
/* availableBottom */ 200,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(-150));
assertTrue("captured rect is empty", result.captured.isEmpty());
}
@@ -266,7 +262,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 0,
/* visiblePageBottom */ 100,
/* availableTop */ -100,
/* availableBottom */ 200);
/* availableBottom */ 200,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(200));
assertTrue("captured rect is empty", result.captured.isEmpty());
}
@@ -280,7 +277,8 @@ public class FakeSessionTest extends SysuiTestCase {
/* visiblePageTop */ 60, // <<---
/* visiblePageBottom */ 0,
/* availableTop */ -150,
/* availableBottom */ 150);
/* availableBottom */ 150,
/* max Tiles */ 30);
ScrollCaptureClient.CaptureResult result = getUnchecked(session.requestTile(0));
assertEquals("requested top", 0, result.requested.top);

View File

@@ -92,14 +92,16 @@ public class ScrollCaptureControllerTest extends SysuiTestCase {
// Each tile is cropped to the visible page size, which is inset 5px from the TOP
// requested result
// 0, 50 5, 50
// -45, 5 -40, 5 <-- clear previous / top
// 5, 55 5, 55 (not cropped, target is positioned fully within visible range)
// 55, 105 55, 105
// 105, 155 105, 155
// 155, 205 155, 205 <-- bottom
// -45, 5 -40, 5
// -90, -40 -85, -40 <-- clear previous / top
// -40, 10 -40, 10 (not cropped, target is positioned fully within visible range)
// 10, 60 10, 60
// 60, 110 60, 110
// 110, 160 110, 160
// 160, 210 160, 210 <-- bottom
assertEquals("top", -40, screenshot.getTop());
assertEquals("bottom", 205, screenshot.getBottom());
assertEquals("top", -85, screenshot.getTop());
assertEquals("bottom", 210, screenshot.getBottom());
}
@Test
@@ -119,13 +121,14 @@ public class ScrollCaptureControllerTest extends SysuiTestCase {
// requested result
// 0, 50 0, 50 // not cropped, positioned within visible range
// -50, 0 -50, 0 <-- clear previous/reverse
// 0, 50 - 0, 45 // target now positioned at page bottom, bottom cropped
// 0, 50 0, 45 // target now positioned at page bottom, bottom cropped
// 45, 95, 45, 90
// 90, 140, 140, 135
// 135, 185 185, 180 <-- bottom
// 135, 185 185, 180
// 180, 230 180, 225 <-- bottom
assertEquals("top", -50, screenshot.getTop());
assertEquals("bottom", 180, screenshot.getBottom());
assertEquals("bottom", 225, screenshot.getBottom());
}
@Test
@@ -265,7 +268,8 @@ public class ScrollCaptureControllerTest extends SysuiTestCase {
mLocalVisibleBottom = mPageHeight;
}
Session session = new FakeSession(mPageHeight, mMaxPages, mTileHeight,
mLocalVisibleTop, mLocalVisibleBottom, mAvailableTop, mAvailableBottom);
mLocalVisibleTop, mLocalVisibleBottom, mAvailableTop, mAvailableBottom,
/* maxTiles */ 30);
ScrollCaptureClient client = mock(ScrollCaptureClient.class);
when(client.start(/* response */ any(), /* maxPages */ anyFloat()))
.thenReturn(immediateFuture(session));