blob: 902f00e2aff3b59904d782bd102f83d8707df2c9 [file] [log] [blame]
/*
* Copyright (C) 2016 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 android.view.cts;
import android.app.Instrumentation;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Color;
import android.graphics.SurfaceTexture;
import android.opengl.GLSurfaceView;
import android.opengl.GLSurfaceView.Renderer;
import android.os.Debug;
import android.os.Debug.MemoryInfo;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.filters.MediumTest;
import android.support.test.rule.ActivityTestRule;
import android.util.Log;
import android.view.PixelCopy;
import android.view.Surface;
import android.view.SurfaceView;
import android.view.PixelCopy.OnPixelCopyFinishedListener;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import static android.opengl.GLES20.GL_COLOR_BUFFER_BIT;
import static android.opengl.GLES20.GL_SCISSOR_TEST;
import static android.opengl.GLES20.glClear;
import static android.opengl.GLES20.glClearColor;
import static android.opengl.GLES20.glEnable;
import static android.opengl.GLES20.glScissor;
import static org.junit.Assert.*;
@MediumTest
public class PixelCopyTests {
private static final String TAG = "PixelCopyTests";
@Rule
public ActivityTestRule<GLSurfaceViewCtsActivity> mGLSurfaceViewActivityRule =
new ActivityTestRule<>(GLSurfaceViewCtsActivity.class, false, false);
@Rule
public ActivityTestRule<PixelCopyVideoSourceActivity> mVideoSourceActivityRule =
new ActivityTestRule<>(PixelCopyVideoSourceActivity.class, false, false);
private Instrumentation mInstrumentation;
@Before
public void setUp() throws Exception {
mInstrumentation = InstrumentationRegistry.getInstrumentation();
assertNotNull(mInstrumentation);
}
@Test
public void testErrors() {
Bitmap dest = null;
SynchronousPixelCopy copyHelper = new SynchronousPixelCopy();
SurfaceTexture surfaceTexture = null;
Surface surface = null;
try {
surfaceTexture = new SurfaceTexture(false);
surface = new Surface(surfaceTexture);
try {
copyHelper.request(surface, dest);
fail("Should have generated an IllegalArgumentException, null dest!");
} catch (IllegalArgumentException iae) {
// success!
} catch (Throwable t) {
throw new AssertionError("Should have generated an IllegalArgumentException!", t);
}
dest = Bitmap.createBitmap(5, 5, Bitmap.Config.ARGB_8888);
int result = copyHelper.request(surface, dest);
assertEquals(PixelCopy.ERROR_SOURCE_NO_DATA, result);
dest.recycle();
try {
copyHelper.request(surface, dest);
fail("Should have generated an IllegalArgumentException!");
} catch (IllegalArgumentException iae) {
// success!
} catch (Throwable t) {
throw new AssertionError(
"Should have generated an IllegalArgumentException, recycled bitmap!", t);
}
} finally {
try {
if (surface != null) surface.release();
} catch (Throwable t) {}
try {
if (surfaceTexture != null) surfaceTexture.release();
} catch (Throwable t) {}
surface = null;
surfaceTexture = null;
}
}
@Test
public void testGlProducer() {
try {
CountDownLatch swapFence = new CountDownLatch(2);
GLSurfaceViewCtsActivity.setGlVersion(2);
GLSurfaceViewCtsActivity.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
GLSurfaceViewCtsActivity.setFixedSize(100, 100);
GLSurfaceViewCtsActivity.setRenderer(new QuadColorGLRenderer(
Color.RED, Color.GREEN, Color.BLUE, Color.BLACK, swapFence));
GLSurfaceViewCtsActivity activity =
mGLSurfaceViewActivityRule.launchActivity(null);
while (!swapFence.await(5, TimeUnit.MILLISECONDS)) {
activity.getView().requestRender();
}
// Test a fullsize copy
Bitmap bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
SynchronousPixelCopy copyHelper = new SynchronousPixelCopy();
int result = copyHelper.request(activity.getView(), bitmap);
assertEquals("Fullsize copy request failed", PixelCopy.SUCCESS, result);
// Make sure nothing messed with the bitmap
assertEquals(100, bitmap.getWidth());
assertEquals(100, bitmap.getHeight());
assertEquals(Config.ARGB_8888, bitmap.getConfig());
assertBitmapQuadColor(bitmap,
Color.RED, Color.GREEN, Color.BLUE, Color.BLACK);
// Test that scaling works
// Since we only sample mid-pixel of each qudrant, filtering
// quality isn't tested
bitmap.reconfigure(20, 20, Config.ARGB_8888);
result = copyHelper.request(activity.getView(), bitmap);
assertEquals("Scaled copy request failed", PixelCopy.SUCCESS, result);
// Make sure nothing messed with the bitmap
assertEquals(20, bitmap.getWidth());
assertEquals(20, bitmap.getHeight());
assertEquals(Config.ARGB_8888, bitmap.getConfig());
assertBitmapQuadColor(bitmap,
Color.RED, Color.GREEN, Color.BLUE, Color.BLACK);
} catch (InterruptedException e) {
fail("Interrupted, error=" + e.getMessage());
} finally {
GLSurfaceViewCtsActivity.resetFixedSize();
GLSurfaceViewCtsActivity.resetGlVersion();
GLSurfaceViewCtsActivity.resetRenderer();
GLSurfaceViewCtsActivity.resetRenderMode();
}
}
private void assertNotLeaking(int iteration, MemoryInfo start, MemoryInfo end) {
Debug.getMemoryInfo(end);
if (Math.abs(start.getTotalPss() - end.getTotalPss()) > 2000 /* kB */) {
System.gc();
System.gc();
Debug.getMemoryInfo(end);
if (Math.abs(start.getTotalPss() - end.getTotalPss()) > 2000 /* kB */) {
// Guarded by if so we don't continually generate garbage for the
// assertion string.
assertEquals("Memory leaked, iteration=" + iteration,
start.getTotalPss(), end.getTotalPss(),
2000 /* kb */);
}
}
}
@Test
@LargeTest
public void testNotLeaking() {
try {
CountDownLatch swapFence = new CountDownLatch(2);
GLSurfaceViewCtsActivity.setGlVersion(2);
GLSurfaceViewCtsActivity.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
GLSurfaceViewCtsActivity.setFixedSize(100, 100);
GLSurfaceViewCtsActivity.setRenderer(new QuadColorGLRenderer(
Color.RED, Color.GREEN, Color.BLUE, Color.BLACK, swapFence));
GLSurfaceViewCtsActivity activity =
mGLSurfaceViewActivityRule.launchActivity(null);
while (!swapFence.await(5, TimeUnit.MILLISECONDS)) {
activity.getView().requestRender();
}
// Test a fullsize copy
Bitmap bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
SynchronousPixelCopy copyHelper = new SynchronousPixelCopy();
MemoryInfo meminfoStart = new MemoryInfo();
MemoryInfo meminfoEnd = new MemoryInfo();
for (int i = 0; i < 1000; i++) {
if (i == 2) {
// Not really the "start" but by having done a couple
// we've fully initialized any state that may be required,
// so memory usage should be stable now
System.gc();
System.gc();
Debug.getMemoryInfo(meminfoEnd);
Debug.getMemoryInfo(meminfoStart);
}
if (i % 100 == 5) {
assertNotLeaking(i, meminfoStart, meminfoEnd);
}
int result = copyHelper.request(activity.getView(), bitmap);
assertEquals("Copy request failed", PixelCopy.SUCCESS, result);
// Make sure nothing messed with the bitmap
assertEquals(100, bitmap.getWidth());
assertEquals(100, bitmap.getHeight());
assertEquals(Config.ARGB_8888, bitmap.getConfig());
assertBitmapQuadColor(bitmap,
Color.RED, Color.GREEN, Color.BLUE, Color.BLACK);
}
assertNotLeaking(1000, meminfoStart, meminfoEnd);
} catch (InterruptedException e) {
fail("Interrupted, error=" + e.getMessage());
}
}
@Test
public void testVideoProducer() throws InterruptedException {
PixelCopyVideoSourceActivity activity =
mVideoSourceActivityRule.launchActivity(null);
if (!activity.canPlayVideo()) {
Log.i(TAG, "Skipping testVideoProducer, video codec isn't supported");
return;
}
// This returns when the video has been prepared and playback has
// been started, it doesn't necessarily means a frame has actually been
// produced. There sadly isn't a callback for that.
// So we'll try for up to 900ms after this event to acquire a frame, otherwise
// it's considered a timeout.
activity.waitForPlaying();
assertTrue("Failed to start video playback", activity.canPlayVideo());
SynchronousPixelCopy copyHelper = new SynchronousPixelCopy();
Bitmap bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
int copyResult = PixelCopy.ERROR_SOURCE_NO_DATA;
for (int i = 0; i < 30; i++) {
copyResult = copyHelper.request(activity.getVideoView(), bitmap);
if (copyResult != PixelCopy.ERROR_SOURCE_NO_DATA) {
break;
}
Thread.sleep(30);
}
assertEquals(PixelCopy.SUCCESS, copyResult);
// A large threshold is used because decoder accuracy is covered in the
// media CTS tests, so we are mainly interested in verifying that rotation
// and YUV->RGB conversion were handled properly.
assertBitmapQuadColor(bitmap, Color.RED, Color.GREEN, Color.BLUE, Color.BLACK, 30);
}
private static int getPixelFloatPos(Bitmap bitmap, float xpos, float ypos) {
return bitmap.getPixel((int) (bitmap.getWidth() * xpos), (int) (bitmap.getHeight() * ypos));
}
private void assertBitmapQuadColor(Bitmap bitmap, int topLeft, int topRight,
int bottomLeft, int bottomRight) {
// Just quickly sample 4 pixels in the various regions.
assertEquals("Top left", topLeft, getPixelFloatPos(bitmap, .25f, .25f));
assertEquals("Top right", topRight, getPixelFloatPos(bitmap, .75f, .25f));
assertEquals("Bottom left", bottomLeft, getPixelFloatPos(bitmap, .25f, .75f));
assertEquals("Bottom right", bottomRight, getPixelFloatPos(bitmap, .75f, .75f));
}
private void assertBitmapQuadColor(Bitmap bitmap, int topLeft, int topRight,
int bottomLeft, int bottomRight, int threshold) {
// Just quickly sample 4 pixels in the various regions.
assertTrue("Top left", pixelsAreSame(topLeft, getPixelFloatPos(bitmap, .25f, .25f), threshold));
assertTrue("Top right", pixelsAreSame(topRight, getPixelFloatPos(bitmap, .75f, .25f), threshold));
assertTrue("Bottom left", pixelsAreSame(bottomLeft, getPixelFloatPos(bitmap, .25f, .75f), threshold));
assertTrue("Bottom right", pixelsAreSame(bottomRight, getPixelFloatPos(bitmap, .75f, .75f), threshold));
}
private boolean pixelsAreSame(int ideal, int given, int threshold) {
int error = Math.abs(Color.red(ideal) - Color.red(given));
error += Math.abs(Color.green(ideal) - Color.green(given));
error += Math.abs(Color.blue(ideal) - Color.blue(given));
return (error < threshold);
}
private static class QuadColorGLRenderer implements Renderer {
private final int mTopLeftColor;
private final int mTopRightColor;
private final int mBottomLeftColor;
private final int mBottomRightColor;
private final CountDownLatch mFence;
private int mWidth, mHeight;
public QuadColorGLRenderer(int topLeft, int topRight,
int bottomLeft, int bottomRight, CountDownLatch fence) {
mTopLeftColor = topLeft;
mTopRightColor = topRight;
mBottomLeftColor = bottomLeft;
mBottomRightColor = bottomRight;
mFence = fence;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
mWidth = width;
mHeight = height;
}
@Override
public void onDrawFrame(GL10 gl) {
int cx = mWidth / 2;
int cy = mHeight / 2;
glEnable(GL_SCISSOR_TEST);
glScissor(0, cy, cx, mHeight - cy);
clearColor(mTopLeftColor);
glScissor(cx, cy, mWidth - cx, mHeight - cy);
clearColor(mTopRightColor);
glScissor(0, 0, cx, cy);
clearColor(mBottomLeftColor);
glScissor(cx, 0, mWidth - cx, cy);
clearColor(mBottomRightColor);
mFence.countDown();
}
private void clearColor(int color) {
glClearColor(Color.red(color) / 255.0f,
Color.green(color) / 255.0f,
Color.blue(color) / 255.0f,
Color.alpha(color) / 255.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
}
private static class SynchronousPixelCopy implements OnPixelCopyFinishedListener {
private static Handler sHandler;
static {
HandlerThread thread = new HandlerThread("PixelCopyHelper");
thread.start();
sHandler = new Handler(thread.getLooper());
}
private int mStatus = -1;
public int request(Surface source, Bitmap dest) {
synchronized (this) {
PixelCopy.request(source, dest, this, sHandler);
return getResultLocked();
}
}
public int request(SurfaceView source, Bitmap dest) {
synchronized (this) {
PixelCopy.request(source, dest, this, sHandler);
return getResultLocked();
}
}
private int getResultLocked() {
try {
this.wait(1000);
} catch (InterruptedException e) {
fail("PixelCopy request didn't complete within 1s");
}
return mStatus;
}
@Override
public void onPixelCopyFinished(int copyResult) {
synchronized (this) {
mStatus = copyResult;
this.notify();
}
}
}
}