My program does some network activity in a background thread. Before starting, it pops up a progress dialog. The dialog is dismissed on the handler. This all works fine, except when screen orientation changes while the dialog is up (and the background thread is going). At this point the app either crashes, or deadlocks, or gets into a weird stage where the app does not work at all until all the threads have been killed.
How can I handle the screen orientation change gracefully?
The sample code below matches roughly what my real program does:
public class MyAct extends Activity implements Runnable {
public ProgressDialog mProgress;
// UI has a button that when pressed calls send
public void send() {
mProgress = ProgressDialog.show(this, "Please wait",
"Please wait",
true, true);
Thread thread = new Thread(this);
thread.start();
}
public void run() {
Thread.sleep(10000);
Message msg = new Message();
mHandler.sendMessage(msg);
}
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mProgress.dismiss();
}
};
}
Stack:
E/WindowManager( 244): Activity MyAct has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager( 244): android.view.WindowLeaked: Activity MyAct has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager( 244): at android.view.ViewRoot.<init>(ViewRoot.java:178)
E/WindowManager( 244): at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:147)
E/WindowManager( 244): at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:90)
E/WindowManager( 244): at android.view.Window$LocalWindowManager.addView(Window.java:393)
E/WindowManager( 244): at android.app.Dialog.show(Dialog.java:212)
E/WindowManager( 244): at android.app.ProgressDialog.show(ProgressDialog.java:103)
E/WindowManager( 244): at android.app.ProgressDialog.show(ProgressDialog.java:91)
E/WindowManager( 244): at MyAct.send(MyAct.java:294)
E/WindowManager( 244): at MyAct$4.onClick(MyAct.java:174)
E/WindowManager( 244): at android.view.View.performClick(View.java:2129)
E/WindowManager( 244): at android.view.View.onTouchEvent(View.java:3543)
E/WindowManager( 244): at android.widget.TextView.onTouchEvent(TextView.java:4664)
E/WindowManager( 244): at android.view.View.dispatchTouchEvent(View.java:3198)
I have tried to dismiss the progress dialog in onSaveInstanceState, but that just prevents an immediate crash. The background thread is still going, and the UI is in partially drawn state. Need to kill the whole app before it starts working again.
Edit: Google engineers do not recommend this approach, as described by Dianne Hackborn (a.k.a. hackbod) in this StackOverflow post. Check out this blog post for more information.
You have to add this to the activity declaration in the manifest:
so it looks like
The matter is that the system destroys the activity when a change in the configuration occurs. See ConfigurationChanges.
So putting that in the configuration file avoids the system to destroy your activity. Instead it invokes the
onConfigurationChanged(Configuration)
method.I came up with a rock-solid solution for these issues that conforms with the 'Android Way' of things. I have all my long-running operations using the IntentService pattern.
That is, my activities broadcast intents, the IntentService does the work, saves the data in the DB and then broadcasts sticky intents. The sticky part is important, such that even if the Activity was paused during during the time after the user initiated the work and misses the real time broadcast from the IntentService we can still respond and pick up the data from the calling Activity.
ProgressDialog
s can work with this pattern quite nicely withonSaveInstanceState()
.Basically, you need to save a flag that you have a progress dialog running in the saved instance bundle. Do not save the progress dialog object because this will leak the entire Activity. To have a persistent handle to the progress dialog, I store it as a weak reference in the application object. On orientation change or anything else that causes the Activity to pause (phone call, user hits home etc.) and then resume, I dismiss the old dialog and recreate a new dialog in the newly created Activity.
For indefinite progress dialogs this is easy. For progress bar style, you have to put the last known progress in the bundle and whatever information you're using locally in the activity to keep track of the progress. On restoring the progress, you'll use this information to re-spawn the progress bar in the same state as before and then update based on the current state of things.
So to summarize, putting long-running tasks into an IntentService coupled with judicious use of
onSaveInstanceState()
allows you to efficiently keep track of dialogs and restore then across the Activity life-cycle events. Relevant bits of Activity code are below. You'll also need logic in your BroadcastReceiver to handle Sticky intents appropriately, but that is beyond the scope of this.The trick is to show/dismiss the dialog within AsyncTask during onPreExecute/onPostExecute as usual, though in case of orientation-change create/show a new instance of the dialog in the activity and pass its reference to the task.
I discovered a solution to this that I haven't yet seen elsewhere. You can use a custom application object that knows if you have background tasks going, instead of trying to do this in the activity that gets destroyed and recreated on orientation change. I blogged about this in here.
If you maintain two layouts, all UI thread should be terminated.
If you use AsynTask, then you can easily call
.cancel()
method insideonDestroy()
method of current activity.For AsyncTask, read more in "Cancelling a task" section at here.
Update: Added condition to check status, as it can be only cancelled if it is in running state. Also note that the AsyncTask can only be executed one time.
I going to contribute my approach to handling this rotation issue. This may not be relevant to OP as he's not using
AsyncTask
, but maybe others will find it useful. It's pretty simple but it seems to do the job for me:I have a login activity with a nested
AsyncTask
class calledBackgroundLoginTask
.In my
BackgroundLoginTask
I don't do anything out of the ordinary except to add a null check upon callingProgressDialog
's dismiss:This is to handle the case where the background task finishes while the
Activity
is not visible and, therefore, the progress dialog has already been dismissed by theonPause()
method.Next, in my parent
Activity
class, I create global static handles to myAsyncTask
class and myProgressDialog
(theAsyncTask
, being nested, can access these variables):This serves two purposes: First, it allows my
Activity
to always access theAsyncTask
object even from a new, post-rotated activity. Second, it allows myBackgroundLoginTask
to access and dismiss theProgressDialog
even after a rotate.Next, I add this to
onPause()
, causing the progress dialog to disappear when ourActivity
is leaving the foreground (preventing that ugly "force close" crash):Finally, I have the following in my
onResume()
method:This allows the
Dialog
to reappear after theActivity
is recreated.Here is the entire class:
I am by no means a seasoned Android developer, so feel free to comment.