Merge changes Id95e80ff,Ica28e470,I63d524ae,I5c765b4e,Id93cfc7d, ... into sc-dev

* changes:
  Long screenshots: allow adjusting the max capture size
  Long screenshots: Fix/simplify ImageTileSet.toBitmap
  Long screenshots: limit batch capture to 3 pages
  Long screenshots: allow capture in reverse direction
  Long screenshots: dispatch listeners on UI thread
  Long screenshots: Fix clipRect for ImageTile
This commit is contained in:
Mark Renouf
2021-02-11 19:41:44 +00:00
committed by Android (Google) Code Review
6 changed files with 152 additions and 75 deletions

View File

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

View File

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

View File

@@ -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<Session> sessionConsumer);
void start(Consumer<Session> 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<Connection> connectionConsumer) {
mConnectionConsumer = connectionConsumer;
@@ -285,12 +285,15 @@ public class ScrollCaptureClient {
// ScrollCaptureController.Connection
@Override
public void start(Consumer<Session> sessionConsumer) {
public void start(Consumer<Session> 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

View File

@@ -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<ScrollCaptureClient.CaptureResult> consumer =
new Consumer<ScrollCaptureClient.CaptureResult>() {
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) {

View File

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

View File

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