GLSurfaceView adapted for 3D Live Wallpapers

I've been using GLSurfaceView since it was introduced in Android 1.5 and I was a little let down to find that the new Live Wallpaper APIs didn't include anything like that. I like the design because it makes it very easy to quickly start working in OpenGL. Without it, there is quite a bit of tedious initialization and thread management code that isn't necessary for the vast majority of apps. Fortunately, for my first live wallpaper (Live Waterpaper), I adapted the GLSurfaceView's code and created a GLWallpaperService with a GLEngine which takes a Renderer and does the job for me.

This is very similar in design to how things work with GLSurfaceView, except the GLWallpaperService does nothing more than create Engines and I have to move around a few of the fields for the GLThread management and configurations. Any event handling code you have should go into your GLEngine subclass and can be passed directly to your Renderer, which can be an exact port of an existing Renderer, with the exception of specifying the different package name on the interface.

I submitted this code to be part of the standard Android API. Hopefully it will make it in.

There is a lot of overlap with this code and GLSurfaceView, perhaps 99%.

So how do you use this? Extend GLWallpaperService and in your subclass, declare an inner class that extends GLEngine. Override onCreateEngine() to return your GLEngine subclass. In your GLEngine subclass, instantiate your Renderer, configure it and set it using setRenderer(Renderer) and setRenderMode(int). Your engine can handle touch events if you call setTouchEventsEnabled(true) and can also handle sensor data and preference changes. Everything else works exactly like it did with GLSurfaceView.

Enjoy!

File android.opengl.GLWallpaperService.java

package android.opengl;

import java.io.Writer;
import java.util.ArrayList;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGL11;
import javax.microedition.khronos.egl.EGLConfig;
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 javax.microedition.khronos.opengles.GL10;

import android.opengl.BaseConfigChooser.ComponentSizeChooser;
import android.opengl.BaseConfigChooser.SimpleEGLConfigChooser;
import android.service.wallpaper.WallpaperService;
import android.util.Log;
import android.view.SurfaceHolder;

public class GLWallpaperService extends WallpaperService {
private static final String TAG = "GLWallpaperService";

@Override
public Engine onCreateEngine() {
return new GLEngine();
}

public class GLEngine extends Engine {
public final static int RENDERMODE_WHEN_DIRTY = 0;
public final static int RENDERMODE_CONTINUOUSLY = 1;

private GLThread mGLThread;
private EGLConfigChooser mEGLConfigChooser;
private EGLContextFactory mEGLContextFactory;
private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory;
private GLWrapper mGLWrapper;
private int mDebugFlags;

public GLEngine() {
super();
}

@Override
public void onVisibilityChanged(boolean visible) {
if (visible) {
onResume();
} else {
onPause();
}
super.onVisibilityChanged(visible);
}

@Override
public void onCreate(SurfaceHolder surfaceHolder) {
super.onCreate(surfaceHolder);
// Log.d(TAG, "GLEngine.onCreate()");
}

@Override
public void onDestroy() {
super.onDestroy();
// Log.d(TAG, "GLEngine.onDestroy()");
}

@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// Log.d(TAG, "onSurfaceChanged()");
mGLThread.onWindowResize(width, height);
super.onSurfaceChanged(holder, format, width, height);
}

@Override
public void onSurfaceCreated(SurfaceHolder holder) {
Log.d(TAG, "onSurfaceCreated()");
mGLThread.surfaceCreated(holder);
super.onSurfaceCreated(holder);
}

@Override
public void onSurfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "onSurfaceDestroyed()");
mGLThread.surfaceDestroyed();
super.onSurfaceDestroyed(holder);
}

/**
* An EGL helper class.
*/
public void setGLWrapper(GLWrapper glWrapper) {
mGLWrapper = glWrapper;
}

public void setDebugFlags(int debugFlags) {
mDebugFlags = debugFlags;
}

public int getDebugFlags() {
return mDebugFlags;
}

public void setRenderer(Renderer renderer) {
checkRenderThreadState();
if (mEGLConfigChooser == null) {
mEGLConfigChooser = new SimpleEGLConfigChooser(true);
}
if (mEGLContextFactory == null) {
mEGLContextFactory = new DefaultContextFactory();
}
if (mEGLWindowSurfaceFactory == null) {
mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
}
mGLThread = new GLThread(renderer, mEGLConfigChooser, mEGLContextFactory, mEGLWindowSurfaceFactory, mGLWrapper);
mGLThread.start();
}

public void setEGLContextFactory(EGLContextFactory factory) {
checkRenderThreadState();
mEGLContextFactory = factory;
}

public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) {
checkRenderThreadState();
mEGLWindowSurfaceFactory = factory;
}

public void setEGLConfigChooser(EGLConfigChooser configChooser) {
checkRenderThreadState();
mEGLConfigChooser = configChooser;
}

public void setEGLConfigChooser(boolean needDepth) {
setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth));
}

public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize,
int stencilSize) {
setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, blueSize, alphaSize, depthSize,
stencilSize));
}

public void setRenderMode(int renderMode) {
mGLThread.setRenderMode(renderMode);
}

public int getRenderMode() {
return mGLThread.getRenderMode();
}

public void requestRender() {
mGLThread.requestRender();
}

public void onPause() {
mGLThread.onPause();
}

public void onResume() {
mGLThread.onResume();
}

public void queueEvent(Runnable r) {
mGLThread.queueEvent(r);
}

private void checkRenderThreadState() {
if (mGLThread != null) {
throw new IllegalStateException("setRenderer has already been called for this instance.");
}
}
}

public interface Renderer {

public void onSurfaceCreated(GL10 gl, EGLConfig config);

public void onSurfaceChanged(GL10 gl, int width, int height);

public void onDrawFrame(GL10 gl);
}
}

class LogWriter extends Writer {
private StringBuilder mBuilder = new StringBuilder();

@Override
public void close() {
flushBuilder();
}

@Override
public void flush() {
flushBuilder();
}

@Override
public void write(char[] buf, int offset, int count) {
for (int i = 0; i < count; i++) {
char c = buf[offset + i];
if (c == '\n') {
flushBuilder();
} else {
mBuilder.append(c);
}
}
}

private void flushBuilder() {
if (mBuilder.length() > 0) {
Log.v("GLSurfaceView", mBuilder.toString());
mBuilder.delete(0, mBuilder.length());
}
}
}

// ----------------------------------------------------------------------

/**
* An interface for customizing the eglCreateContext and eglDestroyContext calls.
*

* This interface must be implemented by clients wishing to call
* {@link GLWallpaperService#setEGLContextFactory(EGLContextFactory)}
*/
interface EGLContextFactory {
EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig);

void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context);
}

class DefaultContextFactory implements EGLContextFactory {

public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) {
return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, null);
}

public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
egl.eglDestroyContext(display, context);
}
}

/**
* An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls.
*

* This interface must be implemented by clients wishing to call
* {@link GLWallpaperService#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)}
*/
interface EGLWindowSurfaceFactory {
EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow);

void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface);
}

class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory {

public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) {
return egl.eglCreateWindowSurface(display, config, nativeWindow, null);
}

public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) {
egl.eglDestroySurface(display, surface);
}
}

interface GLWrapper {
/**
* Wraps a gl interface in another gl interface.
*
* @param gl
* a GL interface that is to be wrapped.
* @return either the input argument or another GL object that wraps the input argument.
*/
GL wrap(GL gl);
}

class EglHelper {

private EGL10 mEgl;
private EGLDisplay mEglDisplay;
private EGLSurface mEglSurface;
private EGLContext mEglContext;
EGLConfig mEglConfig;

private EGLConfigChooser mEGLConfigChooser;
private EGLContextFactory mEGLContextFactory;
private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory;
private GLWrapper mGLWrapper;

public EglHelper(EGLConfigChooser chooser, EGLContextFactory contextFactory,
EGLWindowSurfaceFactory surfaceFactory, GLWrapper wrapper) {
this.mEGLConfigChooser = chooser;
this.mEGLContextFactory = contextFactory;
this.mEGLWindowSurfaceFactory = surfaceFactory;
this.mGLWrapper = wrapper;
}

/**
* Initialize EGL for a given configuration spec.
*
* @param configSpec
*/
public void start() {
/*
* Get an EGL instance
*/
mEgl = (EGL10) EGLContext.getEGL();

/*
* Get to the default display.
*/
mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);

/*
* We can now initialize EGL for that display
*/
int[] version = new int[2];
mEgl.eglInitialize(mEglDisplay, version);
mEglConfig = mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);

/*
* Create an OpenGL ES context. This must be done only once, an OpenGL context is a somewhat heavy object.
*/
mEglContext = mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig);
if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
throw new RuntimeException("createContext failed");
}

mEglSurface = null;
}

/*
* React to the creation of a new surface by creating and returning an OpenGL interface that renders to that
* surface.
*/
public GL createSurface(SurfaceHolder holder) {
/*
* The window size has changed, so we need to create a new surface.
*/
if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {

/*
* Unbind and destroy the old EGL surface, if there is one.
*/
mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface);
}

/*
* Create an EGL surface we can render into.
*/
mEglSurface = mEGLWindowSurfaceFactory.createWindowSurface(mEgl, mEglDisplay, mEglConfig, holder);

if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
throw new RuntimeException("createWindowSurface failed");
}

/*
* Before we can issue GL commands, we need to make sure the context is current and bound to a surface.
*/
if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
throw new RuntimeException("eglMakeCurrent failed.");
}

GL gl = mEglContext.getGL();
if (mGLWrapper != null) {
gl = mGLWrapper.wrap(gl);
}

/*
* if ((mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS))!= 0) { int configFlags = 0; Writer log =
* null; if ((mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) { configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; }
* if ((mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) { log = new LogWriter(); } gl = GLDebugHelper.wrap(gl,
* configFlags, log); }
*/
return gl;
}

/**
* Display the current render surface.
*
* @return false if the context has been lost.
*/
public boolean swap() {
mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);

/*
* Always check for EGL_CONTEXT_LOST, which means the context and all associated data were lost (For instance
* because the device went to sleep). We need to sleep until we get a new surface.
*/
return mEgl.eglGetError() != EGL11.EGL_CONTEXT_LOST;
}

public void destroySurface() {
if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface);
mEglSurface = null;
}
}

public void finish() {
if (mEglContext != null) {
mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext);
mEglContext = null;
}
if (mEglDisplay != null) {
mEgl.eglTerminate(mEglDisplay);
mEglDisplay = null;
}
}
}

class GLThread extends Thread {
private final static boolean LOG_THREADS = false;
public final static int DEBUG_CHECK_GL_ERROR = 1;
public final static int DEBUG_LOG_GL_CALLS = 2;

private final GLThreadManager sGLThreadManager = new GLThreadManager();
private GLThread mEglOwner;

private EGLConfigChooser mEGLConfigChooser;
private EGLContextFactory mEGLContextFactory;
private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory;
private GLWrapper mGLWrapper;

public SurfaceHolder mHolder;
private boolean mSizeChanged = true;

// Once the thread is started, all accesses to the following member
// variables are protected by the sGLThreadManager monitor
public boolean mDone;
private boolean mPaused;
private boolean mHasSurface;
private boolean mWaitingForSurface;
private boolean mHaveEgl;
private int mWidth;
private int mHeight;
private int mRenderMode;
private boolean mRequestRender;
private boolean mEventsWaiting;
// End of member variables protected by the sGLThreadManager monitor.

private GLWallpaperService.Renderer mRenderer;
private ArrayList mEventQueue = new ArrayList();
private EglHelper mEglHelper;

GLThread(GLWallpaperService.Renderer renderer, EGLConfigChooser chooser, EGLContextFactory contextFactory,
EGLWindowSurfaceFactory surfaceFactory, GLWrapper wrapper) {
super();
mDone = false;
mWidth = 0;
mHeight = 0;
mRequestRender = true;
mRenderMode = GLWallpaperService.GLEngine.RENDERMODE_CONTINUOUSLY;
mRenderer = renderer;
this.mEGLConfigChooser = chooser;
this.mEGLContextFactory = contextFactory;
this.mEGLWindowSurfaceFactory = surfaceFactory;
this.mGLWrapper = wrapper;
}

@Override
public void run() {
setName("GLThread " + getId());
if (LOG_THREADS) {
Log.i("GLThread", "starting tid=" + getId());
}

try {
guardedRun();
} catch (InterruptedException e) {
// fall thru and exit normally
} finally {
sGLThreadManager.threadExiting(this);
}
}

/*
* This private method should only be called inside a synchronized(sGLThreadManager) block.
*/
private void stopEglLocked() {
if (mHaveEgl) {
mHaveEgl = false;
mEglHelper.destroySurface();
mEglHelper.finish();
sGLThreadManager.releaseEglSurface(this);
}
}

private void guardedRun() throws InterruptedException {
mEglHelper = new EglHelper(mEGLConfigChooser, mEGLContextFactory, mEGLWindowSurfaceFactory, mGLWrapper);
try {
GL10 gl = null;
boolean tellRendererSurfaceCreated = true;
boolean tellRendererSurfaceChanged = true;

/*
* This is our main activity thread's loop, we go until asked to quit.
*/
while (!isDone()) {
/*
* Update the asynchronous state (window size)
*/
int w = 0;
int h = 0;
boolean changed = false;
boolean needStart = false;
boolean eventsWaiting = false;

synchronized (sGLThreadManager) {
while (true) {
// Manage acquiring and releasing the SurfaceView
// surface and the EGL surface.
if (mPaused) {
stopEglLocked();
}
if (!mHasSurface) {
if (!mWaitingForSurface) {
stopEglLocked();
mWaitingForSurface = true;
sGLThreadManager.notifyAll();
}
} else {
if (!mHaveEgl) {
if (sGLThreadManager.tryAcquireEglSurface(this)) {
mHaveEgl = true;
mEglHelper.start();
mRequestRender = true;
needStart = true;
}
}
}

// Check if we need to wait. If not, update any state
// that needs to be updated, copy any state that
// needs to be copied, and use "break" to exit the
// wait loop.

if (mDone) {
return;
}

if (mEventsWaiting) {
eventsWaiting = true;
mEventsWaiting = false;
break;
}

if ((!mPaused) && mHasSurface && mHaveEgl && (mWidth > 0) && (mHeight > 0)
&& (mRequestRender || (mRenderMode == GLWallpaperService.GLEngine.RENDERMODE_CONTINUOUSLY))) {
changed = mSizeChanged;
w = mWidth;
h = mHeight;
mSizeChanged = false;
mRequestRender = false;
if (mHasSurface && mWaitingForSurface) {
changed = true;
mWaitingForSurface = false;
sGLThreadManager.notifyAll();
}
break;
}

// By design, this is the only place where we wait().

if (LOG_THREADS) {
Log.i("GLThread", "waiting tid=" + getId());
}
sGLThreadManager.wait();
}
} // end of synchronized(sGLThreadManager)

/*
* Handle queued events
*/
if (eventsWaiting) {
Runnable r;
while ((r = getEvent()) != null) {
r.run();
if (isDone()) {
return;
}
}
// Go back and see if we need to wait to render.
continue;
}

if (needStart) {
tellRendererSurfaceCreated = true;
changed = true;
}
if (changed) {
gl = (GL10) mEglHelper.createSurface(mHolder);
tellRendererSurfaceChanged = true;
}
if (tellRendererSurfaceCreated) {
mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
tellRendererSurfaceCreated = false;
}
if (tellRendererSurfaceChanged) {
mRenderer.onSurfaceChanged(gl, w, h);
tellRendererSurfaceChanged = false;
}
if ((w > 0) && (h > 0)) {
/* draw a frame here */
mRenderer.onDrawFrame(gl);

/*
* Once we're done with GL, we need to call swapBuffers() to instruct the system to display the
* rendered frame
*/
mEglHelper.swap();
}
}
} finally {
/*
* clean-up everything...
*/
synchronized (sGLThreadManager) {
stopEglLocked();
}
}
}

private boolean isDone() {
synchronized (sGLThreadManager) {
return mDone;
}
}

public void setRenderMode(int renderMode) {
if (!((GLWallpaperService.GLEngine.RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= GLWallpaperService.GLEngine.RENDERMODE_CONTINUOUSLY))) {
throw new IllegalArgumentException("renderMode");
}
synchronized (sGLThreadManager) {
mRenderMode = renderMode;
if (renderMode == GLWallpaperService.GLEngine.RENDERMODE_CONTINUOUSLY) {
sGLThreadManager.notifyAll();
}
}
}

public int getRenderMode() {
synchronized (sGLThreadManager) {
return mRenderMode;
}
}

public void requestRender() {
synchronized (sGLThreadManager) {
mRequestRender = true;
sGLThreadManager.notifyAll();
}
}

public void surfaceCreated(SurfaceHolder holder) {
mHolder = holder;
synchronized (sGLThreadManager) {
if (LOG_THREADS) {
Log.i("GLThread", "surfaceCreated tid=" + getId());
}
mHasSurface = true;
sGLThreadManager.notifyAll();
}
}

public void surfaceDestroyed() {
synchronized (sGLThreadManager) {
if (LOG_THREADS) {
Log.i("GLThread", "surfaceDestroyed tid=" + getId());
}
mHasSurface = false;
sGLThreadManager.notifyAll();
while (!mWaitingForSurface && isAlive() && !mDone) {
try {
sGLThreadManager.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}

public void onPause() {
synchronized (sGLThreadManager) {
mPaused = true;
sGLThreadManager.notifyAll();
}
}

public void onResume() {
synchronized (sGLThreadManager) {
mPaused = false;
mRequestRender = true;
sGLThreadManager.notifyAll();
}
}

public void onWindowResize(int w, int h) {
synchronized (sGLThreadManager) {
mWidth = w;
mHeight = h;
mSizeChanged = true;
sGLThreadManager.notifyAll();
}
}

public void requestExitAndWait() {
// don't call this from GLThread thread or it is a guaranteed
// deadlock!
synchronized (sGLThreadManager) {
mDone = true;
sGLThreadManager.notifyAll();
}
try {
join();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}

/**
* Queue an "event" to be run on the GL rendering thread.
*
* @param r
* the runnable to be run on the GL rendering thread.
*/
public void queueEvent(Runnable r) {
synchronized (this) {
mEventQueue.add(r);
synchronized (sGLThreadManager) {
mEventsWaiting = true;
sGLThreadManager.notifyAll();
}
}
}

private Runnable getEvent() {
synchronized (this) {
if (mEventQueue.size() > 0) {
return mEventQueue.remove(0);
}

}
return null;
}

private class GLThreadManager {

public synchronized void threadExiting(GLThread thread) {
if (LOG_THREADS) {
Log.i("GLThread", "exiting tid=" + thread.getId());
}
thread.mDone = true;
if (mEglOwner == thread) {
mEglOwner = null;
}
notifyAll();
}

/*
* Tries once to acquire the right to use an EGL surface. Does not block.
*
* @return true if the right to use an EGL surface was acquired.
*/
public synchronized boolean tryAcquireEglSurface(GLThread thread) {
if (mEglOwner == thread || mEglOwner == null) {
mEglOwner = thread;
notifyAll();
return true;
}
return false;
}

public synchronized void releaseEglSurface(GLThread thread) {
if (mEglOwner == thread) {
mEglOwner = null;
}
notifyAll();
}
}
}

interface EGLConfigChooser {
EGLConfig chooseConfig(EGL10 egl, EGLDisplay display);
}

abstract class BaseConfigChooser implements EGLConfigChooser {
public BaseConfigChooser(int[] configSpec) {
mConfigSpec = configSpec;
}

public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
int[] num_config = new int[1];
egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config);

int numConfigs = num_config[0];

if (numConfigs <= 0) {
throw new IllegalArgumentException("No configs match configSpec");
}

EGLConfig[] configs = new EGLConfig[numConfigs];
egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, num_config);
EGLConfig config = chooseConfig(egl, display, configs);
if (config == null) {
throw new IllegalArgumentException("No config chosen");
}
return config;
}

abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs);

protected int[] mConfigSpec;
public static class ComponentSizeChooser extends BaseConfigChooser {
public ComponentSizeChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize,
int stencilSize) {
super(new int[] { EGL10.EGL_RED_SIZE, redSize, EGL10.EGL_GREEN_SIZE, greenSize, EGL10.EGL_BLUE_SIZE,
blueSize, EGL10.EGL_ALPHA_SIZE, alphaSize, EGL10.EGL_DEPTH_SIZE, depthSize, EGL10.EGL_STENCIL_SIZE,
stencilSize, EGL10.EGL_NONE });
mValue = new int[1];
mRedSize = redSize;
mGreenSize = greenSize;
mBlueSize = blueSize;
mAlphaSize = alphaSize;
mDepthSize = depthSize;
mStencilSize = stencilSize;
}

@Override
public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) {
EGLConfig closestConfig = null;
int closestDistance = 1000;
for (EGLConfig config : configs) {
int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0);
int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0);
if (d >= mDepthSize && s >= mStencilSize) {
int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0);
int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0);
int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0);
int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0);
int distance = Math.abs(r - mRedSize) + Math.abs(g - mGreenSize) + Math.abs(b - mBlueSize)
+ Math.abs(a - mAlphaSize);
if (distance < closestDistance) {
closestDistance = distance;
closestConfig = config;
}
}
}
return closestConfig;
}

private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue) {

if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) {
return mValue[0];
}
return defaultValue;
}

private int[] mValue;
// Subclasses can adjust these values:
protected int mRedSize;
protected int mGreenSize;
protected int mBlueSize;
protected int mAlphaSize;
protected int mDepthSize;
protected int mStencilSize;
}

/**
* This class will choose a supported surface as close to RGB565 as possible, with or without a depth buffer.
*
*/
public static class SimpleEGLConfigChooser extends ComponentSizeChooser {
public SimpleEGLConfigChooser(boolean withDepthBuffer) {
super(4, 4, 4, 0, withDepthBuffer ? 16 : 0, 0);
// Adjust target values. This way we'll accept a 4444 or
// 555 buffer if there's no 565 buffer available.
mRedSize = 5;
mGreenSize = 6;
mBlueSize = 5;
}
}
}

And here's the basic template for implementing your wallpaper service:

public class MyWallpaperService extends GLWallpaperService {
public MyWallpaperService() {
super();
}

public Engine onCreateEngine() {
MyEngine engine = new MyEngine();
return engine;
}

// prefs and sensor interface is optional, just showing that this is where you do all of that - everything that would normally be in an activity is in here.
class MyEngine extends GLEngine implements SharedPreferences.OnSharedPreferenceChangeListener, SensorEventListener {
MyRenderer renderer;
public MyEngine() {
super();
// handle prefs, other initialization
renderer = new MyRenderer();
setRenderer(renderer);
setRenderMode(RENDERMODE_WHEN_DIRTY);
}

public void onDestroy() {
super.onDestroy();
if (renderer != null) {
renderer.release(); // assuming yours has this method - it should!
}
renderer = null;
}

14 Comments

Post a comment here or discuss this and other topics in the forums

oops...

Please excuse the double post about the debugging....

GLWallpaper as an Activity

Is there an easy way to use the WallPaper Service in an activity?
- I guess I could just extract the render code to a different class..

No Debug?
- is it just me or can you not debug services?

Nope, they are not

Nope, they are not interchangeable, though Engine and Activity are slightly similar. You should be able to write just one Renderer and then copy lots of the Activity/Engine code between the two. That's how I do things - I write it in an Activity/GLSurfaceView to start then move it over to the GLEngine when I'm ready to test as a Wallpaper.

Thanks a lot

Thanks for this,
i made earth live wallpaper using your code, it is awsome !

Oh cool! I have that one!

Oh cool! I have that one! You did a really nice job on it!

TouchEvents

Thanks very much for the code but I am having problems using the touchevents...

setTouchEventsEnabled(true); of course is set..

and my renderer has an onTouchEvent...

any ideas what else I could check...

ps. Have you managed yet to debug Live Wallpapers? in does not seem to work in my eclipse... I guess I could simply create a simple activity first and then debug...

thx

Gary, I never debug real-time

Gary,

I never debug real-time code like games/physics/simulations. I always use print hooks to show me what's happening in an area of concern.

The touch handler should be in the Engine. Override Engine.onTouchEvent(MotionEvent) and have that send the event to your Renderer, which you can keep as a private field to the Engine. If you already have that in your renderer, just do:

class MyEngine extends GLEngine {
@Override
public void onTouchEvent(MotionEvent event) {
renderer.onTouchEvent(event);
}
}

Excellent, but I'm having a problem with it:

Robert,

You are awesome!

I would really like to see an example that draws a simple object.
Right now, my logcat is showing a null-pointer exception at line 79
in GLWallpaperService --Complaining about null-pointer to holder.

I can make this object work in an Activity, but somehow I don't understand the process
of initializing a SurfaceHolder correctly for GL drawing.

If you could email me at josh.beck2006@gmail.com or something, I'd really appreciate it.
Stayed up all night with it. I'm new to OpenGL.

Thank you again!

That's strange. I don't see

That's strange. I don't see where you could have an NPE there. Could you show me more of your stack trace so that I can be sure about what line you're talking about? Your 79 may not match mine :)

Also, you extended, not modified, GLWallpaperService, right?

I posted a template

I posted a template at the end of the article. Start with that and add-in as you go. :)

Great! Can't wait to get my

Great!
Can't wait to get my hands on this once I'm finished with exams.

example?

Please can you show us an example?
Thank you very much!

You can actually use the

You can actually use the samples in the API demos and plug them right into this with very little modification. I'll see about posting a basic scene with a shape in it animating or something when I get a little more time.

Nice! I was just about to set

Nice! I was just about to set out to do something like this myself, but I'm glad I googled it first. Many thanks.

Post new comment

The content of this field is kept private and will not be shown publicly.
Add image
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <table> <tr> <td>
  • Lines and paragraphs break automatically.
  • Images can be added to this post.
  • Image links with 'rel="lightbox"' in the <a> tag will appear in a Lightbox when clicked on.
  • Image links with 'rel="lightshow"' in the <a> tag will appear in a Lightbox slideshow when clicked on.
  • Links to HTML content with 'rel="lightframe"' in the <a> tag will appear in a Lightbox when clicked on.
  • Links to video content with 'rel="lightvideo"' in the <a> tag will appear in a Lightbox when clicked on.
  • Links to inline or modal content with 'rel="lightmodal"' in the <a> tag will appear in a Lightbox when clicked on.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.