Commit cac1bef5 authored by ckitagawa's avatar ckitagawa Committed by Commit Bot

[Paint Preview] Compress Bitmaps outside of viewport

This CL compresses out-of-viewport bitmaps to reduce memory usage.
This leads to additional pop-in time for tiles outside of the current
viewport, but is reasonably fast and might be necessary to reduce
memory usage to something reasonable.

Bug: TODO
Change-Id: I7dcd050b58fb18b4f79eebc64f55abd04091531b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2445853
Commit-Queue: Calder Kitagawa <ckitagawa@chromium.org>
Reviewed-by: default avatarMehran Mahmoudi <mahmoudi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#815275}
parent 060c5198
......@@ -61,6 +61,7 @@ android_library("java") {
"java/src/org/chromium/components/paintpreview/player/PlayerSwipeRefreshHandler.java",
"java/src/org/chromium/components/paintpreview/player/PlayerUserActionRecorder.java",
"java/src/org/chromium/components/paintpreview/player/PlayerUserFrustrationDetector.java",
"java/src/org/chromium/components/paintpreview/player/frame/CompressibleBitmap.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameBitmapPainter.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameBitmapState.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameBitmapStateController.java",
......@@ -168,6 +169,7 @@ junit_binary("paint_preview_junit_tests") {
sources = [
"junit/src/org/chromium/components/paintpreview/player/PlayerManagerTest.java",
"junit/src/org/chromium/components/paintpreview/player/PlayerUserFrustrationDetectorTest.java",
"junit/src/org/chromium/components/paintpreview/player/frame/CompressibleBitmapTest.java",
"junit/src/org/chromium/components/paintpreview/player/frame/PaintPreviewCustomFlingingShadowScroller.java",
"junit/src/org/chromium/components/paintpreview/player/frame/PlayerFrameBitmapPainterTest.java",
"junit/src/org/chromium/components/paintpreview/player/frame/PlayerFrameCoordinatorTest.java",
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player.frame;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.SequencedTaskRunner;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A class representing a {@link Bitmap} that can be compressed into the associated byte array.
* When compressed the {@link Bitmap} can safely be discarded and restored from the compressed
* version. Compressing the bitmap is preferred for all bitmaps outside the current viewport.
*/
class CompressibleBitmap {
private static final int IN_USE_BACKOFF_MS = 50;
private Bitmap mBitmap;
private byte[] mCompressedData;
private SequencedTaskRunner mTaskRunner;
private AtomicBoolean mInUse = new AtomicBoolean();
private ThreadUtils.ThreadChecker mThreadChecker;
/**
* Creates a new compressible bitmap which starts to compress immediately.
* @param bitmap The bitmap to store.
* @param taskRunner The task runner to compress/inflate the bitmap on.
* @param visible Whether the bitmap is currently visible. If visible, the bitmap won't be
* immediately discarded.
*/
CompressibleBitmap(Bitmap bitmap, SequencedTaskRunner taskRunner, boolean visible) {
mBitmap = bitmap;
mTaskRunner = taskRunner;
mTaskRunner.postTask(() -> { mThreadChecker = new ThreadUtils.ThreadChecker(); });
compressInBackground(visible);
}
/**
* Locks modifying {@link mBitmap} to prevent use/discard from happening in parallel.
*/
boolean lock() {
return mInUse.compareAndSet(false, true);
}
/**
* Unlocks modifying of {@link mBitmap} so that it is available for use/discard by the next
* thread that calls {@link lock()}.
*/
boolean unlock() {
return mInUse.compareAndSet(true, false);
}
/**
* Gets the bitmap if one is inflated.
* @return the bitmap or null if not inflated.
*/
Bitmap getBitmap() {
return mBitmap;
}
/**
* Destroys the data associated with this bitmap.
*/
void destroy() {
mTaskRunner.postTask(this::destroyInternal);
}
/**
* Discards the inflated bitmap if it has been successfully compressed.
*/
void discardBitmap() {
mTaskRunner.postTask(this::discardBitmapInternal);
}
/**
* Inflates the compressed bitmap in the background. Call from the UI thread.
* @param onInflated Callback that is called when inflation is completed on the UI Thread.
* Callers should check that the bitmap was actually inflated via {@link getBitmap()}.
*/
void inflateInBackground(Callback<CompressibleBitmap> onInflated) {
mTaskRunner.postTask(() -> {
inflate();
if (onInflated != null) {
onInflated.onResult(this);
}
});
}
private boolean inflate() {
mThreadChecker.assertOnValidThread();
if (mBitmap != null) return true;
if (mCompressedData == null) return false;
mBitmap = BitmapFactory.decodeByteArray(mCompressedData, 0, mCompressedData.length);
return mBitmap != null;
}
private void compress() {
mThreadChecker.assertOnValidThread();
if (mBitmap == null) return;
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
boolean success = mBitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayStream);
if (success) {
mCompressedData = byteArrayStream.toByteArray();
}
}
private void compressInBackground(boolean visible) {
mTaskRunner.postTask(() -> {
compress();
if (visible) return;
discardBitmapInternal();
});
}
private void discardBitmapInternal() {
mThreadChecker.assertOnValidThread();
if (!lock()) {
mTaskRunner.postDelayedTask(this::discardBitmapInternal, IN_USE_BACKOFF_MS);
return;
}
if (mBitmap != null && mCompressedData != null) {
mBitmap.recycle();
mBitmap = null;
}
unlock();
}
private void destroyInternal() {
mThreadChecker.assertOnValidThread();
if (!lock()) {
mTaskRunner.postDelayedTask(this::destroyInternal, IN_USE_BACKOFF_MS);
return;
}
if (mBitmap != null) {
mBitmap.recycle();
mBitmap = null;
}
mCompressedData = null;
unlock();
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof CompressibleBitmap)) return false;
CompressibleBitmap od = (CompressibleBitmap) o;
if (mCompressedData != null && od.mCompressedData != null) {
return Arrays.equals(mCompressedData, od.mCompressedData);
}
if (mBitmap != null && od.mBitmap != null) {
return mBitmap.equals(od.mBitmap);
}
return false;
}
}
......@@ -7,23 +7,48 @@ package org.chromium.components.paintpreview.player.frame;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Handler;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashSet;
import java.util.Set;
/**
* Given a viewport {@link Rect} and a matrix of {@link Bitmap} tiles, this class draws the bitmaps
* on a {@link Canvas}.
*/
class PlayerFrameBitmapPainter {
private Size mTileSize;
private Bitmap[][] mBitmapMatrix;
private CompressibleBitmap[][] mBitmapMatrix;
private Rect mViewPort = new Rect();
private Rect mDrawBitmapSrc = new Rect();
private Rect mDrawBitmapDst = new Rect();
private Runnable mInvalidateCallback;
private Runnable mFirstPaintListener;
private Handler mHandler = new Handler();
// The following sets should only be modified on {@link mHandler} or UI thread.
/**
* Tracks which bitmaps are used in each {@link onDraw(Canvas)} call. Bitmaps in this set were
* in the viewport for the last draw. Bitmaps that are not in this set but are in
* {@link mInflatedBitmaps} are discarded at the end of {@link onDraw(Canvas)}.
*/
private Set<CompressibleBitmap> mBitmapsToKeep = new HashSet<>();
/**
* Keeps track of which bitmaps are queued for inflation. Each bitmap in this list will be
* inflated. Although if the bitmap leaves the viewport before being added to this set it
* will be discarded in the next {@link onDraw(Canvas)}.
*/
private Set<CompressibleBitmap> mInflatingBitmaps = new HashSet<>();
/**
* Keeps track of which bitmaps are inflated. Bitmaps in this set are cached in inflated form
* to keep {@link onDraw(Canvas)} performant.
*/
private Set<CompressibleBitmap> mInflatedBitmaps = new HashSet<>();
PlayerFrameBitmapPainter(@NonNull Runnable invalidateCallback,
@Nullable Runnable firstPaintListener) {
......@@ -40,7 +65,7 @@ class PlayerFrameBitmapPainter {
mInvalidateCallback.run();
}
void updateBitmapMatrix(Bitmap[][] bitmapMatrix) {
void updateBitmapMatrix(CompressibleBitmap[][] bitmapMatrix) {
mBitmapMatrix = bitmapMatrix;
mInvalidateCallback.run();
}
......@@ -63,11 +88,42 @@ class PlayerFrameBitmapPainter {
rowEnd = Math.min(rowEnd, mBitmapMatrix.length);
colEnd = Math.min(colEnd, rowEnd >= 1 ? mBitmapMatrix[rowEnd - 1].length : 0);
mInflatingBitmaps.clear();
mBitmapsToKeep.clear();
for (int row = rowStart; row < rowEnd; row++) {
for (int col = colStart; col < colEnd; col++) {
Bitmap tileBitmap = mBitmapMatrix[row][col];
CompressibleBitmap compressibleBitmap = mBitmapMatrix[row][col];
if (compressibleBitmap == null) continue;
mBitmapsToKeep.add(compressibleBitmap);
if (!compressibleBitmap.lock()) {
// Re-issue an invalidation on the chance access was blocked due to being
// discarded.
mHandler.post(mInvalidateCallback);
continue;
}
Bitmap tileBitmap = compressibleBitmap.getBitmap();
if (tileBitmap == null) {
compressibleBitmap.unlock();
mInflatingBitmaps.add(compressibleBitmap);
compressibleBitmap.inflateInBackground(inflatedBitmap -> {
final boolean inflated = inflatedBitmap.getBitmap() != null;
// Handler is on the UI thread so the needed bitmaps will be the last
// set of bitmaps requested.
mHandler.post(() -> {
if (inflated) {
mInflatedBitmaps.add(inflatedBitmap);
}
mInflatingBitmaps.remove(inflatedBitmap);
if (mInflatingBitmaps.isEmpty()) {
mInvalidateCallback.run();
}
});
});
continue;
} else {
mInflatedBitmaps.add(compressibleBitmap);
}
// Calculate the portion of this tileBitmap that is visible in mViewPort.
......@@ -87,11 +143,19 @@ class PlayerFrameBitmapPainter {
mDrawBitmapDst.set(canvasLeft, canvasTop, canvasRight, canvasBottom);
canvas.drawBitmap(tileBitmap, mDrawBitmapSrc, mDrawBitmapDst, null);
compressibleBitmap.unlock();
if (mFirstPaintListener != null) {
mFirstPaintListener.run();
mFirstPaintListener = null;
}
}
}
for (CompressibleBitmap inflatedBitmap : mInflatedBitmaps) {
if (mBitmapsToKeep.contains(inflatedBitmap)) continue;
inflatedBitmap.discardBitmap();
}
mInflatedBitmaps.clear();
mInflatedBitmaps.addAll(mBitmapsToKeep);
}
}
......@@ -12,6 +12,7 @@ import androidx.annotation.VisibleForTesting;
import org.chromium.base.Callback;
import org.chromium.base.UnguessableToken;
import org.chromium.base.task.SequencedTaskRunner;
import org.chromium.components.paintpreview.player.PlayerCompositorDelegate;
import java.util.HashSet;
......@@ -27,36 +28,43 @@ public class PlayerFrameBitmapState {
/** The scale factor of bitmaps. */
private float mScaleFactor;
/** Bitmaps that make up the contents. */
private Bitmap[][] mBitmapMatrix;
private CompressibleBitmap[][] mBitmapMatrix;
/** Whether a request for a bitmap tile is pending. */
private boolean[][] mPendingBitmapRequests;
private BitmapRequestHandler[][] mPendingBitmapRequests;
/**
* Whether we currently need a bitmap tile. This is used for deleting bitmaps that we don't
* need and freeing up memory.
*/
private boolean[][] mRequiredBitmaps;
/**
* Whether a bitmap is visible for a given request.
*/
private boolean[][] mVisibleBitmaps;
/** Delegate for accessing native to request bitmaps. */
private final PlayerCompositorDelegate mCompositorDelegate;
private final PlayerFrameBitmapStateController mStateController;
private Set<Integer> mInitialMissingVisibleBitmaps = new HashSet<>();
private final SequencedTaskRunner mTaskRunner;
PlayerFrameBitmapState(UnguessableToken guid, int tileWidth, int tileHeight, float scaleFactor,
Size contentSize, PlayerCompositorDelegate compositorDelegate,
PlayerFrameBitmapStateController stateController) {
PlayerFrameBitmapStateController stateController, SequencedTaskRunner taskRunner) {
mGuid = guid;
mTileSize = new Size(tileWidth, tileHeight);
mScaleFactor = scaleFactor;
mCompositorDelegate = compositorDelegate;
mStateController = stateController;
mTaskRunner = taskRunner;
// Each tile is as big as the initial view port. Here we determine the number of
// columns and rows for the current scale factor.
int rows = (int) Math.ceil((contentSize.getHeight() * scaleFactor) / tileHeight);
int cols = (int) Math.ceil((contentSize.getWidth() * scaleFactor) / tileWidth);
mBitmapMatrix = new Bitmap[rows][cols];
mPendingBitmapRequests = new boolean[rows][cols];
mBitmapMatrix = new CompressibleBitmap[rows][cols];
mPendingBitmapRequests = new BitmapRequestHandler[rows][cols];
mRequiredBitmaps = new boolean[rows][cols];
mVisibleBitmaps = new boolean[rows][cols];
}
@VisibleForTesting
......@@ -64,7 +72,7 @@ public class PlayerFrameBitmapState {
return mRequiredBitmaps;
}
Bitmap[][] getMatrix() {
CompressibleBitmap[][] getMatrix() {
return mBitmapMatrix;
}
......@@ -129,6 +137,7 @@ public class PlayerFrameBitmapState {
*/
void requestBitmapForRect(Rect viewportRect) {
if (mRequiredBitmaps == null || mBitmapMatrix == null) return;
clearVisibleBitmaps();
final int rowStart =
Math.max(0, (int) Math.floor((double) viewportRect.top / mTileSize.getHeight()));
......@@ -142,6 +151,7 @@ public class PlayerFrameBitmapState {
for (int col = colStart; col < colEnd; col++) {
for (int row = rowStart; row < rowEnd; row++) {
mVisibleBitmaps[row][col] = true;
if (requestBitmapForTile(row, col) && mInitialMissingVisibleBitmaps != null) {
mInitialMissingVisibleBitmaps.add(row * mBitmapMatrix.length + col);
}
......@@ -179,8 +189,12 @@ public class PlayerFrameBitmapState {
if (mRequiredBitmaps == null) return false;
mRequiredBitmaps[row][col] = true;
if (mPendingBitmapRequests != null && mPendingBitmapRequests[row][col] != null) {
mPendingBitmapRequests[row][col].setVisible(mVisibleBitmaps[row][col]);
return false;
}
if (mBitmapMatrix == null || mPendingBitmapRequests == null
|| mBitmapMatrix[row][col] != null || mPendingBitmapRequests[row][col]) {
|| mBitmapMatrix[row][col] != null || mPendingBitmapRequests[row][col] != null) {
return false;
}
......@@ -188,8 +202,8 @@ public class PlayerFrameBitmapState {
final int x = col * mTileSize.getWidth();
BitmapRequestHandler bitmapRequestHandler =
new BitmapRequestHandler(row, col, mScaleFactor);
mPendingBitmapRequests[row][col] = true;
new BitmapRequestHandler(row, col, mScaleFactor, mVisibleBitmaps[row][col]);
mPendingBitmapRequests[row][col] = bitmapRequestHandler;
mCompositorDelegate.requestBitmap(mGuid,
new Rect(x, y, x + mTileSize.getWidth(), y + mTileSize.getHeight()), mScaleFactor,
bitmapRequestHandler, bitmapRequestHandler::onError);
......@@ -205,9 +219,9 @@ public class PlayerFrameBitmapState {
for (int row = 0; row < mBitmapMatrix.length; row++) {
for (int col = 0; col < mBitmapMatrix[row].length; col++) {
Bitmap bitmap = mBitmapMatrix[row][col];
CompressibleBitmap bitmap = mBitmapMatrix[row][col];
if (!mRequiredBitmaps[row][col] && bitmap != null) {
bitmap.recycle();
bitmap.destroy();
mBitmapMatrix[row][col] = null;
}
}
......@@ -234,6 +248,16 @@ public class PlayerFrameBitmapState {
mStateController.stateUpdated(this);
}
private void clearVisibleBitmaps() {
if (mVisibleBitmaps == null) return;
for (int row = 0; row < mVisibleBitmaps.length; row++) {
for (int col = 0; col < mVisibleBitmaps[row].length; col++) {
mVisibleBitmaps[row][col] = false;
}
}
}
/**
* Used as the callback for bitmap requests from the Paint Preview compositor.
*/
......@@ -241,11 +265,18 @@ public class PlayerFrameBitmapState {
int mRequestRow;
int mRequestCol;
float mRequestScaleFactor;
boolean mVisible;
private BitmapRequestHandler(int requestRow, int requestCol, float requestScaleFactor) {
private BitmapRequestHandler(
int requestRow, int requestCol, float requestScaleFactor, boolean visible) {
mRequestRow = requestRow;
mRequestCol = requestCol;
mRequestScaleFactor = requestScaleFactor;
mVisible = visible;
}
private void setVisible(boolean visible) {
mVisible = visible;
}
/**
......@@ -259,18 +290,22 @@ public class PlayerFrameBitmapState {
return;
}
if (mBitmapMatrix == null || mPendingBitmapRequests == null || mRequiredBitmaps == null
|| !mPendingBitmapRequests[mRequestRow][mRequestCol]
|| mPendingBitmapRequests[mRequestRow][mRequestCol] == null
|| !mRequiredBitmaps[mRequestRow][mRequestCol]) {
markBitmapReceived(mRequestRow, mRequestCol);
result.recycle();
deleteUnrequiredBitmaps();
markBitmapReceived(mRequestRow, mRequestCol);
if (mPendingBitmapRequests != null) {
mPendingBitmapRequests[mRequestRow][mRequestCol] = null;
}
return;
}
mPendingBitmapRequests[mRequestRow][mRequestCol] = false;
mBitmapMatrix[mRequestRow][mRequestCol] = result;
markBitmapReceived(mRequestRow, mRequestCol);
mBitmapMatrix[mRequestRow][mRequestCol] =
new CompressibleBitmap(result, mTaskRunner, mVisible);
deleteUnrequiredBitmaps();
markBitmapReceived(mRequestRow, mRequestCol);
mPendingBitmapRequests[mRequestRow][mRequestCol] = null;
}
/**
......@@ -284,9 +319,9 @@ public class PlayerFrameBitmapState {
// TODO(crbug.com/1021590): Handle errors.
assert mBitmapMatrix != null;
assert mBitmapMatrix[mRequestRow][mRequestCol] == null;
assert mPendingBitmapRequests[mRequestRow][mRequestCol];
assert mPendingBitmapRequests[mRequestRow][mRequestCol] != null;
mPendingBitmapRequests[mRequestRow][mRequestCol] = false;
mPendingBitmapRequests[mRequestRow][mRequestCol] = null;
}
}
......
......@@ -9,6 +9,7 @@ import android.util.Size;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.UnguessableToken;
import org.chromium.base.task.SequencedTaskRunner;
import org.chromium.components.paintpreview.player.PlayerCompositorDelegate;
/**
......@@ -23,15 +24,17 @@ public class PlayerFrameBitmapStateController {
private final Size mContentSize;
private final PlayerCompositorDelegate mCompositorDelegate;
private final PlayerFrameMediatorDelegate mMediatorDelegate;
private final SequencedTaskRunner mTaskRunner;
PlayerFrameBitmapStateController(UnguessableToken guid, PlayerFrameViewport viewport,
Size contentSize, PlayerCompositorDelegate compositorDelegate,
PlayerFrameMediatorDelegate mediatorDelegate) {
PlayerFrameMediatorDelegate mediatorDelegate, SequencedTaskRunner taskRunner) {
mGuid = guid;
mViewport = viewport;
mContentSize = contentSize;
mCompositorDelegate = compositorDelegate;
mMediatorDelegate = mediatorDelegate;
mTaskRunner = taskRunner;
}
@VisibleForTesting
......@@ -50,9 +53,10 @@ public class PlayerFrameBitmapStateController {
(mLoadingBitmapState == null) ? mVisibleBitmapState : mLoadingBitmapState;
if (scaleUpdated || activeLoadingState == null) {
invalidateLoadingBitmaps();
mLoadingBitmapState = new PlayerFrameBitmapState(mGuid, mViewport.getWidth() / 2,
mViewport.getHeight() / 2, mViewport.getScale(), mContentSize,
mCompositorDelegate, this);
mLoadingBitmapState =
new PlayerFrameBitmapState(mGuid, Math.round(mViewport.getWidth() / 2.0f),
Math.round(mViewport.getHeight() / 2.0f), mViewport.getScale(),
mContentSize, mCompositorDelegate, this, mTaskRunner);
if (mVisibleBitmapState == null) {
mLoadingBitmapState.skipWaitingForVisibleBitmaps();
swap(mLoadingBitmapState);
......
......@@ -4,7 +4,6 @@
package org.chromium.components.paintpreview.player.frame;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.util.Size;
......@@ -13,6 +12,9 @@ import android.view.View;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.UnguessableToken;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.SequencedTaskRunner;
import org.chromium.base.task.TaskTraits;
import org.chromium.components.paintpreview.player.PlayerCompositorDelegate;
import org.chromium.components.paintpreview.player.PlayerGestureListener;
import org.chromium.ui.modelutil.PropertyModel;
......@@ -28,8 +30,8 @@ import java.util.List;
* <li>Maintaining a viewport {@link Rect} that represents the current user-visible section of this
* frame. The dimension of the viewport is constant and is equal to the initial values received on
* {@link #setLayoutDimensions}.</li>
* <li>Constructing a matrix of {@link Bitmap} tiles that represents the content of this frame for a
* given scale factor. Each tile is as big as the view port.</li>
* <li>Constructing a matrix of {@link CompressibleBitmap} tiles that represents the content of this
* frame for a given scale factor. Each tile is as big as the view port.</li>
* <li>Requesting bitmaps from Paint Preview compositor.</li>
* <li>Updating the viewport on touch gesture notifications (scrolling and scaling).<li/>
* <li>Determining which sub-frames are visible given the current viewport and showing them.<li/>
......@@ -85,8 +87,10 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate, PlayerFrameMediato
mInitialScaleFactor = 0f;
mGuid = frameGuid;
mContentSize = contentSize;
SequencedTaskRunner taskRunner =
PostTask.createSequencedTaskRunner(TaskTraits.USER_VISIBLE);
mBitmapStateController = new PlayerFrameBitmapStateController(
mGuid, mViewport, mContentSize, mCompositorDelegate, this);
mGuid, mViewport, mContentSize, mCompositorDelegate, this, taskRunner);
mViewport.offset(initialScrollX, initialScrollY);
mViewport.setScale(0f);
}
......@@ -271,7 +275,7 @@ class PlayerFrameMediator implements PlayerFrameViewDelegate, PlayerFrameMediato
}
@Override
public void updateBitmapMatrix(Bitmap[][] bitmapMatrix) {
public void updateBitmapMatrix(CompressibleBitmap[][] bitmapMatrix) {
mModel.set(PlayerFrameProperties.BITMAP_MATRIX, bitmapMatrix);
}
......
......@@ -4,7 +4,6 @@
package org.chromium.components.paintpreview.player.frame;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.util.Size;
......@@ -67,7 +66,7 @@ public interface PlayerFrameMediatorDelegate {
/**
* Updates the bitmap matrix in the model.
*/
void updateBitmapMatrix(Bitmap[][] bitmapMatrix);
void updateBitmapMatrix(CompressibleBitmap[][] bitmapMatrix);
/**
* Update the model when the bitmap state is swapped.
......
......@@ -4,7 +4,6 @@
package org.chromium.components.paintpreview.player.frame;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.util.Size;
......@@ -20,7 +19,7 @@ import java.util.List;
*/
class PlayerFrameProperties {
/** A matrix of bitmap tiles that collectively make the entire content. */
static final PropertyModel.WritableObjectPropertyKey<Bitmap[][]> BITMAP_MATRIX =
static final PropertyModel.WritableObjectPropertyKey<CompressibleBitmap[][]> BITMAP_MATRIX =
new PropertyModel.WritableObjectPropertyKey<>(true);
/** The dimensions of each bitmap tile in the current bitmap matrix. */
static final PropertyModel.WritableObjectPropertyKey<Size> TILE_DIMENSIONS =
......
......@@ -5,7 +5,6 @@
package org.chromium.components.paintpreview.player.frame;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
......@@ -45,7 +44,7 @@ class PlayerFrameView extends FrameLayout {
super(context);
setWillNotDraw(false);
mDelegate = playerFrameViewDelegate;
mBitmapPainter = new PlayerFrameBitmapPainter(this::invalidate, firstPaintListener);
mBitmapPainter = new PlayerFrameBitmapPainter(this::postInvalidate, firstPaintListener);
mGestureDetector =
new PlayerFrameGestureDetector(context, canDetectZoom, gestureDetectorDelegate);
}
......@@ -80,7 +79,7 @@ class PlayerFrameView extends FrameLayout {
layoutSubFrames();
}
void updateBitmapMatrix(Bitmap[][] bitmapMatrix) {
void updateBitmapMatrix(CompressibleBitmap[][] bitmapMatrix) {
mBitmapPainter.updateBitmapMatrix(bitmapMatrix);
}
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player.frame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.chromium.base.task.SequencedTaskRunner;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
import java.util.concurrent.TimeoutException;
/**
* Tests for the {@link CompressibleBitmap} class.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(shadows = {CompressibleBitmapTest.FakeShadowBitmapFactory.class})
public class CompressibleBitmapTest {
/**
* A fake {@link BitmapFactory} used to avoid native for decoding.
*/
@Implements(BitmapFactory.class)
public static class FakeShadowBitmapFactory {
private static Bitmap sBitmap;
public static void setBitmap(Bitmap bitmap) {
sBitmap = bitmap;
}
@Implementation
public static Bitmap decodeByteArray(byte[] array, int offset, int length) {
return sBitmap;
}
}
@Test
public void testCompressAndDiscard() {
Bitmap bitmap = Mockito.mock(Bitmap.class);
when(bitmap.compress(any(), anyInt(), any())).thenReturn(true);
SequencedTaskRunner taskRunner = Mockito.mock(SequencedTaskRunner.class);
doAnswer(invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
})
.when(taskRunner)
.postTask(any());
CompressibleBitmap compressibleBitmap = new CompressibleBitmap(bitmap, taskRunner, false);
verify(bitmap, times(1)).compress(any(), eq(100), any());
Assert.assertNull(compressibleBitmap.getBitmap());
}
@Test
public void testCompressAndKeep() {
Bitmap bitmap = Mockito.mock(Bitmap.class);
when(bitmap.compress(any(), anyInt(), any())).thenReturn(true);
SequencedTaskRunner taskRunner = Mockito.mock(SequencedTaskRunner.class);
doAnswer(invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
})
.when(taskRunner)
.postTask(any());
CompressibleBitmap compressibleBitmap = new CompressibleBitmap(bitmap, taskRunner, true);
verify(bitmap, times(1)).compress(any(), eq(100), any());
Assert.assertEquals(compressibleBitmap.getBitmap(), bitmap);
compressibleBitmap.discardBitmap();
Assert.assertNull(compressibleBitmap.getBitmap());
// Ensure doing this again doesn't crash.
compressibleBitmap.discardBitmap();
}
@Test
public void testNoDiscardIfCompressFails() {
Bitmap bitmap = Mockito.mock(Bitmap.class);
when(bitmap.compress(any(), anyInt(), any())).thenReturn(false);
SequencedTaskRunner taskRunner = Mockito.mock(SequencedTaskRunner.class);
doAnswer(invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
})
.when(taskRunner)
.postTask(any());
CompressibleBitmap compressibleBitmap = new CompressibleBitmap(bitmap, taskRunner, false);
verify(bitmap, times(1)).compress(any(), eq(100), any());
// Discarding should fail.
Assert.assertEquals(compressibleBitmap.getBitmap(), bitmap);
compressibleBitmap.discardBitmap();
Assert.assertEquals(compressibleBitmap.getBitmap(), bitmap);
}
@Test
public void testInflate() throws TimeoutException {
Bitmap bitmap = Mockito.mock(Bitmap.class);
when(bitmap.compress(any(), anyInt(), any())).thenReturn(true);
SequencedTaskRunner taskRunner = Mockito.mock(SequencedTaskRunner.class);
doAnswer(invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
})
.when(taskRunner)
.postTask(any());
CompressibleBitmap compressibleBitmap = new CompressibleBitmap(bitmap, taskRunner, false);
verify(bitmap, times(1)).compress(any(), eq(100), any());
Assert.assertNull(compressibleBitmap.getBitmap());
FakeShadowBitmapFactory.setBitmap(bitmap);
CallbackHelper helper = new CallbackHelper();
compressibleBitmap.inflateInBackground(compressible -> { helper.notifyCalled(); });
helper.waitForFirst();
Assert.assertEquals(compressibleBitmap.getBitmap(), bitmap);
compressibleBitmap.destroy();
Assert.assertNull(compressibleBitmap.getBitmap());
// Inflation should fail if the CompressibleBitmap is destroyed.
CallbackHelper inflatedNoBitmap = new CallbackHelper();
compressibleBitmap.inflateInBackground(compressible -> {
Assert.assertNull(compressible.getBitmap());
inflatedNoBitmap.notifyCalled();
});
inflatedNoBitmap.waitForFirst();
}
@Test
public void testLocking() throws TimeoutException {
Bitmap bitmap = Mockito.mock(Bitmap.class);
when(bitmap.compress(any(), anyInt(), any())).thenReturn(true);
SequencedTaskRunner taskRunner = Mockito.mock(SequencedTaskRunner.class);
doAnswer(invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
})
.when(taskRunner)
.postTask(any());
CompressibleBitmap compressibleBitmap = new CompressibleBitmap(bitmap, taskRunner, true);
verify(bitmap, times(1)).compress(any(), eq(100), any());
Assert.assertTrue(compressibleBitmap.lock());
Assert.assertFalse(compressibleBitmap.lock());
Assert.assertEquals(compressibleBitmap.getBitmap(), bitmap);
compressibleBitmap.discardBitmap();
Assert.assertEquals(compressibleBitmap.getBitmap(), bitmap);
compressibleBitmap.destroy();
Assert.assertEquals(compressibleBitmap.getBitmap(), bitmap);
Assert.assertTrue(compressibleBitmap.unlock());
Assert.assertFalse(compressibleBitmap.unlock());
compressibleBitmap.discardBitmap();
Assert.assertTrue(compressibleBitmap.lock());
Assert.assertNull(compressibleBitmap.getBitmap());
Assert.assertTrue(compressibleBitmap.unlock());
CallbackHelper helper = new CallbackHelper();
compressibleBitmap.inflateInBackground(compressible -> { helper.notifyCalled(); });
helper.waitForFirst();
compressibleBitmap.destroy();
Assert.assertTrue(compressibleBitmap.lock());
Assert.assertNull(compressibleBitmap.getBitmap());
Assert.assertTrue(compressibleBitmap.unlock());
verify(taskRunner, times(2)).postDelayedTask(any(), anyLong());
}
}
......@@ -4,10 +4,22 @@
package org.chromium.components.paintpreview.player.frame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Looper;
import android.util.Size;
import androidx.annotation.NonNull;
......@@ -16,19 +28,57 @@ import androidx.annotation.Nullable;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.chromium.base.task.SequencedTaskRunner;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Tests for the {@link PlayerFrameBitmapPainter} class.
*/
@RunWith(BaseRobolectricTestRunner.class)
@LooperMode(PAUSED)
@Config(shadows = {PlayerFrameBitmapPainterTest.FakeShadowBitmapFactory.class})
public class PlayerFrameBitmapPainterTest {
/**
* A fake {@link BitmapFactory} used to avoid native for decoding.
*/
@Implements(BitmapFactory.class)
public static class FakeShadowBitmapFactory {
private static Map<Integer, Bitmap> sBitmaps;
public static void setBitmaps(Map<Integer, Bitmap> bitmaps) {
sBitmaps = bitmaps;
}
@Implementation
public static Bitmap decodeByteArray(byte[] array, int offset, int length) {
return sBitmaps.get(fromByteArray(array));
}
}
static byte[] toByteArray(int value) {
return new byte[] {
(byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value)};
}
static int fromByteArray(byte[] bytes) {
return ((bytes[0] & 0xFF) << 24) | ((bytes[1] & 0xFF) << 16) | ((bytes[2] & 0xFF) << 8)
| ((bytes[3] & 0xFF));
}
/**
* Mocks {@link Canvas} and holds all calls to
* {@link Canvas#drawBitmap(Bitmap, Rect, Rect, Paint)}.
......@@ -83,11 +133,13 @@ public class PlayerFrameBitmapPainterTest {
}
}
private Bitmap[][] generateMockBitmapMatrix(int rows, int cols) {
Bitmap[][] matrix = new Bitmap[rows][cols];
private CompressibleBitmap[][] generateMockBitmapMatrix(int rows, int cols) {
CompressibleBitmap[][] matrix = new CompressibleBitmap[rows][cols];
for (int row = 0; row < matrix.length; ++row) {
for (int col = 0; col < matrix[row].length; ++col) {
matrix[row][col] = Mockito.mock(Bitmap.class);
matrix[row][col] = Mockito.mock(CompressibleBitmap.class);
when(matrix[row][col].getBitmap()).thenReturn(Mockito.mock(Bitmap.class));
when(matrix[row][col].lock()).thenReturn(true);
}
}
return matrix;
......@@ -122,7 +174,7 @@ public class PlayerFrameBitmapPainterTest {
public void testDrawFaultyBitmapMatrix() {
PlayerFrameBitmapPainter painter =
new PlayerFrameBitmapPainter(Mockito.mock(Runnable.class), null);
painter.updateBitmapMatrix(new Bitmap[0][0]);
painter.updateBitmapMatrix(new CompressibleBitmap[0][0]);
// This view port is covered by 2 bitmap tiles, so there should be 2 draw operations on
// the canvas.
painter.updateTileDimensions(new Size(10, 10));
......@@ -142,37 +194,171 @@ public class PlayerFrameBitmapPainterTest {
* coordinates, for the given view port.
*/
@Test
public void testDraw() {
public void testDrawNoDecode() {
Runnable invalidator = Mockito.mock(Runnable.class);
PlayerFrameBitmapPainter painter = new PlayerFrameBitmapPainter(invalidator, null);
// Prepare the bitmap matrix.
Bitmap[][] bitmaps = new Bitmap[2][2];
CompressibleBitmap[][] bitmaps = new CompressibleBitmap[2][2];
CompressibleBitmap compressibleBitmap00 = Mockito.mock(CompressibleBitmap.class);
CompressibleBitmap compressibleBitmap10 = Mockito.mock(CompressibleBitmap.class);
CompressibleBitmap compressibleBitmap01 = Mockito.mock(CompressibleBitmap.class);
CompressibleBitmap compressibleBitmap11 = Mockito.mock(CompressibleBitmap.class);
when(compressibleBitmap00.lock()).thenReturn(true);
when(compressibleBitmap10.lock()).thenReturn(true);
when(compressibleBitmap01.lock()).thenReturn(true);
when(compressibleBitmap11.lock()).thenReturn(true);
Bitmap bitmap00 = Mockito.mock(Bitmap.class);
Bitmap bitmap10 = Mockito.mock(Bitmap.class);
Bitmap bitmap01 = Mockito.mock(Bitmap.class);
Bitmap bitmap11 = Mockito.mock(Bitmap.class);
bitmaps[0][0] = bitmap00;
bitmaps[1][0] = bitmap10;
bitmaps[0][1] = bitmap01;
bitmaps[1][1] = bitmap11;
when(compressibleBitmap00.getBitmap()).thenReturn(bitmap00);
when(compressibleBitmap10.getBitmap()).thenReturn(bitmap10);
when(compressibleBitmap01.getBitmap()).thenReturn(bitmap01);
when(compressibleBitmap11.getBitmap()).thenReturn(bitmap11);
bitmaps[0][0] = compressibleBitmap00;
bitmaps[1][0] = compressibleBitmap10;
bitmaps[0][1] = compressibleBitmap01;
bitmaps[1][1] = compressibleBitmap11;
painter.updateBitmapMatrix(bitmaps);
painter.updateTileDimensions(new Size(10, 15));
painter.updateViewPort(5, 10, 15, 25);
// Make sure the invalidator was called after updating the bitmap matrix and the view port.
Mockito.verify(invalidator, Mockito.times(2)).run();
verify(invalidator, times(2)).run();
MockCanvas canvas = new MockCanvas();
painter.onDraw(canvas);
// Verify that the correct portions of each bitmap tiles is painted in the correct
// positions of in the canvas.
canvas.assertNumberOfBitmapDraws(4);
canvas.assertDrawBitmap(
compressibleBitmap00.getBitmap(), new Rect(5, 10, 10, 15), new Rect(0, 0, 5, 5));
canvas.assertDrawBitmap(
compressibleBitmap10.getBitmap(), new Rect(5, 0, 10, 10), new Rect(0, 5, 5, 15));
canvas.assertDrawBitmap(
compressibleBitmap01.getBitmap(), new Rect(0, 10, 5, 15), new Rect(5, 0, 10, 5));
canvas.assertDrawBitmap(
compressibleBitmap11.getBitmap(), new Rect(0, 0, 5, 10), new Rect(5, 5, 10, 15));
}
/**
* Verifies {@link PlayerFrameBitmapPainter#onDraw} draws the right bitmap tiles, at the correct
* coordinates, for the given view port with compression.
*/
@Test
public void testDrawWithDecode() {
Runnable invalidator = Mockito.mock(Runnable.class);
PlayerFrameBitmapPainter painter = new PlayerFrameBitmapPainter(invalidator, null);
SequencedTaskRunner taskRunner = Mockito.mock(SequencedTaskRunner.class);
doAnswer(invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
})
.when(taskRunner)
.postTask(any());
// Prepare the bitmap matrix.
Bitmap bitmap00 = Mockito.mock(Bitmap.class);
Bitmap bitmap10 = Mockito.mock(Bitmap.class);
Bitmap bitmap01 = Mockito.mock(Bitmap.class);
Bitmap bitmap11 = Mockito.mock(Bitmap.class);
Bitmap bitmap20 = Mockito.mock(Bitmap.class);
Bitmap bitmap21 = Mockito.mock(Bitmap.class);
Map<Integer, Bitmap> bitmapMap = new HashMap<>();
bitmapMap.put(bitmap00.hashCode(), bitmap00);
bitmapMap.put(bitmap10.hashCode(), bitmap10);
bitmapMap.put(bitmap01.hashCode(), bitmap01);
bitmapMap.put(bitmap11.hashCode(), bitmap11);
bitmapMap.put(bitmap20.hashCode(), bitmap20);
bitmapMap.put(bitmap21.hashCode(), bitmap21);
for (Bitmap bitmap : bitmapMap.values()) {
when(bitmap.compress(any(), anyInt(), any())).thenAnswer(invocation -> {
((ByteArrayOutputStream) invocation.getArgument(2))
.write(toByteArray(bitmap.hashCode()), 0, 4);
return true;
});
}
FakeShadowBitmapFactory.setBitmaps(bitmapMap);
CompressibleBitmap[][] bitmaps = new CompressibleBitmap[3][2];
CompressibleBitmap compressibleBitmap00 =
new CompressibleBitmap(bitmap00, taskRunner, false);
CompressibleBitmap compressibleBitmap10 =
new CompressibleBitmap(bitmap10, taskRunner, false);
CompressibleBitmap compressibleBitmap01 =
new CompressibleBitmap(bitmap01, taskRunner, false);
CompressibleBitmap compressibleBitmap11 =
new CompressibleBitmap(bitmap11, taskRunner, false);
CompressibleBitmap compressibleBitmap20 =
new CompressibleBitmap(bitmap20, taskRunner, false);
CompressibleBitmap compressibleBitmap21 =
new CompressibleBitmap(bitmap21, taskRunner, false);
bitmaps[0][0] = compressibleBitmap00;
bitmaps[1][0] = compressibleBitmap10;
bitmaps[0][1] = compressibleBitmap01;
bitmaps[1][1] = compressibleBitmap11;
bitmaps[2][0] = compressibleBitmap20;
bitmaps[2][1] = compressibleBitmap21;
painter.updateBitmapMatrix(bitmaps);
painter.updateTileDimensions(new Size(10, 15));
painter.updateViewPort(0, 0, 20, 30);
MockCanvas canvas = new MockCanvas();
// Make sure the invalidator was called after updating the bitmap matrix and the view port.
InOrder inOrder = inOrder(invalidator);
inOrder.verify(invalidator, times(2)).run();
// Draw once to force decode.
painter.onDraw(canvas);
shadowOf(Looper.getMainLooper()).idle();
inOrder.verify(invalidator, times(1)).run();
// Now draw everything.
painter.onDraw(canvas);
// Verify that the correct portions of each bitmap tiles is painted in the correct
// positions of in the canvas.
canvas.assertNumberOfBitmapDraws(4);
canvas.assertDrawBitmap(bitmap00, new Rect(5, 10, 10, 15), new Rect(0, 0, 5, 5));
canvas.assertDrawBitmap(bitmap10, new Rect(5, 0, 10, 10), new Rect(0, 5, 5, 15));
canvas.assertDrawBitmap(bitmap01, new Rect(0, 10, 5, 15), new Rect(5, 0, 10, 5));
canvas.assertDrawBitmap(bitmap11, new Rect(0, 0, 5, 10), new Rect(5, 5, 10, 15));
canvas.assertDrawBitmap(
compressibleBitmap00.getBitmap(), new Rect(0, 0, 10, 15), new Rect(0, 0, 10, 15));
canvas.assertDrawBitmap(
compressibleBitmap10.getBitmap(), new Rect(0, 0, 10, 15), new Rect(0, 15, 10, 30));
canvas.assertDrawBitmap(
compressibleBitmap01.getBitmap(), new Rect(0, 0, 10, 15), new Rect(10, 0, 20, 15));
canvas.assertDrawBitmap(
compressibleBitmap11.getBitmap(), new Rect(0, 0, 10, 15), new Rect(10, 15, 20, 30));
// Simulate scrolling to the bottom only half the tiles will be drawn until the others are
// inflated.
canvas = new MockCanvas();
painter.updateViewPort(0, 15, 20, 45);
inOrder.verify(invalidator, times(1)).run();
painter.onDraw(canvas);
shadowOf(Looper.getMainLooper()).idle();
inOrder.verify(invalidator, times(1)).run();
canvas.assertNumberOfBitmapDraws(2);
canvas.assertDrawBitmap(
compressibleBitmap10.getBitmap(), new Rect(0, 0, 10, 15), new Rect(0, 0, 10, 15));
canvas.assertDrawBitmap(
compressibleBitmap11.getBitmap(), new Rect(0, 0, 10, 15), new Rect(10, 0, 20, 15));
// Now that the bitmaps are decoded another draw can occur with all of them.
canvas = new MockCanvas();
painter.onDraw(canvas);
canvas.assertNumberOfBitmapDraws(4);
canvas.assertDrawBitmap(
compressibleBitmap10.getBitmap(), new Rect(0, 0, 10, 15), new Rect(0, 0, 10, 15));
canvas.assertDrawBitmap(
compressibleBitmap20.getBitmap(), new Rect(0, 0, 10, 15), new Rect(0, 15, 10, 30));
canvas.assertDrawBitmap(
compressibleBitmap11.getBitmap(), new Rect(0, 0, 10, 15), new Rect(10, 0, 20, 15));
canvas.assertDrawBitmap(
compressibleBitmap21.getBitmap(), new Rect(0, 0, 10, 15), new Rect(10, 15, 20, 30));
Assert.assertNull(compressibleBitmap00.getBitmap());
Assert.assertNull(compressibleBitmap01.getBitmap());
}
/**
......@@ -188,8 +374,10 @@ public class PlayerFrameBitmapPainterTest {
MockCanvas canvas = new MockCanvas();
// Prepare the bitmap matrix.
Bitmap[][] bitmaps = new Bitmap[1][1];
bitmaps[0][0] = Mockito.mock(Bitmap.class);
CompressibleBitmap[][] bitmaps = new CompressibleBitmap[1][1];
bitmaps[0][0] = Mockito.mock(CompressibleBitmap.class);
when(bitmaps[0][0].getBitmap()).thenReturn(Mockito.mock(Bitmap.class));
when(bitmaps[0][0].lock()).thenReturn(true);
painter.updateBitmapMatrix(bitmaps);
painter.updateTileDimensions(new Size(10, 15));
......
......@@ -36,6 +36,7 @@ import org.robolectric.shadows.ShadowView;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.UnguessableToken;
import org.chromium.base.task.SequencedTaskRunner;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.components.paintpreview.player.PlayerCompositorDelegate;
import org.chromium.components.paintpreview.player.PlayerGestureListener;
......@@ -270,8 +271,8 @@ public class PlayerFrameMediatorTest {
// The bitmap matrix should be empty, but initialized with the correct number of rows and
// columns. Because we set the initial scale factor to view port width over content width,
// we should have only one column.
Bitmap[][] bitmapMatrix = mModel.get(PlayerFrameProperties.BITMAP_MATRIX);
Assert.assertTrue(Arrays.deepEquals(bitmapMatrix, new Bitmap[4][2]));
CompressibleBitmap[][] bitmapMatrix = mModel.get(PlayerFrameProperties.BITMAP_MATRIX);
Assert.assertTrue(Arrays.deepEquals(bitmapMatrix, new CompressibleBitmap[4][2]));
Assert.assertEquals(new ArrayList<Pair<View, Rect>>(),
mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS));
}
......@@ -558,27 +559,64 @@ public class PlayerFrameMediatorTest {
Bitmap bitmap03 = Mockito.mock(Bitmap.class);
Bitmap bitmap13 = Mockito.mock(Bitmap.class);
Bitmap bitmap23 = Mockito.mock(Bitmap.class);
Bitmap[][] expectedBitmapMatrix = new Bitmap[12][8];
expectedBitmapMatrix[0][0] = bitmap00;
expectedBitmapMatrix[0][1] = bitmap01;
expectedBitmapMatrix[0][2] = bitmap02;
expectedBitmapMatrix[1][0] = bitmap10;
expectedBitmapMatrix[1][1] = bitmap11;
expectedBitmapMatrix[1][2] = bitmap12;
expectedBitmapMatrix[2][0] = bitmap20;
expectedBitmapMatrix[2][1] = bitmap21;
SequencedTaskRunner mockTaskRunner = Mockito.mock(SequencedTaskRunner.class);
CompressibleBitmap compressibleBitmap00 =
new CompressibleBitmap(bitmap00, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap10 =
new CompressibleBitmap(bitmap10, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap20 =
new CompressibleBitmap(bitmap20, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap01 =
new CompressibleBitmap(bitmap01, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap11 =
new CompressibleBitmap(bitmap11, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap21 =
new CompressibleBitmap(bitmap21, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap31 =
new CompressibleBitmap(bitmap31, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap02 =
new CompressibleBitmap(bitmap02, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap12 =
new CompressibleBitmap(bitmap12, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap22 =
new CompressibleBitmap(bitmap22, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap32 =
new CompressibleBitmap(bitmap32, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap03 =
new CompressibleBitmap(bitmap03, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap13 =
new CompressibleBitmap(bitmap13, mockTaskRunner, true);
CompressibleBitmap compressibleBitmap23 =
new CompressibleBitmap(bitmap23, mockTaskRunner, true);
CompressibleBitmap[][] expectedBitmapMatrix = new CompressibleBitmap[12][8];
expectedBitmapMatrix[0][0] = compressibleBitmap00;
expectedBitmapMatrix[0][1] = compressibleBitmap01;
expectedBitmapMatrix[0][2] = compressibleBitmap02;
expectedBitmapMatrix[1][0] = compressibleBitmap10;
expectedBitmapMatrix[1][1] = compressibleBitmap11;
expectedBitmapMatrix[1][2] = compressibleBitmap12;
expectedBitmapMatrix[2][0] = compressibleBitmap20;
expectedBitmapMatrix[2][1] = compressibleBitmap21;
// Call the request callback with mock bitmaps and assert they're added to the model.
mCompositorDelegate.mRequestedBitmap.get(0).mBitmapCallback.onResult(bitmap00);
mCompositorDelegate.mRequestedBitmap.get(1).mBitmapCallback.onResult(bitmap10);
mCompositorDelegate.mRequestedBitmap.get(2).mBitmapCallback.onResult(bitmap01);
mCompositorDelegate.mRequestedBitmap.get(3).mBitmapCallback.onResult(bitmap11);
mCompositorDelegate.mRequestedBitmap.get(4).mBitmapCallback.onResult(bitmap20);
mCompositorDelegate.mRequestedBitmap.get(5).mBitmapCallback.onResult(bitmap02);
mCompositorDelegate.mRequestedBitmap.get(6).mBitmapCallback.onResult(bitmap21);
mCompositorDelegate.mRequestedBitmap.get(7).mBitmapCallback.onResult(bitmap12);
Bitmap[][] mat = mModel.get(PlayerFrameProperties.BITMAP_MATRIX);
mCompositorDelegate.mRequestedBitmap.get(0).mBitmapCallback.onResult(
compressibleBitmap00.getBitmap());
mCompositorDelegate.mRequestedBitmap.get(1).mBitmapCallback.onResult(
compressibleBitmap10.getBitmap());
mCompositorDelegate.mRequestedBitmap.get(2).mBitmapCallback.onResult(
compressibleBitmap01.getBitmap());
mCompositorDelegate.mRequestedBitmap.get(3).mBitmapCallback.onResult(
compressibleBitmap11.getBitmap());
mCompositorDelegate.mRequestedBitmap.get(4).mBitmapCallback.onResult(
compressibleBitmap20.getBitmap());
mCompositorDelegate.mRequestedBitmap.get(5).mBitmapCallback.onResult(
compressibleBitmap02.getBitmap());
mCompositorDelegate.mRequestedBitmap.get(6).mBitmapCallback.onResult(
compressibleBitmap21.getBitmap());
mCompositorDelegate.mRequestedBitmap.get(7).mBitmapCallback.onResult(
compressibleBitmap12.getBitmap());
CompressibleBitmap[][] mat = mModel.get(PlayerFrameProperties.BITMAP_MATRIX);
Assert.assertTrue(Arrays.deepEquals(
expectedBitmapMatrix, mModel.get(PlayerFrameProperties.BITMAP_MATRIX)));
......@@ -589,20 +627,26 @@ public class PlayerFrameMediatorTest {
// tiles. See comments on {@link #testBitmapRequest} for details on which tiles will be
// requested.
// Call the request callback with mock bitmaps and assert they're added to the model.
expectedBitmapMatrix[2][2] = bitmap22;
expectedBitmapMatrix[0][3] = bitmap03;
expectedBitmapMatrix[3][1] = bitmap31;
expectedBitmapMatrix[1][3] = bitmap13;
expectedBitmapMatrix[3][2] = bitmap32;
expectedBitmapMatrix[2][3] = bitmap23;
mCompositorDelegate.mRequestedBitmap.get(8).mBitmapCallback.onResult(bitmap22);
expectedBitmapMatrix[2][2] = compressibleBitmap22;
expectedBitmapMatrix[0][3] = compressibleBitmap03;
expectedBitmapMatrix[3][1] = compressibleBitmap31;
expectedBitmapMatrix[1][3] = compressibleBitmap13;
expectedBitmapMatrix[3][2] = compressibleBitmap32;
expectedBitmapMatrix[2][3] = compressibleBitmap23;
mCompositorDelegate.mRequestedBitmap.get(8).mBitmapCallback.onResult(
compressibleBitmap22.getBitmap());
// Mock a compositing failure for this tile. No bitmaps should be added.
mCompositorDelegate.mRequestedBitmap.get(9).mErrorCallback.run();
mCompositorDelegate.mRequestedBitmap.get(10).mBitmapCallback.onResult(bitmap31);
mCompositorDelegate.mRequestedBitmap.get(11).mBitmapCallback.onResult(bitmap03);
mCompositorDelegate.mRequestedBitmap.get(12).mBitmapCallback.onResult(bitmap13);
mCompositorDelegate.mRequestedBitmap.get(13).mBitmapCallback.onResult(bitmap32);
mCompositorDelegate.mRequestedBitmap.get(14).mBitmapCallback.onResult(bitmap23);
mCompositorDelegate.mRequestedBitmap.get(10).mBitmapCallback.onResult(
compressibleBitmap31.getBitmap());
mCompositorDelegate.mRequestedBitmap.get(11).mBitmapCallback.onResult(
compressibleBitmap03.getBitmap());
mCompositorDelegate.mRequestedBitmap.get(12).mBitmapCallback.onResult(
compressibleBitmap13.getBitmap());
mCompositorDelegate.mRequestedBitmap.get(13).mBitmapCallback.onResult(
compressibleBitmap32.getBitmap());
mCompositorDelegate.mRequestedBitmap.get(14).mBitmapCallback.onResult(
compressibleBitmap23.getBitmap());
Assert.assertTrue(Arrays.deepEquals(
expectedBitmapMatrix, mModel.get(PlayerFrameProperties.BITMAP_MATRIX)));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment