Android - Sharing surfaceview between Activities or prevent drawing tasks to lock the main thread when switching activity

StackOverflow https://stackoverflow.com/questions/22579230

Question

I am developing an app which uses a common header in all its activities. The header contains a sort of a custom progress bar which indicates task completion. The "progress bar" is implemented by subclassing a SurfaceView and the drawing operations are managed by an inner ExecutorService.

The tasks which tell the "progress bar" to run a certain animation are issued by a Singleton custom AsyncTaskManager, which holds a reference to the custom SurfaceView and the current activity.

Some of the AsyncTasks the singleton manager controls are executed upon custom Activities onCreate method, hence sometimes the AsyncTaskManager notifies the progress bar to animate before the activity is actually displayed. It can also happens that the user might choose to switch activity before the progressbar's drawing Runnable task is finished. To better explain, this is what happens when I switch to some activities:

  1. oldActivity tells the ExecutorService to cancel it's Future task that draws on the SurfaceView canvas.

  2. newActivity's onCreate is triggered and issues the AsyncTaskManager singleton to start a new AsyncTask.

  3. The AsyncTask in its onPreExecute tells the progress bar to start drawing on its canvas.

  4. The ExecutorService manages the drawing Runnable, which in turn locks the SurfaceHolder

  5. When the AsyncTask completes, in its onPostExecute method, tells the surfaceview drawing Runnable to draw a different thing according on the result.

The problem I am having is that SOMETIMES (not always - seems randomly but maybe it has to do with tasks threadpools), upon starting the new activity, the application skips frames xx where xx is apparently random (sometimes it skips ~30 frames, other times ~ 300, other times the app gets an ANR).

I have been trying to solve this for some days now, but to no avail.

I think the problem could be one of the following or a combination of both:

  • The drawing thread does not cancel/ends in a timely manner thus causing the SurfaceHolder to stay locked and thus preventing the Activity to take control of the View as it goes onPause/onResume and hence leading to the main thread skipping frames. The animation is by no means heavy in terms of computations (a couple of dots moving around) but it needs to last at least 300ms to properly notify the user.

  • The singleton AsyncTaskManager holds the reference to the "leaving activity"'s SurfaceView preventing the former to be destroyed until the surfaceholder is released and causing the frame-skipping.

I am more prone to believe the second issue is what is making Coreographer's angry and so this leads to the following question:

How can I share the SAME (as in the same instance) surfaceView (or any view, really) between all the activities or alternatively to allow the current instance of SurfaceView to be destroyed and recreated without waiting fot the threads to join/interrupt?

As it is now, the SurfaceView is being destroyed/recreated when switching between activities and I would have nothing against it if its drawing thread would stop as the new activity begins its lifecycle.

This is the custom AsyncTaskManager that holds a reference to the SurfaceView

public class AsyncTaskManager { 

    private RefreshLoaderView mLoader;

    //reference to the customactivity holding the surfaceview
    private CustomBaseActivity mActivity;

    private final ConcurrentSkipListSet<RequestedTask> mRequestedTasks;

    private volatile static AsyncTaskManager instance;

    private AsyncTaskManager() {
        mRequestedTasks = new ConcurrentSkipListSet<RequestedTask>(new RequestedTaskComparator());
    }

    public void setCurrentActivity(CustomBaseActivity activity) {
        mActivity = activity;

        if (mLoader != null) {
            mLoader.onDestroy();
        }
        mLoader = (RefreshLoaderView) mActivity.getViewById(R.id.mainLoader);

    }

This is what happens when an AsyncTask (RequestedTask in the above code snippet) is executed

    @Override
            protected void onPreExecute() {
                if (mLoader != null) {
                    mLoader.notifyTaskStarted();
                }
            }

            @Override
            protected Integer doInBackground(Void... params) {
                //do the heavy lifting here...
            }

            @Override
            protected void onPostExecute(Integer result) {


                switch (result) {

                    case RESULT_SUCCESS: 

                        if (mLoader != null) {
                            mLoader.notifyTaskSuccess();
                        }
                    break; 
//TELLS THE SURFACE VIEW TO PLAY DIFFERENT ANIMATIONS ACCORDING TO RESULT ...

This is the CustomBaseActivity that holds the SurfaceView from which all others activities inherit.

    public abstract class CustomBaseActivity extends FragmentActivity {

        private volatile RefreshLoaderView mLoader;

//...

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

            super.setContentView(R.layout.activity_base);

            mLoaderContainer = (FrameLayout) findViewById(R.id.mainLoaderContainer);
            mLoader = (RefreshLoaderView) findViewById(R.id.mainLoader);

//other uninteresting stuff goin on ...

And the code for the SurfaceView as well:

    public class RefreshLoaderView extends SurfaceView implements SurfaceHolder.Callback {


        private LoaderThread mLoaderThread;
        private volatile SurfaceHolder mHolder;

        private static final int ANIMATION_TIME = 600;

        private final ExecutorService mExecutor; 

        private Future mExecutingTask;


        public RefreshLoaderView(Context context) {
            super(context);
            ...
            init();

        }


        private void init() {
            mLoaderThread = new LoaderThread();
            ...
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int arg1, int arg2, int arg3) {
...
            mHolder = this.getHolder();
        }

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            //uninteresting stuff here
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            stopThread();

        }

        private void stopThread() {
            mLoaderThread.setRunning(false);
            if (mExecutingTask != null) {
                mExecutingTask.cancel(true);
            }
        }

        private void startThread() {
            if (mLoaderThread == null) {
                mLoaderThread = new LoaderThread();
            }

            mLoaderThread.setRunning(true);

            mExecutingTask = mExecutor.submit(mLoaderThread);
        }

        public void notifyTaskStarted() {
            stopThread();
            startThread();
            mLoaderThread.setAction(LoaderThread.ANIMATION_TASK_STARTED);
        }

        public void notifyTaskFailed() {
            mLoaderThread.setAction(LoaderThread.ANIMATION_TASK_FAILED);
        }

        public void notifyTaskSuccess() {
            mLoaderThread.setAction(LoaderThread.ANIMATION_TASK_SUCCESS);
        }


        private class LoaderThread implements Runnable {

            private volatile boolean  mRunning = false;
            private int mAction;
            private long mStartTime;
            private int mMode;
            public final static int ANIMATION_TASK_STARTED = 0;
            public final static int ANIMATION_TASK_FAILED = 1;
            public final static int ANIMATION_TASK_SUCCESS = 2;

            private final static int MODE_COMPLETING = 0;
            private final static int MODE_ENDING = 1;

            public LoaderThread() {
                mMode = 0;
            }

            public synchronized boolean isRunning() {
                return mRunning;
            }

            public synchronized void setRunning(boolean running) {
                mRunning = running;
                if (running) {
                    mStartTime = System.currentTimeMillis();
                }
            }

            public void setAction(int action) {
                mAction = action;
            }

            @Override
            public void run() {

                if (!mRunning) {

                    return;
                }
                while (mRunning) {
                    Canvas c = null;
                    try {
                        c = mHolder.lockCanvas();
                        synchronized (mHolder) {
                            //switcho quello che devo animare
                            if (c != null) {
                                switch (mAction) {
                                    case ANIMATION_TASK_STARTED:
                                        animationTaskStarted(c);
                                    break;
                                    case ANIMATION_TASK_FAILED:
                                        animationTaskFailed(c, mMode);
                                    break;
                                    case ANIMATION_TASK_SUCCESS:
                                        animationTaskSuccess(c, mMode);
                                    break;
                                }
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        if (c != null) {
                            mHolder.unlockCanvasAndPost(c);
                        }
                    }
                }
            }

            private void animationTaskStarted(Canvas canvas) {
                //do an animation here
            }


            private void animationCloseLoaderCycle(Canvas canvas) {
                //do stuff here ...
                } else {
                    mStartTime = System.currentTimeMillis();
                    mMode = MODE_ENDING;
                }
            }

            private void queryThreadClose() {
                mProgress = 0;
                mMode = MODE_COMPLETING;
                mRunning = false;
            }

            private void animationTaskFailed(Canvas canvas, int mode) {

                switch (mode) {
                    case MODE_COMPLETING:
                        animationCloseLoaderCycle(canvas);
                    break;
                    case MODE_ENDING:
                        if (System.currentTimeMillis() - mStartTime < ANIMATION_TIME) {
                            //notify user task is failed
                        } else {
                            queryThreadClose();
                        }
                    break;
                }
            }

            private void animationTaskSuccess(Canvas canvas, int mode) {

                switch (mode) {
                    case MODE_COMPLETING:
                        animationCloseLoaderCycle(canvas);
                    break;
                    case MODE_ENDING:
                        if (System.currentTimeMillis() - mStartTime < ANIMATION_TIME) {
                            //notify user task is failed
                        } else {
                            queryThreadClose();
                        }
                    break;
                }
            }
        }


        public void onPause() {
            stopThread();
        }

        public void onStop() {
            stopThread();
        }

        public void onDestroy() {
            stopThread();
        }

    }

Using DDMS when Coreographer warns me I'm skipping frame shows that there are usually around 30 threads (daemon and normal) running, where an asynctask, the main thread and the drawing task are waiting for something. (Also, how can I check what are they waiting for?)

Thanks in advance for your help.

Edit: these are the main thread calls when it hangs, according to DDMS Threads view:

at hava.lang.Object.wait(Native Method)
at java.lang.Thread.parkFor(Thread.java:1205)
at sun.misc.Unsafe.park(Unsafe.java:325)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:157)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:813)
...
Was it helpful?

Solution

I solved this in the end. There was a mistake in the synchronized block:

while (mRunning) {
                    Canvas c = null;
                    try {
          //mistake was here
                        c = mHolder.lockCanvas();
                        synchronized (mHolder) {

                            if (c != null) {
                             //do stuff
                            }
                        }
                    }

I was getting the canvas outside the synchronized block, thus causing a deadlock when the activity needed to be destroyed/recreated.

moving c = mHolder.lockCanvas(); inside the synchronized block solved this.

in the end the working code is as follows:

synchronized (mHolder) {
                        c = mHolder.lockCanvas();
                        if (c != null) {
                            switch (mAction) {
                                //do stuff
                            }
                        }
                    }

Thanks anyway!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top