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