We are currently migrating Bugzilla to GitHub issues.
Any changes made to the bug tracker now will be lost, so please do not post new bugs or make changes to them.
When we're done, all bug URLs will redirect to their equivalent location on the new bug tracker.

Bug 1293

Summary: [Android] [Feature Request?] Support Pause/Resume
Product: SDL Reporter: wagic.the.homebrew
Component: mainAssignee: Sam Lantinga <slouken>
Status: RESOLVED FIXED QA Contact: Sam Lantinga <slouken>
Severity: normal    
Priority: P2 CC: gabomdq, tim, vitto.giova
Version: HG 2.0   
Hardware: Other   
OS: Android (All)   
Attachments: Support for Pause/Resume in Android
SDLActivity.java with Pause/Resume support
Support for Pause/Resume in Android via Window Events

Description wagic.the.homebrew 2011-08-28 19:08:59 UTC
The current SDLActivity.java samples that can be found in the repository assume that when the SDLSurface object is destroyed, it means the (native) app needs to quit.
This leads to problems, basically there are cases when the SDLSurface is destroyed while the game is still supposed to be "alive" in the background. Simple use cases include : getting a phone call, showing a webview (for people who want to include ads in their app), or pressing the home button.

I am not an Android expert but it seems the SDLActivity java file should have code to "deinit" EGL. In parallel, the native code needs to have "pause" and "resume" functions that can be called from Java. SDLActivity should basically handle onPause/onStop/onStart/onResume, and SDLSurface should not assume that surfaceDestroyed == quit the game.

I believe the ScummVM port on android had some code that does things more gracefully, it might be a good source of inspiration.

I marked this as a feature request, but it is such a "normal" use case on an android phone (multitasking/getting phone calls/showing ads) that I think it should be handled as a bug
Comment 1 wagic.the.homebrew 2011-09-01 06:53:43 UTC
I dug a bit more and I might have a suggested fix, however my current file is so far from the initial SDLActivity.java that I cannot submit a patch immediately, but here are the changes:

/************************************/

SDLACtivity:
    // Events
    @Override
    protected void onPause() {
        //Log.v("SDL", "onPause()");
        super.onPause();
        SDLActivity.nativePause();
        
    }
    
    @Override
    protected void onResume() {
        Log.v("SDL", "onResume()");
        super.onResume();
        SDLActivity.nativeResume();
    }

    @Override
    public void onStop() {
    	super.onStop();
    }

    @Override
    public void onDestroy() {   
    	Log.v("SDL", "onDestroy()");

    	super.onDestroy();

    	SDLActivity.nativeQuit();
    	
    	mSurface.onDestroy();
    }
    
    @Override
    public void onStart() {
    	super.onStart();
    }    
    


[...]


/**
    SDLSurface. This is what we draw on, so we need to know when it's created
    in order to do anything useful. 

    Because of this, that's where we set up the SDL thread
*/
class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, 
    View.OnKeyListener, View.OnTouchListener, SensorEventListener  {

    // This is what SDL runs in. It invokes SDL_main(), eventually
    private Thread mSDLThread;       
    
    // EGL private objects
    private EGLContext  mEGLContext;
    private EGLSurface  mEGLSurface;
    private EGLDisplay  mEGLDisplay;
    private EGLConfig   mEGLConfig;

    // Sensors
    private static SensorManager mSensorManager;

	private static VelocityTracker mVelocityTracker;

	final private Object _sem_surface;
	private SurfaceHolder _surface_holder;
	private Boolean mSurfaceValid;
	
	/**
    Simple nativeInit() runnable
	*/
    void startSDLThread() {
        if (mSDLThread == null) {
            mSDLThread = new Thread(new SDLMain(), "SDLThread"); 
            mSDLThread.start();       
        }    	
    }	
	
	class SDLMain implements Runnable {
	    public void run() {
	        // Runs SDL_main()
	    	try {
		    	// wait for the surfaceChanged callback
		    	synchronized(_sem_surface) {
		    	while (_surface_holder == null)
		    		_sem_surface.wait();
		    	}    
	    	} catch (Exception e) {

	    		throw new RuntimeException("Error preparing the ScummVM thread", e);
	    	}


	    	
	            SDLActivity.nativeInit();
	
	            SDLActivity.nativeQuit();
	        	// On exit, tear everything down for a fresh restart next time.
	        	System.exit(0);        
	        
	        
	        Log.v("SDL", "SDL thread terminated");
	    }
	    
	}	
		
	
	
    // Startup    
    public SDLSurface(Context context) {
        super(context);
    	_sem_surface = new Object();
    	_surface_holder = null;
    	mSurfaceValid = false;
        getHolder().addCallback(this); 
    
        setFocusable(true);
        setFocusableInTouchMode(true);
        requestFocus();
        setOnKeyListener(this); 
        setOnTouchListener(this);   

        mSensorManager = (SensorManager)context.getSystemService("sensor");
    }

    // Called when we have a valid drawing surface
    public void surfaceCreated(SurfaceHolder holder) {
        Log.v("SDL", "surfaceCreated()");

        enableSensor(Sensor.TYPE_ACCELEROMETER, true);
    }

    public void onDestroy() {
        // Send a quit message to the application
    	//should that be in SDLActivity "onDestroy" instead ?
        
        SDLActivity.nativeQuit();

        // Now wait for the SDL thread to quit
        if (mSDLThread != null) {
            try {
                mSDLThread.join();
            } catch(Exception e) {
                Log.v("SDL", "Problem stopping thread: " + e);
            }
            mSDLThread = null;
          
            //Log.v("SDL", "Finished waiting for SDL thread");
        }     	
    }
    
    // Called when we lose the surface
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.v("SDL", "surfaceDestroyed()");
        synchronized(_sem_surface) {
        	_surface_holder = null;
        	mSurfaceValid = false;
        	_sem_surface.notifyAll();
        	}
        enableSensor(Sensor.TYPE_ACCELEROMETER, false);
    }
    
    // Called when the surface is resized
    public void surfaceChanged(SurfaceHolder holder,
                               int format, int width, int height) {
        Log.v("SDL", "surfaceChanged()");

        int sdlFormat = 0x85151002; // SDL_PIXELFORMAT_RGB565 by default
        switch (format) {
        case PixelFormat.A_8:
            Log.v("SDL", "pixel format A_8");
            break;
        case PixelFormat.LA_88:
            Log.v("SDL", "pixel format LA_88");
            break;
        case PixelFormat.L_8:
            Log.v("SDL", "pixel format L_8");
            break;
        case PixelFormat.RGBA_4444:
            Log.v("SDL", "pixel format RGBA_4444");
            sdlFormat = 0x85421002; // SDL_PIXELFORMAT_RGBA4444
            break;
        case PixelFormat.RGBA_5551:
            Log.v("SDL", "pixel format RGBA_5551");
            sdlFormat = 0x85441002; // SDL_PIXELFORMAT_RGBA5551
            break;
        case PixelFormat.RGBA_8888:
            Log.v("SDL", "pixel format RGBA_8888");
            sdlFormat = 0x86462004; // SDL_PIXELFORMAT_RGBA8888
            break;
        case PixelFormat.RGBX_8888:
            Log.v("SDL", "pixel format RGBX_8888");
            sdlFormat = 0x86262004; // SDL_PIXELFORMAT_RGBX8888
            break;
        case PixelFormat.RGB_332:
            Log.v("SDL", "pixel format RGB_332");
            sdlFormat = 0x84110801; // SDL_PIXELFORMAT_RGB332
            break;
        case PixelFormat.RGB_565:
            Log.v("SDL", "pixel format RGB_565");
            sdlFormat = 0x85151002; // SDL_PIXELFORMAT_RGB565
            break;
        case PixelFormat.RGB_888:
            Log.v("SDL", "pixel format RGB_888");
            // Not sure this is right, maybe SDL_PIXELFORMAT_RGB24 instead?
            sdlFormat = 0x86161804; // SDL_PIXELFORMAT_RGB888
            break;
        default:
            Log.v("SDL", "pixel format unknown " + format);
            break;
        }
        SDLActivity.onNativeResize(width, height, sdlFormat);    
        
        // Now start up the C app thread
        synchronized(_sem_surface) {
        	_surface_holder = holder;
        	_sem_surface.notifyAll();
        	}
        
    }

    // unused
    public void onDraw(Canvas canvas) {}


    // EGL functions
    public boolean initEGL(int majorVersion, int minorVersion) {
        Log.v("SDL", "Starting up OpenGL ES " + majorVersion + "." + minorVersion);

        try {
        	
            EGL10 egl = (EGL10)EGLContext.getEGL();

            EGLDisplay dpy = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);

            int[] version = new int[2];
            egl.eglInitialize(dpy, version);

            int EGL_OPENGL_ES_BIT = 1;
            int EGL_OPENGL_ES2_BIT = 4;
            int renderableType = 0;
            if (majorVersion == 2) {
                renderableType = EGL_OPENGL_ES2_BIT;
            } else if (majorVersion == 1) {
                renderableType = EGL_OPENGL_ES_BIT;
            }
            int[] configSpec = {
                //EGL10.EGL_DEPTH_SIZE,   16,
                EGL10.EGL_RENDERABLE_TYPE, renderableType,
                EGL10.EGL_NONE
            };
            EGLConfig[] configs = new EGLConfig[1];
            int[] num_config = new int[1];
            if (!egl.eglChooseConfig(dpy, configSpec, configs, 1, num_config) || num_config[0] == 0) {
                Log.e("SDL", "No EGL config available");
                return false;
            }
            mEGLConfig = configs[0];

            EGLContext ctx = egl.eglCreateContext(dpy, mEGLConfig, EGL10.EGL_NO_CONTEXT, null);
            if (ctx == EGL10.EGL_NO_CONTEXT) {
                Log.e("SDL", "Couldn't create context");
                return false;
            }

            EGLSurface surface = egl.eglCreateWindowSurface(dpy, mEGLConfig, this, null);
            if (surface == EGL10.EGL_NO_SURFACE) {
                Log.e("SDL", "Couldn't create surface");
                return false;
            }

            if (!egl.eglMakeCurrent(dpy, surface, surface, ctx)) {
                Log.e("SDL", "Couldn't make context current");
                return false;
            }

            mEGLContext = ctx;
            mEGLDisplay = dpy;
            mEGLSurface = surface;
            mSurfaceValid = true;

        } catch(Exception e) {
            Log.v("SDL", e + "");
            for (StackTraceElement s : e.getStackTrace()) {
                Log.v("SDL", s.toString());
            }
        }

        return true;
    }
    
    
    public Boolean createSurface(SurfaceHolder holder) {
        /*
         *  The window size has changed, so we need to create a new
         *  surface.
         */
    	EGL10 egl = (EGL10)EGLContext.getEGL();
        if (mEGLSurface != null) {

            /*
             * Unbind and destroy the old EGL surface, if
             * there is one.
             */
            egl.eglMakeCurrent(mEGLDisplay, EGL10.EGL_NO_SURFACE,
                    EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
            egl.eglDestroySurface(mEGLDisplay, mEGLSurface);
        }

        /*
         * Create an EGL surface we can render into.
         */
        mEGLSurface = egl.eglCreateWindowSurface(mEGLDisplay,
        		mEGLConfig, holder, null);

        /*
         * Before we can issue GL commands, we need to make sure
         * the context is current and bound to a surface.
         */
        egl.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface,
        		mEGLContext);
        
        mSurfaceValid = true;

        return true;
        /*
        GL gl = mEGLContext.getGL();
        if (mGLWrapper != null) {
            gl = mGLWrapper.wrap(gl);
        }
        return gl;*/
    }
    

    // EGL buffer flip
    public void flipEGL() {
    	
    	if (!mSurfaceValid)
    	{
    		createSurface(this.getHolder());
    	}
[...]

/**************************************/
Sorry, it's a bit "raw" (EGLInit should probably call surfaceCreated, also surfaceCreated should have more defensive checks, etc...) because I just got it to work and I don't know if I'll be good enough to submit a real patch once I clean up the code, but basically:
- do not destroy the app when the surface is destroyed, instead do it when the Activity is destroyed
- split initEGL into 2 functions: initEGL and createSurface. CreateSurface needs to be called whenever flipBuffers attempts to render on an invalidSurface. a Boolean can be used in surfaceDestroyed to keep track of when the surface becomes invalid.

For me, this solves some of the issues I had with rendering not working after opening an ad webview. I believe this also fixes pause/resume issues. Note that I added my own native pause/resume functions. Pause only sets a "mPause" bool in the C code, and the main SDL loop just checks for this.
Comment 2 Tim Angus 2011-09-16 05:53:30 UTC
In the fragment you've provided it looks like startSDLThread() is never called? It may be better to just attach your entire SDLActivity.java, unless there is something commercially sensitive in it...
Comment 3 wagic.the.homebrew 2011-09-16 18:53:30 UTC
My diff can be found here:
http://code.google.com/p/wagic/source/diff?spec=svn3879&r=3879&format=side&path=/trunk/projects/mtg/Android/src/org/libsdl/app/SDLActivity.java&old_path=/trunk/projects/mtg/Android/src/org/libsdl/app/SDLActivity.java&old=3867

That diff also contains things you don't need:  adMob integration and things related to "JGE" which is our own engine.

The rest are changes to support pause/resume.

I hope this helps
Comment 4 Gabriel Jacobo 2011-12-23 12:48:55 UTC
Created attachment 739 [details]
Support for Pause/Resume in Android
Comment 5 Gabriel Jacobo 2011-12-23 12:55:11 UTC
Created attachment 740 [details]
SDLActivity.java with Pause/Resume support

The attached files provide some improvement over the current handling of pause/resume in Android. 
- I disabled the exit(status) instruction in SDL_main as that makes the entire app instead of the SDL thread exit (while not needed for pause/resume it is needed for Live Wallpapers, an SDLActivity for which I'll upload in a separate bug). 
- Added nativePause and nativeResume which basically just mark the window as visible/hidden, something that the end user needs to take into consideration (ideally pausing the event loop).

Also, this arrangement creates a new GL context when needed, which at least in my test system is every time you go away from the app and come back to it. So, this means that the textures need to be generated again after resuming (a problem the end user didn't have before because the app exited completely when it should've been pausing). I'd like to know if there's a standard way of letting the user know that the GL context has changed and that he needs to refresh his textures, or if this is out of the scope of the library and each user handles it in their own way (I don't know how/if this same thing is handled in  the iPhone backend, but it would be wise to try to imitate that).
Comment 6 Gabriel Jacobo 2011-12-23 12:57:10 UTC
Also, in the SDLActivity the EGL handling code is moved up to the Activity from the Surface code, as I think it is possible (in theory) that the surface is destroyed temporarily while the context remains alive (though in practice in my test system this is not the case)
Comment 7 Gabriel Jacobo 2011-12-28 10:23:49 UTC
Created attachment 743 [details]
Support for Pause/Resume in Android via Window Events

Improved handling via window events.
Comment 8 Sam Lantinga 2012-01-07 22:09:01 UTC
Thanks Gabriel, your changes look fairly clean.  There currently isn't any way to signal to the application that textures need to be reloaded, and this is something that probably needs to be added to the API.  For now, we should note in the documentation that when you regain focus on Android you need to re-initialize your graphics.

This bears further thought, but I'm committing your patch as a place to start.
http://hg.libsdl.org/SDL/rev/e565ac981de6

Thanks!
Comment 9 Vittorio Giovara 2012-01-08 05:18:05 UTC
Just a thought, would it be possible to have a uniform event for ios/android?

Currently on iOS when an app is suspended/resumed it receives SDL_WINDOWEVENT_MINIMIZED/SDL_WINDOWEVENT_RESTORED, it would be nice to have the same events on android.

Should this be on a different bug report?
Comment 10 Sam Lantinga 2012-01-08 10:42:46 UTC
Nope, that's a good point.  Fixed!
http://hg.libsdl.org/SDL/rev/2c0d35b1af4e