How to handle screen orientation change when progr

2018-12-31 08:51发布

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.

26条回答
ら面具成の殇う
2楼-- · 2018-12-31 08:59

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 progress dialog. I let the thread post a message 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 a lot of 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;

    // constructor with handler param
    public MyHandler(..., Handler h) {
        ...
        mHandler = h;
    }

    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.

查看更多
君临天下
3楼-- · 2018-12-31 09:00

Tried to implement jfelectron's solution because it is a "rock-solid solution to these issues that conforms with the 'Android Way' of things" but it took some time to look up and put together all the elements mentioned. Ended up with this slightly different, and I think more elegant, solution posted here in it's entirety.

Uses an IntentService fired from an activity to perform the long running task on a separate thread. The service fires back sticky Broadcast Intents to the activity which update the dialog. The Activity uses showDialog(), onCreateDialog() and onPrepareDialog() to eliminate the need to have persistent data passed in the application object or the savedInstanceState bundle. This should work no matter how your application is interrupted.

Activity Class:

public class TesterActivity extends Activity {
private ProgressDialog mProgressDialog;
private static final int PROGRESS_DIALOG = 0;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    Button b = (Button) this.findViewById(R.id.test_button);
    b.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            buttonClick();
        }
    });
}

private void buttonClick(){
    clearPriorBroadcast();
    showDialog(PROGRESS_DIALOG);
    Intent svc = new Intent(this, MyService.class);
    startService(svc);
}

protected Dialog onCreateDialog(int id) {
    switch(id) {
    case PROGRESS_DIALOG:
        mProgressDialog = new ProgressDialog(TesterActivity.this);
        mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        mProgressDialog.setMax(MyService.MAX_COUNTER);
        mProgressDialog.setMessage("Processing...");
        return mProgressDialog;
    default:
        return null;
    }
}

@Override
protected void onPrepareDialog(int id, Dialog dialog) {
    switch(id) {
    case PROGRESS_DIALOG:
        // setup a broadcast receiver to receive update events from the long running process
        IntentFilter filter = new IntentFilter();
        filter.addAction(MyService.BG_PROCESS_INTENT);
        registerReceiver(new MyBroadcastReceiver(), filter);
        break;
    }
}

public class MyBroadcastReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.hasExtra(MyService.KEY_COUNTER)){
            int count = intent.getIntExtra(MyService.KEY_COUNTER, 0);
            mProgressDialog.setProgress(count);
            if (count >= MyService.MAX_COUNTER){
                dismissDialog(PROGRESS_DIALOG);
            }
        }
    }
}

/*
 * Sticky broadcasts persist and any prior broadcast will trigger in the 
 * broadcast receiver as soon as it is registered.
 * To clear any prior broadcast this code sends a blank broadcast to clear 
 * the last sticky broadcast.
 * This broadcast has no extras it will be ignored in the broadcast receiver 
 * setup in onPrepareDialog()
 */
private void clearPriorBroadcast(){
    Intent broadcastIntent = new Intent();
    broadcastIntent.setAction(MyService.BG_PROCESS_INTENT);
    sendStickyBroadcast(broadcastIntent);
}}

IntentService Class:

public class MyService extends IntentService {

public static final String BG_PROCESS_INTENT = "com.mindspiker.Tester.MyService.TEST";
public static final String KEY_COUNTER = "counter";
public static final int MAX_COUNTER = 100;

public MyService() {
  super("");
}

@Override
protected void onHandleIntent(Intent intent) {
    for (int i = 0; i <= MAX_COUNTER; i++) {
        Log.e("Service Example", " " + i);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Intent broadcastIntent = new Intent();
        broadcastIntent.setAction(BG_PROCESS_INTENT);
        broadcastIntent.putExtra(KEY_COUNTER, i);
        sendStickyBroadcast(broadcastIntent);
    }
}}

Manifest file entries:

before application section:

uses-permission android:name="com.mindspiker.Tester.MyService.TEST"
uses-permission android:name="android.permission.BROADCAST_STICKY"

inside application section

service android:name=".MyService"
查看更多
零度萤火
4楼-- · 2018-12-31 09:05

My solution was to extend the ProgressDialog class to get my own MyProgressDialog.
I redefined show() and dismiss() methods to lock the orientation before showing the Dialog and unlock it back when Dialog is dismissed. So when the Dialog is shown and the orientation of the device changes, the orientation of the screen remains until dismiss() is called, then screen-orientation changes according to sensor-values/device-orientation.

Here is my code:

public class MyProgressDialog extends ProgressDialog {
private Context mContext;

public MyProgressDialog(Context context) {
    super(context);
    mContext = context;
}

public MyProgressDialog(Context context, int theme) {
    super(context, theme);
    mContext = context;
}

public void show() {
    if (mContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT)
        ((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    else
        ((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
    super.show();
}

public void dismiss() {
    super.dismiss();
    ((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
}

}
查看更多
何处买醉
5楼-- · 2018-12-31 09:05

These days there is a much more distinct way to handle these types of issues. The typical approach is:

1. Ensure your data is properly seperated from the UI:

Anything that is a background process should be in a retained Fragment (set this with Fragment.setRetainInstance(). This becomes your 'persistent data storage' where anything data based that you would like retained is kept. After the orientation change event, this Fragment will still be accessible in its original state through a FragmentManager.findFragmentByTag() call (when you create it you should give it a tag not an ID as it is not attached to a View).

See the Handling Runtime Changes developed guide for information about doing this correctly and why it is the best option.

2. Ensure you are interfacing correctly and safely between the background processs and your UI:

You must reverse your linking process. At the moment your background process attaches itself to a View - instead your View should be attaching itself to the background process. It makes more sense right? The View's action is dependent on the background process, whereas the background process is not dependent on the View.This means changing the link to a standard Listener interface. Say your process (whatever class it is - whether it is an AsyncTask, Runnable or whatever) defines a OnProcessFinishedListener, when the process is done it should call that listener if it exists.

This answer is a nice concise description of how to do custom listeners.

3. Link your UI into the data process whenever the UI is created (including orientation changes):

Now you must worry about interfacing the background task with whatever your current View structure is. If you are handling your orientation changes properly (not the configChanges hack people always recommend), then your Dialog will be recreated by the system. This is important, it means that on the orientation change, all your Dialog's lifecycle methods are recalled. So in any of these methods (onCreateDialog is usually a good place), you could do a call like the following:

DataFragment f = getActivity().getFragmentManager().findFragmentByTag("BACKGROUND_TAG");
if (f != null) {
    f.mBackgroundProcess.setOnProcessFinishedListener(new OnProcessFinishedListener() {
        public void onProcessFinished() {
            dismiss();
        }
    });
 }

See the Fragment lifecycle for deciding where setting the listener best fits in your individual implementation.

This is a general approach to providing a robust and complete solution to the generic problem asked in this question. There is probably a few minor pieces missing in this answer depending on your individual scenario, but this is generally the most correct approach for properly handling orientation change events.

查看更多
伤终究还是伤i
6楼-- · 2018-12-31 09:06

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.

查看更多
大哥的爱人
7楼-- · 2018-12-31 09:08

The original perceived problem was that the code would not survive a screen orientation change. Apparently this was "solved" by having the program handle the screen orientation change itself, instead of letting the UI framework do it (via calling onDestroy)).

I would submit that if the underlying problem is that the program will not survive onDestroy(), then the accepted solution is just a workaround that leaves the program with serious other problems and vulnerabilities. Remember that the Android framework specifically states that your activity is at risk for being destroyed almost at any time due to circumstances outside your control. Therefore, your activity must be able to survive onDestroy() and subsequent onCreate() for any reason, not just a screen orientation change.

If you are going to accept handling screen orientation changes yourself to solve the OP's problem, you need to verify that other causes of onDestroy() do not result in the same error. Are you able to do this? If not, I would question whether the "accepted" answer is really a very good one.

查看更多
登录 后发表回答