diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageTile.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageTile.java
new file mode 100644
index 0000000000000..143121af9f2c1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageTile.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.screenshot;
+
+import static android.graphics.ColorSpace.Named.SRGB;
+
+import static java.util.Objects.requireNonNull;
+
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.media.Image;
+
+/**
+ * Holds a hardware image, coordinates and render node to draw the tile. The tile manages clipping
+ * and dimensions. The tile must be drawn translated to the correct target position:
+ *
+ * ImageTile tile = getTile();
+ * canvas.save();
+ * canvas.translate(tile.getLeft(), tile.getTop());
+ * canvas.drawRenderNode(tile.getDisplayList());
+ * canvas.restore();
+ *
+ */
+class ImageTile implements AutoCloseable {
+ private final Image mImage;
+ private final Rect mLocation;
+ private RenderNode mNode;
+
+ private static final ColorSpace COLOR_SPACE = ColorSpace.get(SRGB);
+
+ /**
+ * Create an image tile from the given image.
+ *
+ * @param image an image containing a hardware buffer
+ * @param location the captured area represented by image tile (virtual coordinates)
+ */
+ ImageTile(Image image, Rect location) {
+ mImage = requireNonNull(image, "image");
+ mLocation = location;
+
+ requireNonNull(mImage.getHardwareBuffer(), "image must be a hardware image");
+ }
+
+ RenderNode getDisplayList() {
+ if (mNode == null) {
+ mNode = new RenderNode("Tile{" + Integer.toHexString(mImage.hashCode()) + "}");
+ }
+ if (mNode.hasDisplayList()) {
+ return mNode;
+ }
+ final int w = Math.min(mImage.getWidth(), mLocation.width());
+ final int h = Math.min(mImage.getHeight(), mLocation.height());
+ mNode.setPosition(0, 0, w, h);
+
+ RecordingCanvas canvas = mNode.beginRecording(w, h);
+ Rect rect = new Rect(0, 0, w, h);
+ canvas.save();
+ canvas.clipRect(0, 0, mLocation.right, mLocation.bottom);
+ canvas.drawBitmap(Bitmap.wrapHardwareBuffer(mImage.getHardwareBuffer(), COLOR_SPACE),
+ 0, 0, null);
+ canvas.restore();
+ mNode.endRecording();
+ return mNode;
+ }
+
+ Rect getLocation() {
+ return mLocation;
+ }
+
+ int getLeft() {
+ return mLocation.left;
+ }
+
+ int getTop() {
+ return mLocation.top;
+ }
+
+ int getRight() {
+ return mLocation.right;
+ }
+
+ int getBottom() {
+ return mLocation.bottom;
+ }
+
+ @Override
+ public void close() {
+ mImage.close();
+ mNode.discardDisplayList();
+ }
+
+ @Override
+ public String toString() {
+ return "{location=" + mLocation + ", source=" + mImage
+ + ", buffer=" + mImage.getHardwareBuffer() + "}";
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java
new file mode 100644
index 0000000000000..8ff66f548172a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.screenshot;
+
+import android.graphics.Bitmap;
+import android.graphics.HardwareRenderer;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.UiThread;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Owns a series of partial screen captures (tiles).
+ *
+ * To display on-screen, use {@link #getDrawable()}.
+ */
+@UiThread
+class ImageTileSet {
+
+ private static final String TAG = "ImageTileSet";
+
+ 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.
+ */
+ void onContentChanged();
+ }
+
+ private final List mTiles = new ArrayList<>();
+ private final Rect mBounds = new Rect();
+
+ private OnContentChangedListener mOnContentChangedListener;
+ private OnBoundsChangedListener mOnBoundsChangedListener;
+
+ void setOnBoundsChangedListener(OnBoundsChangedListener listener) {
+ mOnBoundsChangedListener = listener;
+ }
+
+ void setOnContentChangedListener(OnContentChangedListener listener) {
+ mOnContentChangedListener = listener;
+ }
+
+ void addTile(ImageTile tile) {
+ final Rect newBounds = new Rect(mBounds);
+ final Rect newRect = tile.getLocation();
+ mTiles.add(tile);
+ newBounds.union(newRect);
+ if (!newBounds.equals(mBounds)) {
+ mBounds.set(newBounds);
+ if (mOnBoundsChangedListener != null) {
+ mOnBoundsChangedListener.onBoundsChanged(
+ newBounds.left, newBounds.top, newBounds.right, newBounds.bottom);
+ }
+ }
+ if (mOnContentChangedListener != null) {
+ mOnContentChangedListener.onContentChanged();
+ }
+ }
+
+ /**
+ * 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()},
+ * and {@link #getBottom()} which are dimensions relative to the capture start position
+ * (positive or negative).
+ *
+ * @return a drawable to display the image content
+ */
+ Drawable getDrawable() {
+ return new TiledImageDrawable(this);
+ }
+
+ boolean isEmpty() {
+ return mTiles.isEmpty();
+ }
+
+ int size() {
+ return mTiles.size();
+ }
+
+ ImageTile get(int i) {
+ return mTiles.get(i);
+ }
+
+ Bitmap toBitmap() {
+ if (mTiles.isEmpty()) {
+ return null;
+ }
+ final RenderNode output = new RenderNode("Bitmap Export");
+ output.setPosition(0, 0, getWidth(), getHeight());
+ RecordingCanvas canvas = output.beginRecording();
+ canvas.translate(-getLeft(), -getTop());
+ for (ImageTile tile : mTiles) {
+ canvas.save();
+ canvas.translate(tile.getLeft(), tile.getTop());
+ canvas.drawRenderNode(tile.getDisplayList());
+ canvas.restore();
+ }
+ output.endRecording();
+ return HardwareRenderer.createHardwareBitmap(output, getWidth(), getHeight());
+ }
+
+ int getLeft() {
+ return mBounds.left;
+ }
+
+ int getTop() {
+ return mBounds.top;
+ }
+
+ int getRight() {
+ return mBounds.right;
+ }
+
+ int getBottom() {
+ return mBounds.bottom;
+ }
+
+ int getWidth() {
+ return mBounds.width();
+ }
+
+ int getHeight() {
+ return mBounds.height();
+ }
+
+ void clear() {
+ mBounds.set(0, 0, 0, 0);
+ mTiles.forEach(ImageTile::close);
+ mTiles.clear();
+ if (mOnBoundsChangedListener != null) {
+ mOnBoundsChangedListener.onBoundsChanged(0, 0, 0, 0);
+ }
+ if (mOnContentChangedListener != null) {
+ mOnContentChangedListener.onContentChanged();
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java
index e159992bc9a54..bb07012f23550 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java
@@ -18,6 +18,7 @@ package com.android.systemui.screenshot;
import static com.android.systemui.screenshot.LogConfig.DEBUG_SCROLL;
+import static java.lang.Math.min;
import static java.util.Objects.requireNonNull;
import android.annotation.UiContext;
@@ -44,15 +45,20 @@ import java.util.function.Consumer;
import javax.inject.Inject;
/**
- * High level interface to scroll capture API.
+ * High(er) level interface to scroll capture API.
*/
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;
private static final String TAG = LogConfig.logTag(ScrollCaptureClient.class);
+
/**
* A connection to a remote window. Starts a capture session.
*/
@@ -60,13 +66,10 @@ 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 maxBuffers the maximum number of buffers (tiles) that may be in use at one
- * time, tiles are not cached anywhere so set this to a large enough
- * number to retain offscreen content until it is no longer needed
+
* @param sessionConsumer listener to receive the session once active
*/
- void start(int maxBuffers, Consumer sessionConsumer);
+ void start(Consumer sessionConsumer);
/**
* Close the connection.
@@ -100,26 +103,33 @@ public class ScrollCaptureClient {
*/
interface Session {
/**
- * Request the given horizontal strip. Values are y-coordinates in captured space, relative
- * to start position.
+ * Request an image tile at the given position, from top, to top + {@link #getTileHeight()},
+ * and from left 0, to {@link #getPageWidth()}
*
- * @param contentRect the area to capture, in content rect space, relative to scroll-bounds
+ * @param top the top (y) position of the tile to capture, in content rect space
* @param consumer listener to be informed of the result
*/
- void requestTile(Rect contentRect, Consumer consumer);
+ void requestTile(int top, Consumer consumer);
/**
- * End the capture session, return the target app to original state. The returned
- * stage must be waited for to complete to allow the target app a chance to restore to
- * original state before becoming visible.
+ * Returns the maximum number of tiles which may be requested and retained without
+ * being {@link Image#close() closed}.
*
- * @return a stage presenting the session shutdown
+ * @return the maximum number of open tiles allowed
+ */
+ int getMaxTiles();
+
+ int getTileHeight();
+
+ int getPageHeight();
+
+ int getPageWidth();
+
+ /**
+ * End the capture session, return the target app to original state. The listener
+ * will be called when the target app is ready to before visible and interactive.
*/
void end(Runnable listener);
-
- int getMaxTileHeight();
-
- int getMaxTileWidth();
}
private final IWindowManager mWindowManagerService;
@@ -131,6 +141,12 @@ public class ScrollCaptureClient {
mWindowManagerService = windowManagerService;
}
+ /**
+ * Set the window token for the screenshot window/ This is required to avoid targeting our
+ * window or any above it.
+ *
+ * @param token the windowToken of the screenshot window
+ */
public void setHostWindowToken(IBinder token) {
mHostWindowToken = token;
}
@@ -176,6 +192,8 @@ public class ScrollCaptureClient {
private ImageReader mReader;
private Rect mScrollBounds;
+ private int mTileHeight;
+ private int mTileWidth;
private Rect mRequestRect;
private boolean mStarted;
@@ -197,6 +215,15 @@ public class ScrollCaptureClient {
mScrollBounds = scrollBounds;
mConnectionConsumer.accept(this);
mConnectionConsumer = null;
+
+ int pxPerPage = mScrollBounds.width() * mScrollBounds.height();
+ int pxPerTile = min(TILE_SIZE_PX_MAX, (pxPerPage / TILES_PER_PAGE));
+ mTileWidth = mScrollBounds.width();
+ mTileHeight = pxPerTile / mScrollBounds.width();
+ if (DEBUG_SCROLL) {
+ Log.d(TAG, "scrollBounds: " + mScrollBounds);
+ Log.d(TAG, "tile dimen: " + mTileWidth + "x" + mTileHeight);
+ }
}
@Override
@@ -257,24 +284,19 @@ public class ScrollCaptureClient {
// ScrollCaptureController.Connection
- // -> Error handling: BiConsumer ?
@Override
- public void start(int maxBufferCount, Consumer sessionConsumer) {
+ public void start(Consumer sessionConsumer) {
if (DEBUG_SCROLL) {
- Log.d(TAG, "start(maxBufferCount=" + maxBufferCount
- + ", sessionConsumer=" + sessionConsumer + ")");
+ Log.d(TAG, "start(sessionConsumer=" + sessionConsumer + ")");
}
- mReader = ImageReader.newInstance(mScrollBounds.width(), mScrollBounds.height(),
- PixelFormat.RGBA_8888, maxBufferCount, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
+ mReader = ImageReader.newInstance(mTileWidth, mTileHeight, PixelFormat.RGBA_8888,
+ MAX_IMAGE_COUNT, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
mSessionConsumer = sessionConsumer;
try {
mConnection.startCapture(mReader.getSurface());
mStarted = true;
} catch (RemoteException e) {
- Log.w(TAG, "should not be happening :-(");
- // ?
- //mSessionListener.onError(e);
- //mSessionListener = null;
+ Log.w(TAG, "Failed to start", e);
}
}
@@ -307,27 +329,36 @@ public class ScrollCaptureClient {
}
@Override
- public int getMaxTileHeight() {
+ public int getPageHeight() {
return mScrollBounds.height();
}
@Override
- public int getMaxTileWidth() {
+ public int getPageWidth() {
return mScrollBounds.width();
}
@Override
- public void requestTile(Rect contentRect, Consumer consumer) {
+ public int getTileHeight() {
+ return mTileHeight;
+ }
+
+ @Override
+ public int getMaxTiles() {
+ return MAX_IMAGE_COUNT;
+ }
+
+ @Override
+ public void requestTile(int top, Consumer consumer) {
if (DEBUG_SCROLL) {
- Log.d(TAG, "requestTile(contentRect=" + contentRect + "consumer=" + consumer + ")");
+ Log.d(TAG, "requestTile(top=" + top + ", consumer=" + consumer + ")");
}
- mRequestRect = new Rect(contentRect);
+ mRequestRect = new Rect(0, top, mTileWidth, top + mTileHeight);
mResultConsumer = consumer;
try {
mConnection.requestImage(mRequestRect);
} catch (RemoteException e) {
Log.e(TAG, "Caught remote exception from requestImage", e);
- // ?
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
index 27c74ac0938d7..c75efbcc5f804 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -16,16 +16,9 @@
package com.android.systemui.screenshot;
-import static android.graphics.ColorSpace.Named.SRGB;
-
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.ColorSpace;
-import android.graphics.Picture;
-import android.graphics.Rect;
-import android.media.Image;
import android.net.Uri;
import android.os.UserHandle;
import android.util.Log;
@@ -46,6 +39,8 @@ import java.util.function.Consumer;
public class ScrollCaptureController {
private static final String TAG = "ScrollCaptureController";
+ private static final boolean USE_TILED_IMAGE = false;
+
public static final int MAX_PAGES = 5;
public static final int MAX_HEIGHT = 12000;
@@ -55,7 +50,7 @@ public class ScrollCaptureController {
private final Executor mUiExecutor;
private final Executor mBgExecutor;
private final ImageExporter mImageExporter;
- private Picture mPicture;
+ private final ImageTileSet mImageTileSet;
public ScrollCaptureController(Context context, Connection connection, Executor uiExecutor,
Executor bgExecutor, ImageExporter exporter) {
@@ -64,6 +59,7 @@ public class ScrollCaptureController {
mUiExecutor = uiExecutor;
mBgExecutor = bgExecutor;
mImageExporter = exporter;
+ mImageTileSet = new ImageTileSet();
}
/**
@@ -72,50 +68,50 @@ public class ScrollCaptureController {
* @param after action to take after the flow is complete
*/
public void run(final Runnable after) {
- mConnection.start(MAX_PAGES, (session) -> startCapture(session, after));
+ mConnection.start((session) -> startCapture(session, after));
}
private void startCapture(Session session, final Runnable onDismiss) {
- Rect requestRect = new Rect(0, 0,
- session.getMaxTileWidth(), session.getMaxTileHeight());
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) {
- mPicture = stackBelow(mPicture, result.image, result.captured.width(),
- result.captured.height());
+ mImageTileSet.addTile(new ImageTile(result.image, result.captured));
}
+
if (emptyFrame || mFrameCount >= MAX_PAGES
- || requestRect.bottom > MAX_HEIGHT) {
- if (mPicture != null) {
- exportToFile(mPicture, session, onDismiss);
+ || mTop + session.getTileHeight() > MAX_HEIGHT) {
+ if (!mImageTileSet.isEmpty()) {
+ exportToFile(mImageTileSet.toBitmap(), session, onDismiss);
+ mImageTileSet.clear();
} else {
session.end(onDismiss);
}
return;
}
- requestRect.offset(0, session.getMaxTileHeight());
- session.requestTile(requestRect, /* consumer */ this);
+ mTop += result.captured.height();
+ session.requestTile(mTop, /* consumer */ this);
}
};
// fire it up!
- session.requestTile(requestRect, consumer);
+ session.requestTile(0, consumer);
};
- void exportToFile(Picture picture, Session session, Runnable afterEnd) {
+ void exportToFile(Bitmap bitmap, Session session, Runnable afterEnd) {
mImageExporter.setFormat(Bitmap.CompressFormat.PNG);
mImageExporter.setQuality(6);
ListenableFuture future =
- mImageExporter.export(mBgExecutor, Bitmap.createBitmap(picture));
+ mImageExporter.export(mBgExecutor, bitmap);
future.addListener(() -> {
- picture.close(); // release resources
try {
launchViewer(future.get());
} catch (InterruptedException | ExecutionException e) {
@@ -126,41 +122,6 @@ public class ScrollCaptureController {
}, mUiExecutor);
}
- /**
- * Combine the top {@link Picture} with an {@link Image} by appending the image directly
- * below, creating a result that is the combined height of both.
- *
- * Note: no pixel data is transferred here, only a record of drawing commands. Backing
- * hardware buffers must not be modified/recycled until the picture is
- * {@link Picture#close closed}.
- *
- * @param top the existing picture
- * @param below the image to append below
- * @param cropWidth the width of the pixel data to use from the image
- * @param cropHeight the height of the pixel data to use from the image
- *
- * @return a new Picture which draws the previous picture with the image below it
- */
- private static Picture stackBelow(Picture top, Image below, int cropWidth, int cropHeight) {
- int width = cropWidth;
- int height = cropHeight;
- if (top != null) {
- height += top.getHeight();
- width = Math.max(width, top.getWidth());
- }
- Picture combined = new Picture();
- Canvas canvas = combined.beginRecording(width, height);
- int y = 0;
- if (top != null) {
- canvas.drawPicture(top, new Rect(0, 0, top.getWidth(), top.getHeight()));
- y += top.getHeight();
- }
- canvas.drawBitmap(Bitmap.wrapHardwareBuffer(
- below.getHardwareBuffer(), ColorSpace.get(SRGB)), 0, y, null);
- combined.endRecording();
- return combined;
- }
-
void launchViewer(Uri uri) {
Intent editIntent = new Intent(Intent.ACTION_VIEW);
editIntent.setType("image/png");
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TiledImageDrawable.java b/packages/SystemUI/src/com/android/systemui/screenshot/TiledImageDrawable.java
new file mode 100644
index 0000000000000..72f489bdd398d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TiledImageDrawable.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.screenshot;
+
+import android.annotation.Nullable;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+/**
+ * Draws a set of hardware image tiles from a display list. The tiles exist in virtual coordinate
+ * space that may extend into positive or negative values. The origin is the upper-left-most corner
+ * of bounding box, which is drawn at 0,0 to the drawable (output) bounds.
+ */
+public class TiledImageDrawable extends Drawable {
+
+ private static final String TAG = "TiledImageDrawable";
+
+ private final ImageTileSet mTiles;
+ private RenderNode mNode;
+
+ public TiledImageDrawable(ImageTileSet tiles) {
+ mTiles = tiles;
+ mTiles.setOnContentChangedListener(this::onContentChanged);
+ }
+
+ private void onContentChanged() {
+ if (mNode != null && mNode.hasDisplayList()) {
+ mNode.discardDisplayList();
+ }
+ invalidateSelf();
+ }
+
+ private void rebuildDisplayListIfNeeded() {
+ if (mNode != null && mNode.hasDisplayList()) {
+ return;
+ }
+ if (mNode == null) {
+ mNode = new RenderNode("TiledImageDrawable");
+ }
+ mNode.setPosition(0, 0, mTiles.getWidth(), mTiles.getHeight());
+ Canvas canvas = mNode.beginRecording(mTiles.getWidth(), mTiles.getHeight());
+ // 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++) {
+ ImageTile tile = mTiles.get(i);
+ canvas.save();
+ canvas.translate(tile.getLeft(), tile.getTop());
+ canvas.drawRenderNode(tile.getDisplayList());
+ canvas.restore();
+ }
+ mNode.endRecording();
+ }
+
+ /**
+ * Draws the tiled image to the canvas, with the top/left (virtual) coordinate aligned to 0,0
+ * placed at left/top of the drawable's bounds.
+ */
+ @Override
+ public void draw(Canvas canvas) {
+ rebuildDisplayListIfNeeded();
+ if (canvas.isHardwareAccelerated()) {
+ Rect bounds = getBounds();
+ canvas.save();
+ canvas.clipRect(bounds);
+ canvas.translate(bounds.left, bounds.top);
+ canvas.drawRenderNode(mNode);
+ canvas.restore();
+ } else {
+ Log.d(TAG, "Canvas is not hardware accelerated. Skipping draw!");
+ }
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mTiles.getWidth();
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mTiles.getHeight();
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ if (mNode.setAlpha(alpha / 255f)) {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ throw new IllegalArgumentException("not implemented");
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.OPAQUE;
+ }
+}
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 4aa730efa91e7..c1c637129d851 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureClientTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureClientTest.java
@@ -96,13 +96,13 @@ public class ScrollCaptureClientTest extends SysuiTestCase {
Connection conn = mConnectionConsumer.getValue();
- conn.start(5, mSessionConsumer);
+ conn.start(mSessionConsumer);
verify(mSessionConsumer, timeout(100)).accept(any(Session.class));
Session session = mSessionConsumer.getValue();
- Rect request = new Rect(0, 0, session.getMaxTileWidth(), session.getMaxTileHeight());
+ Rect request = new Rect(0, 0, session.getPageWidth(), session.getTileHeight());
- session.requestTile(request, mResultConsumer);
+ session.requestTile(0, mResultConsumer);
verify(mResultConsumer, timeout(100)).accept(any(CaptureResult.class));
CaptureResult result = mResultConsumer.getValue();