diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageTile.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageTile.java
index 212e6c86e9da3..a95c91bfeceb3 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageTile.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageTile.java
@@ -70,7 +70,7 @@ class ImageTile implements AutoCloseable {
RecordingCanvas canvas = mNode.beginRecording(w, h);
canvas.save();
- canvas.clipRect(0, 0, mLocation.right, mLocation.bottom);
+ canvas.clipRect(0, 0, mLocation.width(), mLocation.height());
canvas.drawBitmap(Bitmap.wrapHardwareBuffer(mImage.getHardwareBuffer(), COLOR_SPACE),
0, 0, null);
canvas.restore();
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java
index 20f8451037233..ae3cd9996f044 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java
@@ -21,6 +21,8 @@ import android.graphics.RecordingCanvas;
import android.graphics.Rect;
import android.graphics.RenderNode;
import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.Log;
import androidx.annotation.UiThread;
@@ -32,11 +34,14 @@ import java.util.List;
*
* To display on-screen, use {@link #getDrawable()}.
*/
-@UiThread
class ImageTileSet {
private static final String TAG = "ImageTileSet";
+ ImageTileSet(@UiThread Handler handler) {
+ mHandler = handler;
+ }
+
interface OnBoundsChangedListener {
/**
* Reports an update to the bounding box that contains all active tiles. These are virtual
@@ -54,6 +59,7 @@ class ImageTileSet {
private final List mTiles = new ArrayList<>();
private final Rect mBounds = new Rect();
+ private final Handler mHandler;
private OnContentChangedListener mOnContentChangedListener;
private OnBoundsChangedListener mOnBoundsChangedListener;
@@ -73,13 +79,32 @@ class ImageTileSet {
newBounds.union(newRect);
if (!newBounds.equals(mBounds)) {
mBounds.set(newBounds);
- if (mOnBoundsChangedListener != null) {
- mOnBoundsChangedListener.onBoundsChanged(
- newBounds.left, newBounds.top, newBounds.right, newBounds.bottom);
- }
+ notifyBoundsChanged(mBounds);
}
- if (mOnContentChangedListener != null) {
+ notifyContentChanged();
+ }
+
+ void notifyContentChanged() {
+ if (mOnContentChangedListener == null) {
+ return;
+ }
+ if (mHandler.getLooper().isCurrentThread()) {
mOnContentChangedListener.onContentChanged();
+ } else {
+ mHandler.post(() -> mOnContentChangedListener.onContentChanged());
+ }
+ }
+
+ void notifyBoundsChanged(Rect bounds) {
+ if (mOnBoundsChangedListener == null) {
+ return;
+ }
+ if (mHandler.getLooper().isCurrentThread()) {
+ mOnBoundsChangedListener.onBoundsChanged(
+ bounds.left, bounds.top, bounds.right, bounds.bottom);
+ } else {
+ mHandler.post(() -> mOnBoundsChangedListener.onBoundsChanged(
+ bounds.left, bounds.top, bounds.right, bounds.bottom));
}
}
@@ -117,22 +142,16 @@ class ImageTileSet {
* getHeight()).
*/
Bitmap toBitmap(Rect bounds) {
+ Log.d(TAG, "exporting with bounds: " + bounds);
if (mTiles.isEmpty()) {
return null;
}
final RenderNode output = new RenderNode("Bitmap Export");
- output.setPosition(0, 0, getWidth(), getHeight());
+ output.setPosition(0, 0, bounds.width(), bounds.height());
RecordingCanvas canvas = output.beginRecording();
- canvas.translate(-getLeft(), -getTop());
- // Additional translation to account for the requested bounds
- canvas.translate(-bounds.left, -bounds.top);
- canvas.clipRect(bounds);
- for (ImageTile tile : mTiles) {
- canvas.save();
- canvas.translate(tile.getLeft(), tile.getTop());
- canvas.drawRenderNode(tile.getDisplayList());
- canvas.restore();
- }
+ Drawable drawable = getDrawable();
+ drawable.setBounds(bounds);
+ drawable.draw(canvas);
output.endRecording();
return HardwareRenderer.createHardwareBitmap(output, bounds.width(), bounds.height());
}
@@ -162,14 +181,13 @@ class ImageTileSet {
}
void clear() {
- mBounds.set(0, 0, 0, 0);
+ if (mBounds.isEmpty()) {
+ return;
+ }
+ mBounds.setEmpty();
mTiles.forEach(ImageTile::close);
mTiles.clear();
- if (mOnBoundsChangedListener != null) {
- mOnBoundsChangedListener.onBoundsChanged(0, 0, 0, 0);
- }
- if (mOnContentChangedListener != null) {
- mOnContentChangedListener.onContentChanged();
- }
+ notifyBoundsChanged(mBounds);
+ notifyContentChanged();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java
index bb07012f23550..d56c806554d47 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java
@@ -50,8 +50,6 @@ 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_PAGES = 5;
- private static final int MAX_IMAGE_COUNT = MAX_PAGES * TILES_PER_PAGE;
@VisibleForTesting
static final int MATCH_ANY_TASK = ActivityTaskManager.INVALID_TASK_ID;
@@ -66,10 +64,11 @@ public class ScrollCaptureClient {
/**
* Session start should be deferred until UI is active because of resource allocation and
* potential visible side effects in the target window.
-
+ *
* @param sessionConsumer listener to receive the session once active
+ * @param maxPages the capture buffer size expressed as a multiple of the content height
*/
- void start(Consumer sessionConsumer);
+ void start(Consumer sessionConsumer, float maxPages);
/**
* Close the connection.
@@ -196,6 +195,7 @@ public class ScrollCaptureClient {
private int mTileWidth;
private Rect mRequestRect;
private boolean mStarted;
+ private int mMaxTiles;
private ControllerCallbacks(Consumer connectionConsumer) {
mConnectionConsumer = connectionConsumer;
@@ -285,12 +285,15 @@ public class ScrollCaptureClient {
// ScrollCaptureController.Connection
@Override
- public void start(Consumer sessionConsumer) {
+ public void start(Consumer sessionConsumer, float maxPages) {
if (DEBUG_SCROLL) {
- Log.d(TAG, "start(sessionConsumer=" + sessionConsumer + ")");
+ Log.d(TAG, "start(sessionConsumer=" + sessionConsumer + ","
+ + " maxPages=" + maxPages + ")"
+ + " [maxHeight: " + (mMaxTiles * mTileHeight) + "px]");
}
+ mMaxTiles = (int) Math.ceil(maxPages * TILES_PER_PAGE);
mReader = ImageReader.newInstance(mTileWidth, mTileHeight, PixelFormat.RGBA_8888,
- MAX_IMAGE_COUNT, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
+ mMaxTiles, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
mSessionConsumer = sessionConsumer;
try {
mConnection.startCapture(mReader.getSurface());
@@ -345,7 +348,7 @@ public class ScrollCaptureClient {
@Override
public int getMaxTiles() {
- return MAX_IMAGE_COUNT;
+ return mMaxTiles;
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
index a78126e701a94..d97f644c5d234 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -24,6 +24,7 @@ import android.content.Intent;
import android.graphics.Rect;
import android.net.Uri;
import android.os.UserHandle;
+import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
@@ -34,6 +35,7 @@ import android.widget.ImageView;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
+import com.android.systemui.screenshot.ScrollCaptureClient.CaptureResult;
import com.android.systemui.screenshot.ScrollCaptureClient.Connection;
import com.android.systemui.screenshot.ScrollCaptureClient.Session;
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
@@ -44,13 +46,23 @@ import java.time.ZonedDateTime;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
-import java.util.function.Consumer;
/**
* Interaction controller between the UI and ScrollCaptureClient.
*/
public class ScrollCaptureController implements OnComputeInternalInsetsListener {
private static final String TAG = "ScrollCaptureController";
+ private static final float MAX_PAGES_DEFAULT = 3f;
+
+ private static final String SETTING_KEY_MAX_PAGES = "screenshot.scroll_max_pages";
+
+ private static final int UP = -1;
+ private static final int DOWN = 1;
+
+ private int mDirection = DOWN;
+ private boolean mAtBottomEdge;
+ private boolean mAtTopEdge;
+ private Session mSession;
// TODO: Support saving without additional action.
private enum PendingAction {
@@ -59,7 +71,6 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
SAVE
}
- public static final int MAX_PAGES = 5;
public static final int MAX_HEIGHT = 12000;
private final Connection mConnection;
@@ -91,7 +102,7 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
mBgExecutor = bgExecutor;
mImageExporter = exporter;
mUiEventLogger = uiEventLogger;
- mImageTileSet = new ImageTileSet();
+ mImageTileSet = new ImageTileSet(context.getMainThreadHandler());
}
/**
@@ -129,7 +140,9 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
mEdit.setOnClickListener(this::onClicked);
mShare.setOnClickListener(this::onClicked);
- mConnection.start(this::startCapture);
+ float maxPages = Settings.Secure.getFloat(mContext.getContentResolver(),
+ SETTING_KEY_MAX_PAGES, MAX_PAGES_DEFAULT);
+ mConnection.start(this::startCapture, maxPages);
}
@@ -232,41 +245,82 @@ public class ScrollCaptureController implements OnComputeInternalInsetsListener
return mWindow.findViewById(res);
}
+
+ private void onCaptureResult(CaptureResult result) {
+ Log.d(TAG, "onCaptureResult: " + result);
+ boolean emptyResult = result.captured.height() == 0;
+ boolean partialResult = !emptyResult
+ && result.captured.height() < result.requested.height();
+ boolean finish = false;
+
+ if (partialResult) {
+ // Potentially reached a vertical boundary. Extend in the other direction.
+ switch (mDirection) {
+ case DOWN:
+ Log.d(TAG, "Reached bottom edge.");
+ mAtBottomEdge = true;
+ mDirection = UP;
+ break;
+ case UP:
+ Log.d(TAG, "Reached top edge.");
+ mAtTopEdge = true;
+ mDirection = DOWN;
+ break;
+ }
+
+ if (mAtTopEdge && mAtBottomEdge) {
+ Log.d(TAG, "Reached both top and bottom edge, ending.");
+ finish = true;
+ } else {
+ // only reverse if the edge was relatively close to the starting point
+ if (mImageTileSet.getHeight() < mSession.getPageHeight() * 3) {
+ Log.d(TAG, "Restarting in reverse direction.");
+
+ // Because of temporary limitations, we cannot just jump to the opposite edge
+ // and continue there. Instead, clear the results and start over capturing from
+ // here in the other direction.
+ mImageTileSet.clear();
+ } else {
+ Log.d(TAG, "Capture is tall enough, stopping here.");
+ finish = true;
+ }
+ }
+ }
+
+ if (!emptyResult) {
+ mImageTileSet.addTile(new ImageTile(result.image, result.captured));
+ }
+
+ Log.d(TAG, "bounds: " + mImageTileSet.getLeft() + "," + mImageTileSet.getTop()
+ + " - " + mImageTileSet.getRight() + "," + mImageTileSet.getBottom()
+ + " (" + mImageTileSet.getWidth() + "x" + mImageTileSet.getHeight() + ")");
+
+
+ // Stop when "too tall"
+ if (mImageTileSet.size() >= mSession.getMaxTiles()
+ || mImageTileSet.getHeight() > MAX_HEIGHT) {
+ Log.d(TAG, "Max height and/or tile count reached.");
+ finish = true;
+ }
+
+ if (finish) {
+ Session session = mSession;
+ mSession = null;
+ Log.d(TAG, "Stop.");
+ mUiExecutor.execute(() -> afterCaptureComplete(session));
+ return;
+ }
+
+ int nextTop = (mDirection == DOWN) ? result.captured.bottom
+ : result.captured.top - mSession.getTileHeight();
+ Log.d(TAG, "requestTile: " + nextTop);
+ mSession.requestTile(nextTop, /* consumer */ this::onCaptureResult);
+ }
+
private void startCapture(Session session) {
- Log.d(TAG, "startCapture");
- Consumer consumer =
- new Consumer() {
-
- int mFrameCount = 0;
- int mTop = 0;
-
- @Override
- public void accept(ScrollCaptureClient.CaptureResult result) {
- mFrameCount++;
-
- boolean emptyFrame = result.captured.height() == 0;
- if (!emptyFrame) {
- ImageTile tile = new ImageTile(result.image, result.captured);
- Log.d(TAG, "Adding tile: " + tile);
- mImageTileSet.addTile(tile);
- Log.d(TAG, "New dimens: w=" + mImageTileSet.getWidth() + ", "
- + "h=" + mImageTileSet.getHeight());
- }
-
- if (emptyFrame || mFrameCount >= MAX_PAGES
- || mTop + session.getTileHeight() > MAX_HEIGHT) {
-
- mUiExecutor.execute(() -> afterCaptureComplete(session));
- return;
- }
- mTop += result.captured.height();
- session.requestTile(mTop, /* consumer */ this);
- }
- };
-
- // fire it up!
- session.requestTile(0, consumer);
- };
+ mSession = session;
+ session.requestTile(0, this::onCaptureResult);
+ }
@UiThread
void afterCaptureComplete(Session session) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TiledImageDrawable.java b/packages/SystemUI/src/com/android/systemui/screenshot/TiledImageDrawable.java
index 72f489bdd398d..4ec8eb22c67ae 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TiledImageDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TiledImageDrawable.java
@@ -56,7 +56,7 @@ public class TiledImageDrawable extends Drawable {
mNode = new RenderNode("TiledImageDrawable");
}
mNode.setPosition(0, 0, mTiles.getWidth(), mTiles.getHeight());
- Canvas canvas = mNode.beginRecording(mTiles.getWidth(), mTiles.getHeight());
+ Canvas canvas = mNode.beginRecording();
// Align content (virtual) top/left with 0,0, within the render node
canvas.translate(-mTiles.getLeft(), -mTiles.getTop());
for (int i = 0; i < mTiles.size(); i++) {
@@ -79,8 +79,8 @@ public class TiledImageDrawable extends Drawable {
if (canvas.isHardwareAccelerated()) {
Rect bounds = getBounds();
canvas.save();
- canvas.clipRect(bounds);
- canvas.translate(bounds.left, bounds.top);
+ canvas.clipRect(0, 0, bounds.width(), bounds.height());
+ canvas.translate(-bounds.left, -bounds.top);
canvas.drawRenderNode(mNode);
canvas.restore();
} else {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureClientTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureClientTest.java
index c1c637129d851..580f800fbc440 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureClientTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureClientTest.java
@@ -57,6 +57,8 @@ import org.mockito.stubbing.Answer;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class ScrollCaptureClientTest extends SysuiTestCase {
+ private static final float MAX_PAGES = 3f;
+
private Context mContext;
private IWindowManager mWm;
@@ -96,7 +98,7 @@ public class ScrollCaptureClientTest extends SysuiTestCase {
Connection conn = mConnectionConsumer.getValue();
- conn.start(mSessionConsumer);
+ conn.start(mSessionConsumer, MAX_PAGES);
verify(mSessionConsumer, timeout(100)).accept(any(Session.class));
Session session = mSessionConsumer.getValue();