Add Camera + SurfaceTexture tests into CTS.

Tests basic functionality for setting a SurfaceTexture as the camera
preview destination.

Change-Id: I42181ce2955a438a22967e849409d24d4e9ed9d1
diff --git a/tests/src/android/opengl/cts/GLSurfaceViewStubActivity.java b/tests/src/android/opengl/cts/GLSurfaceViewStubActivity.java
index 5a8f310..0644913 100644
--- a/tests/src/android/opengl/cts/GLSurfaceViewStubActivity.java
+++ b/tests/src/android/opengl/cts/GLSurfaceViewStubActivity.java
@@ -25,6 +25,7 @@
 
 /**
  * A minimal activity for testing {@link android.opengl.GLSurfaceView}.
+ * Also accepts non-blank renderers to allow its use for more complex tests.
  */
 public class GLSurfaceViewStubActivity extends Activity {
 
@@ -45,11 +46,61 @@
 
     private GLSurfaceView mView;
 
+    /** To override the blank renderer, or other settings, these
+     * static set* methods must be called before onCreate() is called.
+     * If using ActivityInstrumentationTestCase2, that means the set
+     * methods need to be called before calling getActivity in the
+     * test setUp().
+     */
+    private static GLSurfaceView.Renderer mRenderer = null;
+    public static void setRenderer(GLSurfaceView.Renderer renderer) {
+        mRenderer = renderer;
+    }
+    public static void resetRenderer() {
+        mRenderer = null;
+    }
+
+
+    private static int mRenderMode = 0;
+    private static boolean mRenderModeSet = false;
+    public static void setRenderMode(int renderMode) {
+        mRenderModeSet = true;
+        mRenderMode = renderMode;
+    }
+    public static void resetRenderMode() {
+        mRenderModeSet = false;
+        mRenderMode = 0;
+    }
+
+    private static int mGlVersion = 0;
+    private static boolean mGlVersionSet = false;
+    public static void setGlVersion(int glVersion) {
+        mGlVersionSet = true;
+        mGlVersion = glVersion;
+    }
+    public static void resetGlVersion() {
+        mGlVersionSet = false;
+        mGlVersion = 0;
+    }
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mView = new GLSurfaceView(this);
-        mView.setRenderer(new Renderer());
+        // Only set this if explicitly asked for
+        if (mGlVersionSet) {
+            mView.setEGLContextClientVersion(mGlVersion);
+        }
+        // Use no-op renderer by default
+        if (mRenderer == null) {
+            mView.setRenderer(new Renderer());
+        } else {
+            mView.setRenderer(mRenderer);
+        }
+        // Only set this if explicitly asked for
+        if (mRenderModeSet) {
+            mView.setRenderMode(mRenderMode);
+        }
         setContentView(mView);
     }
 
diff --git a/tests/tests/hardware/src/android/hardware/cts/CameraGLTest.java b/tests/tests/hardware/src/android/hardware/cts/CameraGLTest.java
new file mode 100644
index 0000000..6f6494f
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/CameraGLTest.java
@@ -0,0 +1,751 @@
+/*
+ * Copyright (C) 2011 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.hardware.cts;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+
+import android.opengl.GLES20;
+import android.opengl.GLSurfaceView;
+import android.opengl.Matrix;
+import android.opengl.cts.GLSurfaceViewStubActivity;
+
+import android.os.ConditionVariable;
+import android.os.Environment;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.MoreAsserts;
+import android.test.UiThreadTest;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import dalvik.annotation.TestLevel;
+import dalvik.annotation.TestTargetClass;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * This test case must run with hardware. It can't be tested in emulator
+ */
+@LargeTest
+@TestTargetClass(Camera.class)
+public class CameraGLTest extends ActivityInstrumentationTestCase2<GLSurfaceViewStubActivity> {
+    private static final String TAG = "CameraGLTest";
+    private static final String PACKAGE = "com.android.cts.stub";
+    private static final boolean LOGV = false;
+    private static final int EGL_OPENGL_ES2_BIT = 0x0004;
+
+    private boolean mSurfaceTextureCallbackResult = false;
+
+    private static final int WAIT_FOR_COMMAND_TO_COMPLETE = 1500;  // Milliseconds.
+    private static final int WAIT_FOR_FOCUS_TO_COMPLETE = 3000;
+    private static final int WAIT_FOR_SNAPSHOT_TO_COMPLETE = 5000;
+
+    private SurfaceTextureCallback mSurfaceTextureCallback = new SurfaceTextureCallback();
+    private SurfaceTextureBurstCallback mSurfaceTextureBurstCallback = new SurfaceTextureBurstCallback();
+    private PreviewCallback mPreviewCallback = new PreviewCallback();
+
+    private Looper mLooper = null;
+    private final ConditionVariable mSurfaceTextureDone = new ConditionVariable();
+    private final ConditionVariable mPreviewDone = new ConditionVariable();
+
+    Camera mCamera;
+    SurfaceTexture mSurfaceTexture;
+    Renderer mRenderer;
+    GLSurfaceView mGLView;
+
+    public CameraGLTest() {
+        super(PACKAGE, GLSurfaceViewStubActivity.class);
+        if (LOGV) Log.v(TAG, "CameraGLTest Constructor");
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        // Set up renderer instance
+        mRenderer = this.new Renderer();
+        GLSurfaceViewStubActivity.setRenderer(mRenderer);
+        GLSurfaceViewStubActivity.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+        GLSurfaceViewStubActivity.setGlVersion(2);
+        // Start CameraStubActivity.
+        GLSurfaceViewStubActivity stubActivity = getActivity();
+        // Store a link to the view so we can redraw it when needed
+        mGLView = stubActivity.getView();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (mCamera != null) {
+            mCamera.release();
+            mCamera = null;
+        }
+        // Clean up static values in stub so it can be reused
+        GLSurfaceViewStubActivity.resetRenderMode();
+        GLSurfaceViewStubActivity.resetRenderer();
+        GLSurfaceViewStubActivity.resetGlVersion();
+
+        super.tearDown();
+    }
+
+    /**
+     * Initializes the message looper so that the Camera object can
+     * receive the callback messages.
+     */
+    private void initializeMessageLooper(final int cameraId) {
+        final ConditionVariable startDone = new ConditionVariable();
+        new Thread() {
+            @Override
+            public void run() {
+                Log.v(TAG, "Start camera/surfacetexture thread");
+                // Set up a looper to be used by camera.
+                Looper.prepare();
+                // Save the looper so that we can terminate this thread
+                // after we are done with it.
+                mLooper = Looper.myLooper();
+                // These must be instantiated outside the UI thread, since the
+                // UI thread will be doing a lot of waiting, stopping callbacks.
+                mCamera = Camera.open(cameraId);
+                mSurfaceTexture = new SurfaceTexture(mRenderer.getTextureID());
+                Log.v(TAG, "Camera " + cameraId + " is opened.");
+                startDone.open();
+                Looper.loop(); // Blocks forever until Looper.quit() is called.
+                Log.v(TAG, "Stop camera/surfacetexture thread");
+            }
+        }.start();
+
+        Log.v(TAG, "start waiting for looper");
+        if (!startDone.block(WAIT_FOR_COMMAND_TO_COMPLETE)) {
+            Log.v(TAG, "initializeMessageLooper: start timeout");
+            fail("initializeMessageLooper: start timeout");
+        }
+    }
+
+    /**
+     * Terminates the message looper thread.
+     */
+    private void terminateMessageLooper() throws Exception {
+        mCamera.release();
+        mLooper.quit();
+        // Looper.quit() is asynchronous. The looper may still has some
+        // preview callbacks in the queue after quit is called. The preview
+        // callback still uses the camera object (setHasPreviewCallback).
+        // After camera is released, RuntimeException will be thrown from
+        // the method. So we need to join the looper thread here.
+        mLooper.getThread().join();
+        mCamera = null;
+        mSurfaceTexture = null;
+    }
+
+    /** The camera preview callback. Stops capture after the first callback */
+    private final class PreviewCallback
+            implements android.hardware.Camera.PreviewCallback {
+        public void onPreviewFrame(byte [] data, Camera camera) {
+            assertNotNull(data);
+            Size size = camera.getParameters().getPreviewSize();
+            assertEquals(size.width * size.height * 3 / 2, data.length);
+            mCamera.stopPreview();
+            if (LOGV) Log.v(TAG, "notify the preview callback");
+            mPreviewDone.open();
+            if (LOGV) Log.v(TAG, "Preview callback stop");
+        }
+    }
+
+    /** A simple SurfaceTexture listener callback, meant to be used together with the camera preview
+     * callback */
+    private final class SurfaceTextureCallback
+            implements android.graphics.SurfaceTexture.OnFrameAvailableListener {
+        public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+            if (LOGV) Log.v(TAG, "SurfaceTextureCallback");
+            mSurfaceTextureDone.open();
+            // Assumes preview is stopped elsewhere
+        }
+    }
+
+    /** A burst SurfaceTexture listener callback, used for multiple-frame capture */
+    private final class SurfaceTextureBurstCallback
+            implements android.graphics.SurfaceTexture.OnFrameAvailableListener {
+
+        public void setBurstCount(int burstCount) {
+            mBurstCount = burstCount;
+        }
+
+        public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+            if (LOGV) Log.v(TAG, "SurfaceTextureBurstCallback");
+            mBurstCount = mBurstCount-1;
+            if (!mSurfaceTextureCallbackResult) {
+                if (mBurstCount <= 0) {
+                    if (LOGV) Log.v(TAG, "SurfaceTextureBurstCallback: Stopping preview");
+                    mCamera.stopPreview();
+                    if (LOGV) Log.v(TAG, "SurfaceTextureBurstCallback: Preview stopped");
+                    mSurfaceTextureCallbackResult = true;
+                }
+                mSurfaceTextureDone.open();
+            }
+        }
+
+        private int mBurstCount = 0;
+    }
+
+    /** Waits until surface texture callbacks have fired */
+    private boolean waitForSurfaceTextureDone() {
+        if (LOGV) Log.v(TAG, "Wait for surface texture callback");
+        if (!mSurfaceTextureDone.block(WAIT_FOR_COMMAND_TO_COMPLETE)) {
+            // timeout could be expected or unexpected. The caller will decide.
+            Log.v(TAG, "waitForSurfaceTextureDone: timeout");
+            return false;
+        }
+        mSurfaceTextureDone.close();
+        return true;
+    }
+
+    /** Waits until the camera preview callback has fired */
+    private boolean waitForPreviewDone() {
+        if (LOGV) Log.v(TAG, "Wait for preview callback");
+        if (!mPreviewDone.block(WAIT_FOR_COMMAND_TO_COMPLETE)) {
+            // timeout could be expected or unexpected. The caller will decide.
+            Log.v(TAG, "waitForPreviewDone: timeout");
+            return false;
+        }
+        mPreviewDone.close();
+        return true;
+    }
+
+    /** @return OpenGL ES major version 1 or 2 or some negative number for error */
+    private static int getDetectedVersion() {
+        /*
+         * Get all the device configurations and check if any of the attributes specify the
+         * the EGL_OPENGL_ES2_BIT to determine whether the device supports 2.0.
+         */
+        EGL10 egl = (EGL10) EGLContext.getEGL();
+        EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+        int[] numConfigs = new int[1];
+
+        if (egl.eglInitialize(display, null)) {
+            try {
+                if (egl.eglGetConfigs(display, null, 0, numConfigs)) {
+                    EGLConfig[] configs = new EGLConfig[numConfigs[0]];
+                    if (egl.eglGetConfigs(display, configs, numConfigs[0], numConfigs)) {
+                        int[] value = new int[1];
+                        for (int i = 0; i < numConfigs[0]; i++) {
+                            if (egl.eglGetConfigAttrib(display, configs[i],
+                                    EGL10.EGL_RENDERABLE_TYPE, value)) {
+                                if ((value[0] & EGL_OPENGL_ES2_BIT) == EGL_OPENGL_ES2_BIT) {
+                                    return 2;
+                                }
+                            } else {
+                                Log.w(TAG, "Getting config attribute with "
+                                        + "EGL10#eglGetConfigAttrib failed "
+                                        + "(" + i + "/" + numConfigs[0] + "): "
+                                        + egl.eglGetError());
+                            }
+                        }
+                        return 1;
+                    } else {
+                        Log.e(TAG, "Getting configs with EGL10#eglGetConfigs failed: "
+                                + egl.eglGetError());
+                        return -1;
+                    }
+                } else {
+                    Log.e(TAG, "Getting number of configs with EGL10#eglGetConfigs failed: "
+                            + egl.eglGetError());
+                    return -2;
+                }
+              } finally {
+                  egl.eglTerminate(display);
+              }
+        } else {
+            Log.e(TAG, "Couldn't initialize EGL.");
+            return -3;
+        }
+    }
+
+    /** Generic per-camera test interface */
+    private interface RunPerCamera {
+        void run(int cameraId) throws Exception;
+    }
+
+    /** Generic camera test runner, to minimize boilerplace duplication */
+    private void runForAllCameras(RunPerCamera test) throws Exception {
+        /* Currently OpenGL ES 2.0 is supported for this test, so just skip it
+           if only 1.0 is available. */
+        int glVersion = getDetectedVersion();
+        assertTrue(glVersion > 0);
+        if (glVersion != 2) {
+            Log.w(TAG, "Skipping test because OpenGL ES 2 is not supported");
+            return;
+        }
+
+        /* Make sure the screen stays on while testing - otherwise the OpenGL context may disappear */
+        PowerManager pm = (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
+        PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "CameraGLTest");
+        wl.acquire();
+        try {
+            /* Run the requested test per camera */
+            int nCameras = Camera.getNumberOfCameras();
+            for (int id = 0; id < nCameras; id++) {
+                Log.v(TAG, "Camera id=" + id);
+                test.run(id);
+            }
+        } finally {
+            wl.release();
+        }
+    }
+
+    /** Test Camera.setPreviewTexture in conjunction with the standard Camera preview callback */
+    @UiThreadTest
+    public void testSetPreviewTexturePreviewCallback() throws Exception {
+        runForAllCameras(testSetPreviewTexturePreviewCallbackByCamera);
+    }
+
+    private RunPerCamera testSetPreviewTexturePreviewCallbackByCamera = new RunPerCamera() {
+        public void run(int cameraId) throws Exception {
+            boolean noTimeout;
+            // Check the order: startPreview->setPreviewTexture, with a PreviewCallback as well
+            mPreviewDone.close();
+            initializeMessageLooper(cameraId);
+            mCamera.setPreviewCallback(mPreviewCallback);
+            mCamera.startPreview();
+            mCamera.setPreviewTexture(mSurfaceTexture);
+            noTimeout = waitForPreviewDone();
+            assertTrue(noTimeout);
+            terminateMessageLooper();
+
+            // Check the order: setPreviewTexture->startPreview.
+            initializeMessageLooper(cameraId);
+            mCamera.setPreviewCallback(mPreviewCallback);
+            mCamera.setPreviewTexture(mSurfaceTexture);
+            mCamera.startPreview();
+            noTimeout = waitForPreviewDone();
+            assertTrue(noTimeout);
+
+            // Check the order: setting preview display to null->startPreview->
+            // setPreviewTexture.
+            mCamera.setPreviewCallback(mPreviewCallback);
+            mCamera.setPreviewTexture(null);
+            mCamera.startPreview();
+            mCamera.setPreviewTexture(mSurfaceTexture);
+            noTimeout = waitForPreviewDone();
+            assertTrue(noTimeout);
+            terminateMessageLooper();
+        }
+    };
+
+    /** Test Camera.setPreviewTexture in conjunction with both the standard Camera preview callback,
+        and the SurfaceTexture onFrameAvailable callback */
+    @UiThreadTest
+    public void testSetPreviewTextureBothCallbacks() throws Exception {
+        runForAllCameras(testSetPreviewTextureBothCallbacksByCamera);
+    }
+
+    private RunPerCamera testSetPreviewTextureBothCallbacksByCamera = new RunPerCamera() {
+        public void run(int cameraId) throws Exception {
+            boolean noTimeout;
+            // Check SurfaceTexture callback together with preview callback
+            // Check the order: setPreviewTexture->startPreview
+            mSurfaceTextureDone.close();
+            initializeMessageLooper(cameraId);
+            mRenderer.setCameraSizing(mCamera.getParameters().getPreviewSize());
+            mCamera.setPreviewCallback(mPreviewCallback);
+            mSurfaceTexture.setOnFrameAvailableListener(mSurfaceTextureCallback);
+            mCamera.setPreviewTexture(mSurfaceTexture);
+            mCamera.startPreview();
+
+            noTimeout = waitForSurfaceTextureDone();
+            assertTrue(noTimeout);
+            noTimeout = waitForPreviewDone();
+            assertTrue(noTimeout);
+
+            mGLView.requestRender();
+            terminateMessageLooper();
+
+            // Check the order: startPreview->setPreviewTexture
+            mSurfaceTextureDone.close();
+            initializeMessageLooper(cameraId);
+            mRenderer.setCameraSizing(mCamera.getParameters().getPreviewSize());
+            mCamera.setPreviewCallback(mPreviewCallback);
+            mSurfaceTexture.setOnFrameAvailableListener(mSurfaceTextureCallback);
+            mCamera.startPreview();
+            mCamera.setPreviewTexture(mSurfaceTexture);
+
+            noTimeout = waitForSurfaceTextureDone();
+            assertTrue(noTimeout);
+            noTimeout = waitForPreviewDone();
+            assertTrue(noTimeout);
+
+            mGLView.requestRender();
+
+            // Check the order: setting preview to null->startPreview->setPreviewTexture
+            mCamera.setPreviewCallback(mPreviewCallback);
+            mCamera.setPreviewTexture(null);
+            mSurfaceTexture.setOnFrameAvailableListener(mSurfaceTextureCallback);
+            mCamera.startPreview();
+            mCamera.setPreviewTexture(mSurfaceTexture);
+            noTimeout = waitForPreviewDone();
+            assertTrue(noTimeout);
+            terminateMessageLooper();
+        }
+    };
+
+    /** Test Camera.setPreviewTexture in conjunction with just the SurfaceTexture onFrameAvailable callback */
+    @UiThreadTest
+    public void testSetPreviewTextureTextureCallback() throws Exception {
+        runForAllCameras(testSetPreviewTextureTextureCallbackByCamera);
+    }
+
+    private RunPerCamera testSetPreviewTextureTextureCallbackByCamera = new RunPerCamera() {
+        public void run(int cameraId) throws Exception {
+            boolean noTimeout;
+            // Check that SurfaceTexture callbacks work with no standard
+            // preview callback
+            mSurfaceTextureCallbackResult = false;
+            mSurfaceTextureDone.close();
+            initializeMessageLooper(cameraId);
+            mSurfaceTextureBurstCallback.setBurstCount(1);
+            mSurfaceTexture.setOnFrameAvailableListener(mSurfaceTextureBurstCallback);
+            mCamera.setPreviewTexture(mSurfaceTexture);
+            mRenderer.setCameraSizing(mCamera.getParameters().getPreviewSize());
+            mCamera.startPreview();
+
+            noTimeout = waitForSurfaceTextureDone();
+            mGLView.requestRender();
+            assertTrue(noTimeout);
+
+            terminateMessageLooper();
+            assertTrue(mSurfaceTextureCallbackResult);
+
+            // Check that SurfaceTexture callbacks also work with
+            // startPreview->setPreviewTexture
+            mSurfaceTextureCallbackResult = false;
+            mSurfaceTextureDone.close();
+            initializeMessageLooper(cameraId);
+            mSurfaceTextureBurstCallback.setBurstCount(1);
+            mSurfaceTexture.setOnFrameAvailableListener(mSurfaceTextureBurstCallback);
+            mRenderer.setCameraSizing(mCamera.getParameters().getPreviewSize());
+            mCamera.startPreview();
+            mCamera.setPreviewTexture(mSurfaceTexture);
+
+            noTimeout = waitForSurfaceTextureDone();
+            assertTrue(noTimeout);
+
+            terminateMessageLooper();
+            assertTrue(mSurfaceTextureCallbackResult);
+
+            // Check that SurfaceTexture callbacks also work with
+            // null->startPreview->setPreviewTexture
+            mSurfaceTextureCallbackResult = false;
+            mSurfaceTextureDone.close();
+            initializeMessageLooper(cameraId);
+            mSurfaceTextureBurstCallback.setBurstCount(1);
+            mSurfaceTexture.setOnFrameAvailableListener(mSurfaceTextureBurstCallback);
+            mRenderer.setCameraSizing(mCamera.getParameters().getPreviewSize());
+            mCamera.setPreviewTexture(null);
+            mCamera.startPreview();
+            mCamera.setPreviewTexture(mSurfaceTexture);
+
+            noTimeout = waitForSurfaceTextureDone();
+            assertTrue(noTimeout);
+
+            terminateMessageLooper();
+            assertTrue(mSurfaceTextureCallbackResult);
+        }
+    };
+
+    /** Basic OpenGL ES 2.0 renderer to draw SurfaceTexture-sourced frames to the screen */
+    private class Renderer implements GLSurfaceView.Renderer {
+        public Renderer() {
+            mTriangleVertices =
+                    ByteBuffer.allocateDirect(mTriangleVerticesData.length * FLOAT_SIZE_BYTES).
+                    order(ByteOrder.nativeOrder()).asFloatBuffer();
+            mTriangleVertices.put(mTriangleVerticesData).position(0);
+
+            Matrix.setIdentityM(mSTMatrix, 0);
+            Matrix.setIdentityM(mMMatrix, 0);
+
+            mTextureID = 0;
+        }
+
+        public void setCameraSizing(Camera.Size previewSize) {
+            mCameraRatio = (float)previewSize.width/previewSize.height;
+        }
+
+        public boolean waitForDrawDone() {
+            if (!mDrawDone.block(WAIT_FOR_COMMAND_TO_COMPLETE) ) {
+                // timeout could be expected or unexpected. The caller will decide.
+                Log.v(TAG, "waitForSurfaceTextureDone: timeout");
+                return false;
+            }
+            mDrawDone.close();
+            return true;
+        }
+
+        private final ConditionVariable mDrawDone = new ConditionVariable();
+
+        public void onDrawFrame(GL10 glUnused) {
+            if (LOGV) Log.v(TAG, "onDrawFrame()");
+            if (CameraGLTest.this.mSurfaceTexture != null) {
+                CameraGLTest.this.mSurfaceTexture.updateTexImage();
+                CameraGLTest.this.mSurfaceTexture.getTransformMatrix(mSTMatrix);
+                mDrawDone.open();
+            }
+
+            // Ignore the passed-in GL10 interface, and use the GLES20
+            // class's static methods instead.
+            GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
+            GLES20.glUseProgram(mProgram);
+            checkGlError("glUseProgram");
+
+            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+            GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, mTextureID);
+
+            mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET);
+            GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false,
+                                         TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices);
+            checkGlError("glVertexAttribPointer maPosition");
+            GLES20.glEnableVertexAttribArray(maPositionHandle);
+            checkGlError("glEnableVertexAttribArray maPositionHandle");
+
+            mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET);
+            GLES20.glVertexAttribPointer(maTextureHandle, 3, GLES20.GL_FLOAT, false,
+                                         TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices);
+            checkGlError("glVertexAttribPointer maTextureHandle");
+            GLES20.glEnableVertexAttribArray(maTextureHandle);
+            checkGlError("glEnableVertexAttribArray maTextureHandle");
+
+            Matrix.multiplyMM(mMVPMatrix, 0, mVMatrix, 0, mMMatrix, 0);
+            Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mMVPMatrix, 0);
+
+            GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0);
+            GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0);
+            GLES20.glUniform1f(muCRatioHandle, mCameraRatio);
+
+            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+            checkGlError("glDrawArrays");
+        }
+
+        public void onSurfaceChanged(GL10 glUnused, int width, int height) {
+            if (LOGV) Log.v(TAG, "onSurfaceChanged()");
+            // Ignore the passed-in GL10 interface, and use the GLES20
+            // class's static methods instead.
+            GLES20.glViewport(0, 0, width, height);
+            mRatio = (float) width / height;
+            Matrix.frustumM(mProjMatrix, 0, -mRatio, mRatio, -1, 1, 3, 7);
+        }
+
+        public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
+            if (LOGV) Log.v(TAG, "onSurfaceCreated()");
+            // Ignore the passed-in GL10 interface, and use the GLES20
+            // class's static methods instead.
+
+            /* Set up shaders and handles to their variables */
+            mProgram = createProgram(mVertexShader, mFragmentShader);
+            if (mProgram == 0) {
+                return;
+            }
+            maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
+            checkGlError("glGetAttribLocation aPosition");
+            if (maPositionHandle == -1) {
+                throw new RuntimeException("Could not get attrib location for aPosition");
+            }
+            maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord");
+            checkGlError("glGetAttribLocation aTextureCoord");
+            if (maTextureHandle == -1) {
+                throw new RuntimeException("Could not get attrib location for aTextureCoord");
+            }
+
+            muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
+            checkGlError("glGetUniformLocation uMVPMatrix");
+            if (muMVPMatrixHandle == -1) {
+                throw new RuntimeException("Could not get attrib location for uMVPMatrix");
+            }
+
+            muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix");
+            checkGlError("glGetUniformLocation uSTMatrix");
+            if (muMVPMatrixHandle == -1) {
+                throw new RuntimeException("Could not get attrib location for uSTMatrix");
+            }
+
+            muCRatioHandle = GLES20.glGetUniformLocation(mProgram, "uCRatio");
+            checkGlError("glGetUniformLocation uCRatio");
+            if (muMVPMatrixHandle == -1) {
+                throw new RuntimeException("Could not get attrib location for uCRatio");
+            }
+
+            /*
+             * Create our texture. This has to be done each time the
+             * surface is created.
+             */
+
+            int[] textures = new int[1];
+            GLES20.glGenTextures(1, textures, 0);
+
+            mTextureID = textures[0];
+            GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, mTextureID);
+            checkGlError("glBindTexture mTextureID");
+
+            // Can't do mipmapping with camera source
+            GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
+                                   GLES20.GL_NEAREST);
+            GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
+                                   GLES20.GL_LINEAR);
+            // Clamp to edge is the only option
+            GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
+                                   GLES20.GL_CLAMP_TO_EDGE);
+            GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
+                                   GLES20.GL_CLAMP_TO_EDGE);
+            checkGlError("glTexParameteri mTextureID");
+
+            Matrix.setLookAtM(mVMatrix, 0, 0, 0, 4f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
+        }
+
+        public int getTextureID() {
+            return mTextureID;
+        }
+
+        private int loadShader(int shaderType, String source) {
+            int shader = GLES20.glCreateShader(shaderType);
+            if (shader != 0) {
+                GLES20.glShaderSource(shader, source);
+                GLES20.glCompileShader(shader);
+                int[] compiled = new int[1];
+                GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
+                if (compiled[0] == 0) {
+                    Log.e(TAG, "Could not compile shader " + shaderType + ":");
+                    Log.e(TAG, GLES20.glGetShaderInfoLog(shader));
+                    GLES20.glDeleteShader(shader);
+                    shader = 0;
+                }
+            }
+            return shader;
+        }
+
+        private int createProgram(String vertexSource, String fragmentSource) {
+            int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
+            if (vertexShader == 0) {
+                return 0;
+            }
+            int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
+            if (pixelShader == 0) {
+                return 0;
+            }
+
+            int program = GLES20.glCreateProgram();
+            if (program != 0) {
+                GLES20.glAttachShader(program, vertexShader);
+                checkGlError("glAttachShader");
+                GLES20.glAttachShader(program, pixelShader);
+                checkGlError("glAttachShader");
+                GLES20.glLinkProgram(program);
+                int[] linkStatus = new int[1];
+                GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
+                if (linkStatus[0] != GLES20.GL_TRUE) {
+                    Log.e(TAG, "Could not link program: ");
+                    Log.e(TAG, GLES20.glGetProgramInfoLog(program));
+                    GLES20.glDeleteProgram(program);
+                    program = 0;
+                }
+            }
+            return program;
+        }
+
+        private void checkGlError(String op) {
+            int error;
+            while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
+                Log.e(TAG, op + ": glError " + error);
+                throw new RuntimeException(op + ": glError " + error);
+            }
+        }
+
+        private static final int FLOAT_SIZE_BYTES = 4;
+        private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES;
+        private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0;
+        private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3;
+        private final float[] mTriangleVerticesData = {
+            // X, Y, Z, U, V
+            -1.0f, -1.0f, 0, 0.f, 0.f,
+            1.0f, -1.0f, 0, 1.f, 0.f,
+            -1.0f,  1.0f, 0, 0.f, 1.f,
+            1.0f,   1.0f, 0, 1.f, 1.f,
+        };
+
+        private FloatBuffer mTriangleVertices;
+
+        private final String mVertexShader =
+                "uniform mat4 uMVPMatrix;\n" +
+                "uniform mat4 uSTMatrix;\n" +
+                "uniform float uCRatio;\n" +
+                "attribute vec4 aPosition;\n" +
+                "attribute vec4 aTextureCoord;\n" +
+                "varying vec2 vTextureCoord;\n" +
+                "void main() {\n" +
+                "  gl_Position = vec4(uCRatio,1,1,1) * uMVPMatrix * aPosition;\n" +
+                "  vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" +
+                "}\n";
+
+        private final String mFragmentShader =
+                "#extension GL_OES_EGL_image_external : require\n" +
+                "precision mediump float;\n" +
+                "varying vec2 vTextureCoord;\n" +
+                "uniform samplerExternalOES sTexture;\n" +
+                "void main() {\n" +
+                "  gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
+                "}\n";
+
+        private float[] mMVPMatrix = new float[16];
+        private float[] mProjMatrix = new float[16];
+        private float[] mMMatrix = new float[16];
+        private float[] mVMatrix = new float[16];
+        private float[] mSTMatrix = new float[16];
+
+        private int mProgram;
+        private int mTextureID;
+        private int muMVPMatrixHandle;
+        private int muSTMatrixHandle;
+        private int muCRatioHandle;
+        private int maPositionHandle;
+        private int maTextureHandle;
+
+        private float mRatio = 1.0f;
+        private float mCameraRatio = 1.0f;
+
+        private Context mContext;
+        private static final String TAG = "CameraGLTest.Renderer";
+
+        // Magic key
+        private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;
+    }
+
+}