tags:

views:

14192

answers:

8

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)
E/WindowManager(  244):     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:857)
E/WindowManager(  244):     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:857)
E/WindowManager(  244):     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:857)
E/WindowManager(  244):     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:857)
E/WindowManager(  244):     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:857)
E/WindowManager(  244):     at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:1593)
E/WindowManager(  244):     at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1089)
E/WindowManager(  244):     at android.app.Activity.dispatchTouchEvent(Activity.java:1871)
E/WindowManager(  244):     at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:1577)
E/WindowManager(  244):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1140)
E/WindowManager(  244):     at android.os.Handler.dispatchMessage(Handler.java:88)
E/WindowManager(  244):     at android.os.Looper.loop(Looper.java:123)
E/WindowManager(  244):     at android.app.ActivityThread.main(ActivityThread.java:3739)
E/WindowManager(  244):     at java.lang.reflect.Method.invokeNative(Native Method)
E/WindowManager(  244):     at java.lang.reflect.Method.invoke(Method.java:515)
E/WindowManager(  244):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
E/WindowManager(  244):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:497)
E/WindowManager(  244):     at dalvik.system.NativeStart.main(Native Method)

and:

W/dalvikvm(  244): threadid=3: thread exiting with uncaught exception (group=0x4000fe68)
E/AndroidRuntime(  244): Uncaught handler: thread main exiting due to uncaught exception
E/AndroidRuntime(  244): java.lang.IllegalArgumentException: View not attached to window manager
E/AndroidRuntime(  244):    at android.view.WindowManagerImpl.findViewLocked(WindowManagerImpl.java:331)
E/AndroidRuntime(  244):    at android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:200)
E/AndroidRuntime(  244):    at android.view.Window$LocalWindowManager.removeView(Window.java:401)
E/AndroidRuntime(  244):    at android.app.Dialog.dismissDialog(Dialog.java:249)
E/AndroidRuntime(  244):    at android.app.Dialog.access$000(Dialog.java:59)
E/AndroidRuntime(  244):    at android.app.Dialog$1.run(Dialog.java:93)
E/AndroidRuntime(  244):    at android.app.Dialog.dismiss(Dialog.java:233)
E/AndroidRuntime(  244):    at MyAct$1.handleMessage(MyAct.java:321)
E/AndroidRuntime(  244):    at android.os.Handler.dispatchMessage(Handler.java:88)
E/AndroidRuntime(  244):    at android.os.Looper.loop(Looper.java:123)
E/AndroidRuntime(  244):    at android.app.ActivityThread.main(ActivityThread.java:3739)
E/AndroidRuntime(  244):    at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(  244):    at java.lang.reflect.Method.invoke(Method.java:515)
E/AndroidRuntime(  244):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
E/AndroidRuntime(  244):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:497)
E/AndroidRuntime(  244):    at dalvik.system.NativeStart.main(Native Method)
I/Process (   46): Sending signal. PID: 244 SIG: 3
I/dalvikvm(  244): threadid=7: reacting to signal 3
I/dalvikvm(  244): Wrote stack trace to '/data/anr/traces.txt'
I/Process (  244): Sending signal. PID: 244 SIG: 9
I/ActivityManager(   46): Process MyAct (pid 244) has died.

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.

+1  A: 

Move the long task to a seperate class. Implement it as a subject-observer pattern. Whenever the activity is created register and while closing unregister with the task class. Task class can use AsyncTask.

Vinay
I don't see how that would help. Could you explain in more detail how this prevents the problems I am seeing.
Heikki Toivonen
As Haseman said it prevents the backend to access the UI elements and we can seperate the UI from backend, backend runs in seperate thread and it continues to run even after the screen is re-oriented and Register-UnRegister with the Backend task for status updates. The real example I have solved using this is that I have a Download Task, I have moved that to a seperate thread, whenever the thread is created I register-unregister with it.
Vinay
Ok, I am revisiting this issue and I don't think I still fully understand this answer. Suppose we have the main activity start an AsyncTask to do a long-running network operation we don't want to interrupt during screen orientation change. I don't see how the new activity can send a message to the AsyncTask started by the old activity. Can you give a code example?
Heikki Toivonen
+4  A: 

When you switch orientations, Android will create a new View. You're probably getting crashes because your background thread is trying to change the state on the old one. (It may also be having trouble because your background thread isn't on the UI thread)

I'd suggest making that mHandler volatile and updating it when the orientation changes.

haseman
You might have pinpointed the reason for the crash. I got rid of the crash, but I still haven't figured out how to restore the UI to the state it was in before the orientation change in a reliable way. But your answer moved me forward, so awarding it as answer.
Heikki Toivonen
You should get an onStart in your activity when the orientation changes. Essentially, you have to reconfigure the view using the old data. So I'd suggest requesting numerical status udpates from the progress bar and rebuilding a new view when you get that new 'onStart'I can't remember offhand if you get a new activity as well but some hunting through the documentation should help.
haseman
Having played with it recently, I can pass on that you do get a new activity when your app changes orientation. (You also get a new view) If you try to update the old view you'll get an exception because the old view has an invalid application context (your old activity)You can kinda get around this by passing in myActivity.getApplicationContext() instead of a pointer to the activity itself.
haseman
+3  A: 

I met the same problem. My activity needs to parse some data from a URL and it's slow. So I create a thread to do so then show a progressdialog. I let the thread post a msg back to UI thread via Handler when it's finished. In Handler.handleMessage, I get the data object (ready now) from thread and populate it to UI. So it's very similar to your example.

After many trial and error it looks like I found a solution. At least now I can rotate screen at any moment, before or after the thread is done. In all tests, the dialog is properly closed and all behaviors are as expected.

What I did is shown below. The goal is to fill my data model (mDataObject) and then populate it to UI. Should allow screen rotation at any moment without surprise.

class MyActivity {

private MyDataObject mDataObject = null;
private static MyThread mParserThread = null; // static, or make it singleton

OnCreate() {
        ...
        Object retained = this.getLastNonConfigurationInstance();
        if(retained != null) {
            // data is already completely obtained before config change
            // by my previous self.
            // no need to create thread or show dialog at all
            mDataObject = (MyDataObject) retained;
            populateUI();
        } else if(mParserThread != null && mParserThread.isAlive()){
            // note: mParserThread is a static member or singleton object.
            // config changed during parsing in previous instance. swap handler
            // then wait for it to finish.
            mParserThread.setHandler(new MyHandler());
        } else {
            // no data and no thread. likely initial run
            // create thread, show dialog
            mParserThread = new MyThread(..., new MyHandler());
            mParserThread.start();
            showDialog(DIALOG_PROGRESS);
        }
}

// http://android-developers.blogspot.com/2009/02/faster-screen-orientation-change.html
public Object onRetainNonConfigurationInstance() {
        // my future self can get this without re-downloading
        // if it's already ready.
        return mDataObject;
}

// use Activity.showDialog instead of ProgressDialog.show
// so the dialog can be automatically managed across config change
@Override
protected Dialog onCreateDialog(int id) {
    // show progress dialog here
}

// inner class of MyActivity
private class MyHandler extends Handler {
    public void handleMessage(msg) {
        mDataObject = mParserThread.getDataObject();
        populateUI();
        dismissDialog(DIALOG_PROGRESS);
    }
}
}

class MyThread extends Thread {
    Handler mHandler;
    MyDataObject mDataObject;

    public MyHandler(..., Handler h) {...; mHandler = h;} // constructor with handler param
    public void setHandler(Handler h) { mHandler = h; } // for handler swapping after config change
    public MyDataObject getDataObject() { return mDataObject; } // return data object (completed) to caller
    public void run() {
        mDataObject = new MyDataObject();
        // do the lengthy task to fill mDataObject with data
        lengthyTask(mDataObject);
        // done. notify activity
        mHandler.sendEmptyMessage(0); // tell activity: i'm ready. come pick up the data.
    }
}

That's what works for me. I don't know if this is the "correct" method as designed by Android -- they claim this "destroy/recreate activity during screen rotation" actually makes things easier, so I guess it shouldn't be too tricky.

Let me know if you see a problem in my code. As said above I don't really know if there is any side effect.

samsonsu
Thanks a lot! The hint to `onRetainNonConfigurationInstance()` and `getLastNonConfigurationInstance()` helped me to solve my problem. Thumbs up!
sven
A: 

I've tried EVERYTHING. Spent days experimenting. I didn't want to block the activity from rotating. My scenario was:

  1. A progress dialog showing dynamic information to the user. E.g.: "Connecting to server...", "Downloading data...", etc.
  2. A thread doing the heavy stuff and updating the dialog
  3. Updating the UI with the results at the end.

The problem was, when rotating the screen, every solution on the book failed. Even with the AsyncTask class, which is the correct Android way of dealing with this situations. When rotating the screen, the current Context that the starting thread is working with, is gone, and that messes up with the dialog that is showing. The problem was always the Dialog, no matter how many tricks I added to the code (passing new contexts to running threads, retaining thread states through rotations, etc...). The code complexity at the end was always huge and there was always something that could go wrong.

The only solution that worked for me was the Activity/Dialog trick. It's simple and genius and it's all rotation proof:

  1. Instead of creating a Dialog and ask to show it, create an Activity that has been set in the manifest with android:theme="@android:style/Theme.Dialog". So, it just looks like a dialog.

  2. Replace showDialog(DIALOG_ID) with startActivityForResult(yourActivityDialog, yourCode);

  3. Use onActivityResult in the calling Activity to get the results from the executing thread (even the errors) and update the UI.

  4. On your 'ActivityDialog', use threads or AsyncTask to execute long tasks and onRetainNonConfigurationInstance to save "dialog" state when rotating the screen.

This is fast and works fine. I still use dialogs for other tasks and the AsyncTask for something that doesn't require a constant dialog on screen. But with this scenario, I always go for the Activity/Dialog pattern.

And, I didn't try it, but it's even possible to block that Activity/Dialog from rotating, when the thread is running, speeding things up, while allowing the calling Activity to rotate.

Rui
+13  A: 

You have to add this to the activity declaration in the manifest:

android:configChanges="orientation"

so it looks like

<activity android:label="@string/app_name" android:configChanges="orientation" 
        android:name=".your.package">

The matter is that the system destroys the activity when a change in the configuration occurs. See http://developer.android.com/reference/android/app/Activity.html#ConfigurationChanges.

So putting that in the configuration file avoids the system to destroy your activity. Instead it invokes the onConfigurationChanged(Configuration) method.

Hope to help you.

Visit http://blog.androidrunner.com !!

sonxurxo
Will this prevent the UI from changing layout?
Janusz
This is definitely the best solution; as it simply rotates the layout (the behavior you expect in first place). Just be sure to put android:configChanges="orientation|keyboardHidden" (because of phones that have landscape keyboard)
kape123
A: 

the

android:configChanges="orientation"

(in my AndroidManifest.xml element)

solution worked beautifully for me in my TabManager based layout.

Thanks sonxurxo!

Andrew Sotzing
+2  A: 

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.

Heikki Toivonen
A: 

I faced this same problem, and I came up with a solution that didn't invole using the ProgressDialog and I get faster results.

What I did was create a layout that has a ProgressBar in it.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ProgressBar
    android:id="@+id/progressImage"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    />
</RelativeLayout>

Then in the onCreate method do the following

public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    setContentView(R.layout.progress);
}

Then do the long task in a thread, and when that's finished have a Runnable set the content view to the real layout you want to use for this activity.

For example:

mHandler.post(new Runnable(){

public void run() {
        setContentView(R.layout.my_layout);
    } 
});

This is what I did, and I've found that it runs faster than showing the ProgressDialog and it's less intrusive and has a better look in my opinion.

However, if you're wanting to use the ProgressDialog, then this answer isn't for you.

Pzanno