Convert Colors from GLSurfaceView to TextureView.

(At great personal cost.)

This fixes the super-janky startup for Colors.

Bug: 7171323
Change-Id: If9103eaccdb3202ff94bafea7874437d0f4ddcb4
diff --git a/src/com/android/dreams/basic/Colors.java b/src/com/android/dreams/basic/Colors.java
index e4e22bd..021189f 100644
--- a/src/com/android/dreams/basic/Colors.java
+++ b/src/com/android/dreams/basic/Colors.java
@@ -16,53 +16,114 @@
 
 package com.android.dreams.basic;
 
-import android.animation.Animator;
-import android.opengl.GLSurfaceView;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
-import android.graphics.drawable.Drawable;
 import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.net.Uri;
-import android.os.BatteryManager;
-import android.os.Handler;
-import android.provider.Settings;
+import android.graphics.SurfaceTexture;
 import android.service.dreams.Dream;
 import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.WindowManager;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.DecelerateInterpolator;
-import android.webkit.WebSettings;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.widget.TextView;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.view.TextureView;
+import android.os.Looper;
 import android.os.SystemClock;
 
+import javax.microedition.khronos.egl.EGL10;
 import javax.microedition.khronos.egl.EGLConfig;
-import javax.microedition.khronos.opengles.GL10;
-
-import android.opengl.GLES20;
-import android.opengl.GLSurfaceView;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+import javax.microedition.khronos.opengles.GL;
+import android.opengl.GLUtils;
 
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.FloatBuffer;
 import java.nio.ShortBuffer;
 
-public class Colors extends Dream {
+import static android.opengl.GLES20.*;
+
+public class Colors extends Dream implements TextureView.SurfaceTextureListener {
+    static final String TAG = Colors.class.getSimpleName();
+    static final boolean DEBUG = true;
+    public static final void LOG(String fmt, Object... args) {
+        if (!DEBUG) return;
+        Log.v(TAG, String.format(fmt, args));
+    }
+
     // It's so easy to use OpenGLES 2.0!
-    GLSurfaceView gl;
+    private EGL10 mEgl;
+    private EGLDisplay mEglDisplay;
+    private EGLConfig mEglConfig;
+    private EGLContext mEglContext;
+    private EGLSurface mEglSurface;
+    private SurfaceTexture mSurface;
+    private GL mGL;
+
+    static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+    static final int EGL_OPENGL_ES2_BIT = 4;
+
+    volatile boolean mStop = false;
+
     Square mSquare;
+    TextureView mTextureView;
+
+    private long mLastFrameTime;
+    private int mFrameNum = 0;
+
+    private FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
+        @Override
+        public void doFrame(long frameTimeNanos) {
+            if (mStop) {
+                Looper.myLooper().quit();
+                return;
+            }
+
+            if (mSquare == null) {
+                initGL();
+
+                mSquare = new Square();
+                glClearColor(1f, 0f, 0f, 1.0f);
+
+                if (DEBUG) {
+                    mLastFrameTime = frameTimeNanos;
+                }
+            }
+
+            checkCurrent();
+
+            glViewport(0, 0, mWidth, mHeight);
+
+            if (DEBUG) {
+                mFrameNum ++;
+                final long t2 = frameTimeNanos;
+                final long dt = t2-mLastFrameTime;
+                final int fps = (int) (1e9f/dt);
+                if (0 == (mFrameNum % 10)) {
+                    LOG("frame %d fps=%d", mFrameNum, fps);
+                }
+                if (fps < 40) {
+                    LOG("JANK! (%d ms)", dt);
+                }
+                mLastFrameTime = t2;
+            }
+
+            glClear(GL_COLOR_BUFFER_BIT);
+            checkGlError();
+
+            mSquare.draw();
+
+            if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
+                throw new RuntimeException("Cannot swap buffers");
+            }
+            checkEglError();
+
+            mChoreographer.postFrameCallback(mFrameCallback);
+        }
+    };
+
+    private int mHeight;
+
+    private int mWidth;
+    private Choreographer mChoreographer;
 
     class Square {
         // Straight from the API guide
@@ -99,20 +160,20 @@
                                   1f,  1f, 0f }; // top right
 
         private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices (CCW)
-        
+
         private final float HUES[] = { // reverse order due to CCW winding
                 60,  // yellow
                 120, // green
                 343, // red
                 200, // blue
         };
-        
+
         private final int vertexCount = squareCoords.length / COORDS_PER_VERTEX;
         private final int vertexStride = COORDS_PER_VERTEX * 4; // bytes per vertex
 
         private float cornerFrequencies[] = new float[vertexCount];
         private int cornerRotation;
-        
+
         final int COLOR_PLANES_PER_VERTEX = 4;
         private final int colorStride = COLOR_PLANES_PER_VERTEX * 4; // bytes per vertex
 
@@ -121,7 +182,7 @@
 
         public Square() {
             for (int i=0; i<vertexCount; i++) {
-                cornerFrequencies[i] = 1f + (float)(Math.random() * 5); 
+                cornerFrequencies[i] = 1f + (float)(Math.random() * 5);
             }
             cornerRotation = (int)(Math.random() * vertexCount);
             // initialize vertex byte buffer for shape coordinates
@@ -132,7 +193,7 @@
             vertexBuffer = bb.asFloatBuffer();
             vertexBuffer.put(squareCoords);
             vertexBuffer.position(0);
-            
+
             bb = ByteBuffer.allocateDirect(vertexCount * colorStride);
             bb.order(ByteOrder.nativeOrder());
             colorBuffer = bb.asFloatBuffer();
@@ -146,39 +207,36 @@
             drawListBuffer.put(drawOrder);
             drawListBuffer.position(0);
 
-            // prepare shaders and OpenGL program
-            int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER,
-                                                       vertexShaderCode);
-            int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,
-                                                         fragmentShaderCode);
+            mProgram = buildProgram(vertexShaderCode, fragmentShaderCode);
 
-            mProgram = GLES20.glCreateProgram();             // create empty OpenGL Program
-            GLES20.glAttachShader(mProgram, vertexShader);   // add the vertex shader to program
-            GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
-            GLES20.glLinkProgram(mProgram);                  // create OpenGL program executables
+            // Add program to OpenGL environment
+            glUseProgram(mProgram);
+            checkGlError("glUseProgram(" + mProgram + ")");
+
+            // get handle to vertex shader's a_position member
+            mPositionHandle = glGetAttribLocation(mProgram, "a_position");
+            checkGlError("glGetAttribLocation(a_position)");
+
+            // Enable a handle to the triangle vertices
+            glEnableVertexAttribArray(mPositionHandle);
+
+            // Prepare the triangle coordinate data
+            glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
+                    GL_FLOAT, false,
+                    vertexStride, vertexBuffer);
+
+            mColorHandle = glGetAttribLocation(mProgram, "a_color");
+            checkGlError("glGetAttribLocation(a_color)");
+            glEnableVertexAttribArray(mColorHandle);
+            checkGlError("glEnableVertexAttribArray");
         }
 
         final float[] _tmphsv = new float[3];
         public void draw() {
-            // Add program to OpenGL environment
-            GLES20.glUseProgram(mProgram);
-
-            // get handle to vertex shader's a_position member
-            mPositionHandle = GLES20.glGetAttribLocation(mProgram, "a_position");
-
-            // Enable a handle to the triangle vertices
-            GLES20.glEnableVertexAttribArray(mPositionHandle);
-
-            // Prepare the triangle coordinate data
-            GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
-                                         GLES20.GL_FLOAT, false,
-                                         vertexStride, vertexBuffer);
-
             // same thing for colors
             long now = SystemClock.uptimeMillis();
             colorBuffer.clear();
-            final float t = (float)now / 4000f; // set the base period to 4sec
-//            android.util.Slog.v("Colors", "t=" + t);
+            final float t = now / 4000f; // set the base period to 4sec
             for(int i=0; i<vertexCount; i++) {
                 final float freq = (float) Math.sin(2 * Math.PI * t / cornerFrequencies[i]);
                 _tmphsv[0] = HUES[(i + cornerRotation) % vertexCount];
@@ -191,102 +249,240 @@
                 colorBuffer.put(/*a*/ 1f);
             }
             colorBuffer.position(0);
-            mColorHandle = GLES20.glGetAttribLocation(mProgram, "a_color");
-            checkGlError("glGetAttribLocation");
-            GLES20.glEnableVertexAttribArray(mColorHandle);
-            checkGlError("glEnableVertexAttribArray");
-            GLES20.glVertexAttribPointer(mColorHandle, COLOR_PLANES_PER_VERTEX,
-                    GLES20.GL_FLOAT, false,
+            glVertexAttribPointer(mColorHandle, COLOR_PLANES_PER_VERTEX,
+                    GL_FLOAT, false,
                     colorStride, colorBuffer);
             checkGlError("glVertexAttribPointer");
 
             // Draw the triangle
-            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vertexCount);
-
-            // Disable vertex array
-            GLES20.glDisableVertexAttribArray(mPositionHandle);
-            GLES20.glDisableVertexAttribArray(mColorHandle);
-        }
-
-        public int loadShader(int type, String shaderCode){
-
-            // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
-            // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
-            int shader = GLES20.glCreateShader(type);
-
-            // add the source code to the shader and compile it
-            GLES20.glShaderSource(shader, shaderCode);
-            GLES20.glCompileShader(shader);
-
-            return shader;
-        }
-
-        /**
-         * Utility method for debugging OpenGL calls. Provide the name of the call
-         * just after making it:
-         *
-         * <pre>
-         * mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
-         * MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
-         *
-         * If the operation is not successful, the check throws an error.
-         *
-         * @param glOperation - Name of the OpenGL call to check.
-         */
-        public void checkGlError(String glOperation) {
-            int error;
-            while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
-                Log.e("GL", glOperation + ": glError " + error);
-                throw new RuntimeException(String.format("%s: glError 0x%04x", glOperation, error));
-            }
+            glDrawArrays(GL_TRIANGLE_FAN, 0, vertexCount);
         }
     }
 
-    private class ColorsRenderer implements GLSurfaceView.Renderer {
-        public void onSurfaceCreated(GL10 unused, EGLConfig config) {
-            mSquare = new Square();
-            GLES20.glClearColor(0f, 0f, 0f, 1.0f);
+    private static int buildProgram(String vertex, String fragment) {
+        int vertexShader = buildShader(vertex, GL_VERTEX_SHADER);
+        if (vertexShader == 0) return 0;
+
+        int fragmentShader = buildShader(fragment, GL_FRAGMENT_SHADER);
+        if (fragmentShader == 0) return 0;
+
+        int program = glCreateProgram();
+        glAttachShader(program, vertexShader);
+        checkGlError();
+
+        glAttachShader(program, fragmentShader);
+        checkGlError();
+
+        glLinkProgram(program);
+        checkGlError();
+
+        int[] status = new int[1];
+        glGetProgramiv(program, GL_LINK_STATUS, status, 0);
+        if (status[0] != GL_TRUE) {
+            String error = glGetProgramInfoLog(program);
+            Log.d(TAG, "Error while linking program:\n" + error);
+            glDeleteShader(vertexShader);
+            glDeleteShader(fragmentShader);
+            glDeleteProgram(program);
+            return 0;
         }
 
-        public void onDrawFrame(GL10 unused) {
-            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
-
-            mSquare.draw();
-        }
-
-        public void onSurfaceChanged(GL10 unused, int width, int height) {
-            GLES20.glViewport(0, 0, width, height);
-        }
+        return program;
     }
 
+    private static int buildShader(String source, int type) {
+        int shader = glCreateShader(type);
+
+        glShaderSource(shader, source);
+        checkGlError();
+
+        glCompileShader(shader);
+        checkGlError();
+
+        int[] status = new int[1];
+        glGetShaderiv(shader, GL_COMPILE_STATUS, status, 0);
+        if (status[0] != GL_TRUE) {
+            String error = glGetShaderInfoLog(shader);
+            Log.d(TAG, "Error while compiling shader:\n" + error);
+            glDeleteShader(shader);
+            return 0;
+        }
+
+        return shader;
+    }
     @Override
     public void onStart() {
         super.onStart();
     }
 
-    class TheySaidIHadToHaveAGLSurfaceView extends GLSurfaceView {
-
-        public TheySaidIHadToHaveAGLSurfaceView(Context context){
-            super(context);
-
-            setEGLContextClientVersion(2);
-
-            setRenderer(new ColorsRenderer());
+    private void checkEglError() {
+        int error = mEgl.eglGetError();
+        if (error != EGL10.EGL_SUCCESS) {
+            Log.w(TAG, "EGL error = 0x" + Integer.toHexString(error));
         }
     }
 
+    private static void checkGlError() {
+        checkGlError("");
+    }
+
+    private static void checkGlError(String what) {
+        int error = glGetError();
+        if (error != GL_NO_ERROR) {
+            Log.w(TAG, "GL error: (" + what + ") = 0x" + Integer.toHexString(error));
+        }
+    }
+
+    private void finishGL() {
+        mEgl.eglDestroyContext(mEglDisplay, mEglContext);
+        mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
+    }
+
+    private void checkCurrent() {
+        if (!mEglContext.equals(mEgl.eglGetCurrentContext()) ||
+                !mEglSurface.equals(mEgl.eglGetCurrentSurface(EGL10.EGL_DRAW))) {
+            if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
+                throw new RuntimeException("eglMakeCurrent failed "
+                        + GLUtils.getEGLErrorString(mEgl.eglGetError()));
+            }
+        }
+    }
+
+    private void initGL() {
+        mEgl = (EGL10) EGLContext.getEGL();
+
+        mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+        if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
+            throw new RuntimeException("eglGetDisplay failed "
+                    + GLUtils.getEGLErrorString(mEgl.eglGetError()));
+        }
+
+        int[] version = new int[2];
+        if (!mEgl.eglInitialize(mEglDisplay, version)) {
+            throw new RuntimeException("eglInitialize failed " +
+                    GLUtils.getEGLErrorString(mEgl.eglGetError()));
+        }
+
+        mEglConfig = chooseEglConfig();
+        if (mEglConfig == null) {
+            throw new RuntimeException("eglConfig not initialized");
+        }
+
+        mEglContext = createContext(mEgl, mEglDisplay, mEglConfig);
+
+        mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, mSurface, null);
+
+        if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
+            int error = mEgl.eglGetError();
+            if (error == EGL10.EGL_BAD_NATIVE_WINDOW) {
+                Log.e(TAG, "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
+                return;
+            }
+            throw new RuntimeException("createWindowSurface failed "
+                    + GLUtils.getEGLErrorString(error));
+        }
+
+        if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
+            throw new RuntimeException("eglMakeCurrent failed "
+                    + GLUtils.getEGLErrorString(mEgl.eglGetError()));
+        }
+
+        mGL = mEglContext.getGL();
+    }
+
+
+    EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) {
+        int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
+        return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
+    }
+
+    private EGLConfig chooseEglConfig() {
+        int[] configsCount = new int[1];
+        EGLConfig[] configs = new EGLConfig[1];
+        int[] configSpec = getConfig();
+        if (!mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount)) {
+            throw new IllegalArgumentException("eglChooseConfig failed " +
+                    GLUtils.getEGLErrorString(mEgl.eglGetError()));
+        } else if (configsCount[0] > 0) {
+            return configs[0];
+        }
+        return null;
+    }
+
+    private static int[] getConfig() {
+        return new int[] {
+                EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+                EGL10.EGL_RED_SIZE, 8,
+                EGL10.EGL_GREEN_SIZE, 8,
+                EGL10.EGL_BLUE_SIZE, 8,
+                EGL10.EGL_ALPHA_SIZE, 8,
+                EGL10.EGL_DEPTH_SIZE, 0,
+                EGL10.EGL_STENCIL_SIZE, 0,
+                EGL10.EGL_NONE
+        };
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        setInteractive(false);
+
+        mTextureView = new TextureView(this);
+        mTextureView.setSurfaceTextureListener(this);
+    }
+
     @Override
     public void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        
-        setFullscreen(true);
         setInteractive(false);
-        
-        gl = new TheySaidIHadToHaveAGLSurfaceView(Colors.this);
-        gl.postDelayed(new Runnable() {
+        setLowProfile(true);
+        setFullscreen(true);
+        setContentView(mTextureView);
+    }
+
+    @Override
+    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+        LOG("onSurfaceTextureAvailable(%s, %d, %d)", surface, width, height);
+        mSurface = surface;
+
+        mWidth = width;
+        mHeight = height;
+
+        new Thread() {
+            @Override
             public void run() {
-                Colors.this.setContentView(gl);
+                Looper.prepare();
+    
+                mChoreographer = Choreographer.getInstance();
+                mChoreographer.postFrameCallback(mFrameCallback);
+
+                Looper.loop();
             }
-        }, 1000);
+        }.start();
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+        LOG("onSurfaceTextureSizeChanged(%s, %d, %d)", surface, width, height);
+        mWidth = width;
+        mHeight = height;
+    }
+
+    @Override
+    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+        LOG("onSurfaceTextureDestroyed(%s)", surface);
+        return false;
+    }
+
+    @Override
+    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+        LOG("onSurfaceTextureUpdated(%s)", surface);
+    }
+
+    @Override
+    public void finish() {
+        mStop = true;
+        finishGL();
+        super.finish();
     }
 }