How to handle AsyncTask onPostExecute when paused to avoid IllegalStateException
Asked Answered
J

5

40

I appreciate the numerous postings regarding AsyncTask on a rotation change. I have the following problem when using the compatability lib and trying to dismiss a DialogFragment in onPostExecute.

I have a fragment which fires of an AsyncTask which displays a progress DialogFragment, then in onPostExecute dismisses the dialog and then potentially throws up another DialogFragment.

If when the progress dialog is being displayed I put the application into the background I get the following for my fragment:

1) onPause

2) onSaveInstanceState

3) onPostExecute in which I try to dismiss and invoke a dialog.

I get an IllegalStateException because I'm trying to effectively commit a transaction when the activity has saved its state and I understand this.

On a rotation I've assumed (perhaps incorrectly) that I wouldn't get an onPostExecute until the activity has been recreated. However, when putting the application into the background I assumed (definitely incorrectly) that the onPostExectute wouldn't get called while the fragment/activity was paused.

My question is, is my solution to simply detect in onPostExecute that the fragment/activity is paused and simply perform what I need to do in onResume instead? Seems somewhat ugly to me.

Thanks in advance, peter.

Edit 1

Need to support 2.1 and above

Edit 2

I have considered showing the dialog using FragmentTransaction:add and FragmentTransaction:commitAllowingStateLosshowever this isn't without its problems.

Jussive answered 3/11, 2011 at 9:19 Comment(1)
My blog post on fragment transactions and activity state loss might help. :)Hymn
M
16

If you need to synchronize your task with the activity lifecycle, I believe that Loaders are exactly what you need. More specifically, you should use AsyncTaskLoader to do the job. So now instead of running an AsyncTask, you launch your loader, then wait for response in a listener. If the activity is paused, you won't get a callback, this part will be managed for you.

There is another way to handle this task: using a fragment which retains its instance. The general idea is that you create a fragment without UI and call setRetainInstance(true). It has a task which is being notified about the activity being available or not. If not, the task's thread suspends until an activity becomes available.

Melanson answered 3/11, 2011 at 9:28 Comment(4)
Thanks, need to investigate this further although feels like a bit of a sledgehammer to crack a nut in my case where just delaying UI tasks while paused would be the simple solution.Jussive
I'm working on a bit of the same problem right now, and I've found a new solution. Maybe it will help you too. Check it out, I've updated my answer.Melanson
@Melanson can you give link to any documentation which says If the activity is paused, you won't get a callback? I couldn't find it anywhere in the loaders documentation.Curd
@zack It isn't stated in the documentation explicitly as far as I know, but I looked in the source code, and that's how it behaves. I would still check if the app is resumed, though.Melanson
V
8

Another way of achieving what you require is to implement the PauseHandler class that I documented in this post.

Then in your onPostExecute method call sendMessage() to post your message into the handler.

When your application resumes the action will be handled.

Void answered 14/11, 2011 at 12:49 Comment(0)
C
3

Rather then using BroadcastReceiver, I prefer using bus libraries like guava, otto or eventbus. Their performance is much better then the broadcast receiver implementation.

Concordant answered 21/1, 2016 at 14:18 Comment(0)
P
2

I came up with a solution for this problem without any major workaround: The basic idea how to maintain a progressdialog and a asynctask is described in this blogentry (of course I used the AsyncTaskComplex-Version). All credits go to the author of this blogentry, I only added a tiny thing:

Obviously I'm not using showDialog() anymore. Instead I stick with DialogFragments.

The second tweak is the importent one and also solves the problem with the IllegalStateException:

Instead of only telling the asynctask in onRetainCustomNonConfigurationInstance() that there is no more activity I also do it in onPause(). And instead of only telling the asynctask in onCreate() that there is a new activity I also do it in onResume().

And there you go, your AsyncTask will not try to inform your activity about his finish causing an IllegalStateException when the activity is not visible.

If you would like to see more code instead of words, leave a comment.

/edit: Sourcecode to show my solution, which I think is a pretty decent one :)

public class MyActivity extends Activity {

private MyTask mTask;

@Override
protected void onCreate(Bundle pSavedInstanceState) {
    super.onCreate(pSavedInstanceState);
    setContentView(R.layout.editaccount);

    Object retained = getLastCustomNonConfigurationInstance();
    if ( retained instanceof NewContactFolderIdTask ) {
        mTask = (MyTask) retained;
        mTask.setActivity(this);
    }

}
@Override
protected void onPause() {
    if(mTask != null) {
        mTask.setActivity(null);
    }
    super.onPause();
}

@Override
public Object onRetainCustomNonConfigurationInstance() {
    if(mTask != null) {
        mTask.setActivity(null);
        return mTask;
    }
    return null;
}

@Override
protected void onResume() {
    if(mTask != null) {
        mTask.setActivity(this);
    }
    loadValues(); // or refreshListView or whatever you need to do
    super.onResume();
}

public void onTaskCompleted() {
    loadValues();  // or refreshListView or whatever you need to do
    DialogFragment dialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(PROGRESS_DIALOG_FRAGMENT);
    if(dialogFragment != null) {
        dialogFragment.dismiss();
    }
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater menuInflater = getMenuInflater();
    menuInflater.inflate(R.menu.main, menu);
    return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            // app icon in Action Bar clicked; go home
            Intent intent = new Intent(this, OXClient.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            startActivity(intent);
            return true;
        case R.id.menu_refresh:
            mTask = new MyTask(this);
            mTask.execute();
            break;
    }
    return super.onOptionsItemSelected(item);
}


private class NewContactFolderIdTask extends AsyncTask<Boolean, Integer, Bundle> {
    private MyActivity mActivity;
    private boolean mCompleted;

    private NewContactFolderIdTask(MyActivity pActivity) {
        this.mActivity = pActivity;
    }

    public void setActivity(MyActivity pActivity) {
        this.mActivity = pActivity;
        if(mCompleted) {
            notifiyActivityTaskCompleted();
        }
    }

    private void notifiyActivityTaskCompleted() {
        if(mActivity != null) {
            mActivity.onTaskCompleted();
        }
    }

    @Override
    protected Bundle doInBackground(Boolean... pBoolean) {
        // Do your stuff, return result
    }

    @Override
    protected void onPreExecute() {
        DialogFragment newFragment = ProgressDialogFragment.newInstance();
        newFragment.show(getSupportFragmentManager(), PROGRESS_DIALOG_FRAGMENT);
    }

    @Override
    protected void onPostExecute(Bundle pResult) {
        mCompleted = true;
        notifiyActivityTaskCompleted();
    }
}

}

Propene answered 8/11, 2011 at 11:59 Comment(9)
Thanks, the above is fine however if you put the application into the background before you get your onPostExecute I would still expect the dialogFragment to be dimissed at which point you'll see an illegalStateException unless I'm missing something obvious.Jussive
Yes, you are right. I'm also getting a IllegalStateException when my asyncTask finishs while the activity/application is in the background. Never tried that befor :/Propene
Sorry for not reading your questions carefully, totally overred the "put in background" part :/Propene
I solved the problem, at least my local tests were all positive. As I already said: If you would like to see more code, just leave a comment.Propene
Definitely be interested to see what you did. At the moment I'm considering either AsyncTaskLoader or simply delaying onPostExecute functionality until onResumeJussive
Sourcecode added. I think the AsyncTaskLoader is, as you said, a bit of a sledgehammer to crack a nutt and also a pseudo fragment wihtout UI, srsly? Just don't let the AsyncTask call your activity while it is in the background and you are good to go.Propene
Thanks, I need to test this but I'm still a little confused. I'm getting in onPause you set your async task's activity to null? (code missing). Still don't see though what stops you getting onPostExecute after onPause or why it would come in after onResume.Jussive
I added the onPause() implementation and did further tests: My onPostExecute is NOT called when I press the "HOME"-Button?!?Propene
OK, Thanks, yep can now see how it works. Although I don't see why your onPostExecute can't get called after onPause but anyway its onTaskCompleted functionality won't get processed until onResume after setting the activity.Jussive
C
2

On How to handle Handler messages when activity/fragment is paused I offer another approach using a BroadcastReceiver.

I consider it cleaner more elegant and it offers the advantages that you can invoke code on your base fragment from everywhere within your app and by using sticky broadcasts your invocation can be "remembered" and executed after your fragment resumes.

Creed answered 28/9, 2014 at 10:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.