| /* |
| * 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(); |
| } |
| } |
| } |
| } |