Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e029492f authored by Mark Renouf's avatar Mark Renouf Committed by Android (Google) Code Review
Browse files

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
parents 8033b081 3ace0147
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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();
+42 −24
Original line number Diff line number Diff line
@@ -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);
        }
        notifyContentChanged();
    }

    void notifyContentChanged() {
        if (mOnContentChangedListener == null) {
            return;
        }
        if (mOnContentChangedListener != null) {
        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();
    }
}
+11 −8
Original line number Diff line number Diff line
@@ -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
+92 −38
Original line number Diff line number Diff line
@@ -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 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());

    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 (emptyFrame || mFrameCount >= MAX_PAGES
                                || mTop + session.getTileHeight() > MAX_HEIGHT) {
            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;
        }
                        mTop += result.captured.height();
                        session.requestTile(mTop, /* consumer */ this);

        int nextTop = (mDirection == DOWN) ? result.captured.bottom
                : result.captured.top - mSession.getTileHeight();
        Log.d(TAG, "requestTile: " + nextTop);
        mSession.requestTile(nextTop, /* consumer */ this::onCaptureResult);
    }
                };

        // fire it up!
        session.requestTile(0, consumer);
    };
    private void startCapture(Session session) {
        mSession = session;
        session.requestTile(0, this::onCaptureResult);
    }

    @UiThread
    void afterCaptureComplete(Session session) {
+3 −3
Original line number Diff line number Diff line
@@ -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 {
Loading