15

I have a surfaceView setup and running, but when I resume it I get an error that the thread has already been started. What's the proper way to handle when the app goes to the background and then back to the foreground? I've tinkered around and managed to get the app to come back without crashing... but the surfaceView doesn't draw anything anymore. My code:

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
           Log.e("sys","surfaceCreated was called.");
           if(systemState==BACKGROUND){
                  thread.setRunning(true);

           }
           else {
        thread.setRunning(true);
               thread.start();
               Log.e("sys","started thread");
               systemState=READY;
           }



    }
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
           Log.e("sys","surfaceDestroyed was called.");
           thread.setRunning(false);
           systemState=BACKGROUND;
    }
pgSystemTester
  • 8,979
  • 2
  • 23
  • 49
jfisk
  • 6,125
  • 20
  • 77
  • 113

7 Answers7

11

The easy solution is to simply kill and restart the thread. Create methods resume() - creates thread object and starts it - and pause() - kills thread (see Lunarlander example) - in your SurfaceView class and call these from surfaceCreated and surfaceDestroyed to start and stop the thread.

Now in the Activity that runs the SurfaceView, you will also need to call the resume() and pause() methods in the SurfaceView from the Activity's (or fragment's) onResume() and onPause(). It's not an elegant solution, but it will work.

Michael A.
  • 4,163
  • 3
  • 28
  • 47
  • I love Ur idea, I have been working around to find something easy. Because "surfaceDestroyed " is not called everytime but "onPause" is. Just like pressing "power" button then return. So I think your choice is a really good one. – Steven Shih Oct 30 '10 at 02:34
5

This bug appears to relate to the lunar lander bug, which is quite famous (do a Google search on it). After all this time, and after several android version releases, the bug still exists and no one has bothered to update it. i have found this to work with the least code clutter:

  public void surfaceCreated(SurfaceHolder holder) {     
          if (thread.getState==Thread.State.TERMINATED) { 
               thread = new MainThread(getHolder(),this);
          }
          thread.setRunning(true);
          thread.start();
  }
Androidcoder
  • 4,389
  • 5
  • 35
  • 50
  • Activity `onPause` will not **always** invoke `surfaceDestroyed` on it's own. So this will alone not solve the problem. See this question http://stackoverflow.com/q/11495842/1180117 – kiranpradeep Mar 10 '15 at 09:43
2

The best way I have found is to override the onResume method of the activity controlling the surface view so that with the method it re-instantiates the SurfaceView and then sets it with setContentView. The problem with this approach is that you need to reload any state that your SurfaceView was taking care of.

public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(new MyCustomSurfaceView(this));
    }

    @Override
    protected void onResume() {
        super.onResume();
        setContentView(new MyCustomSurfaceView(this));
    }
tjb
  • 11,480
  • 9
  • 70
  • 91
  • That did the trick for me. When I tried it before only in the onResume it did not work. – sivi Mar 13 '14 at 19:27
1

Tried to comment on the accepted answer above but couldn't, new to this. I don't think you should be calling your start/stop thread methods from both your SurfaceView and Activity. This will result in starting/stopping the thread doubly, and you can't start a thread more than once. Just call your methods from the Activity's onPause and onResume. They're called when exiting and re-entering the app so this will make sure your states are handled properly. surfaceDestroyed isn't always called, which messed me up for a while.

If you use this method make sure to check for a valid surface in your run code before working with your canvas, because the Activity will start the thread in onResume before the surface is available.

        while (_run) {
            if (_surfaceHolder.getSurface().isValid()) {
                ...
            }
        } //end _run
Matt K
  • 6,620
  • 3
  • 38
  • 60
1

This is what I have used. The app does not crashes now.

View Class:

holder.addCallback(new Callback() {

        public void surfaceDestroyed(SurfaceHolder holder) {
            gameLoopThread.setRunning(false);
            gameLoopThread.stop();
        }

        public void surfaceCreated(SurfaceHolder holder) {
            gameLoopThread.setRunning(true);
            gameLoopThread.start();

        }

In the GameLoopThread :

private boolean running = false;

public void setRunning(boolean run) {
    running = run;
}
@Override
public void run() {
    long ticksPs=1000/FPS;
    long startTime;
    long sleepTime;

while(running){
        Canvas c = null;
        startTime=System.currentTimeMillis();
        try {
            c = view.getHolder().lockCanvas();
            synchronized (view.getHolder()) {

                view.onDraw(c);

            }

        } finally {

            if (c != null) {
                view.getHolder().unlockCanvasAndPost(c);
            }

        }
        sleepTime=ticksPs-(System.currentTimeMillis()-startTime);
        try{

            if(sleepTime>0){
                sleep(sleepTime);
            }
            else
                sleep(10);
        } catch(Exception e){}
}

}

I hope it will help.

  • Activity `onPause` will not **always** invoke `surfaceDestroyed` on it's own. So this will not solve the problem. See this question http://stackoverflow.com/q/11495842/1180117 – kiranpradeep Mar 10 '15 at 09:38
0

You should use the Activities onPause() and onResume() methods.

First, in surfaceCreated(), start the thread. Also, in onResume(), make sure the thread isn't already started (keep a variable inside the thread or something). Then if it is not running, set it as running again. in onPause(), pause the thread. In surfaceDestroyed, pause the thread again.

Moncader
  • 3,388
  • 3
  • 22
  • 28
  • I have it properly set to use setRunning in the appropriate places, but despite having thread.setRunning(true) in my onResume my surfaceView is blank when its back in the forefront. The thread is still there, it doesnt crash the app when i go to the home screen, and if i do try another thread.start() i get the error saying that the threads been started. Any ideas? – jfisk Aug 20 '10 at 18:35
  • You can only ever start a thread once. I don't know what your setRunning method does on the inside, but the only way to properly stop a thread on Android is to let it return from its run() method. From that point onwards, you 'must' create a new thread object. You can not use that thread ever again. If you want to 'pause' the thread, you have to use wait() and notifyAll(). Google around for example and proper understanding on that. – Moncader Aug 21 '10 at 01:15
0

Another solution for this good-known problem. Sadly, I don't understand why it works -- it came out accidentally. But it works good for me and it's easy to implement: no overriding of Activity's onPause(), onResume(), onStart(), onStop(), nor writing of special thread methods (like resume(), pause()) are required.

Special requirement is to put all changing variables in something other than rendering thread class.

Main points to add to render-thread class:

class RefresherThread extends Thread {
    static SurfaceHolder threadSurfaceHolder;
    static YourAppViewClass threadView;
    static boolean running;

    public void run (){
        while(running){
            //your amazing draw/logic cycle goes here
        }
    }
}

Now, important things about YourAppViewClass:

class YourAppViewClass extends SurfaceView implements SurfaceHolder.Callback  {
    static RefresherThread surfaceThread;

    public YourAppViewClass(Activity inpParentActivity) {
        getHolder().addCallback(this);
        RefresherThread.threadSurfaceHolder = getHolder();
        RefresherThread.threadView = this;
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        surfaceThread = new RefresherThread();
        surfaceThread.running=true;
        surfaceThread.start();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        surfaceThread.running=false;
        try {
            surfaceThread.join();
        } catch (InterruptedException e) {  
        }               
    }
}

Two code blocks above are not full-written classes, but mere notion of which commands in which methods are needed. Also note that each return to app invokes surfaceChanged().

Sorry for such space-consuming answer. I hope it will work properly and will help.

Fyodor
  • 2,793
  • 4
  • 21
  • 24
  • Activity `onPause` will not **always** invoke `surfaceDestroyed` on it's own. So this will not solve the problem. See this question http://stackoverflow.com/q/11495842/1180117 – kiranpradeep Mar 10 '15 at 09:37