103

Slight variation on my other posting

Basically I have a message Handler in my Fragment which receives a bunch of messages that can result in dialogs being dismissed or shown.

When the app is put into the background I get an onPause but then still get my messages coming through as one would expect. However, because I'm using fragments I can't just dismiss and show dialogs as that will result in an IllegalStateException.

I can't just dismiss or cancel allowing state loss.

Given that I have a Handler I'm wondering whether there is a recommended approach as to how I should handle messages while in a paused state.

One possible solution I'm considering is to record the messages coming through while paused and play them back on an onResume. This is somewhat unsatisfactory and I'm thinking that there must be something in the framework to handle this more elegantly.

Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
PJL
  • 18,735
  • 17
  • 71
  • 68
  • 1
    you could remove all the messages in the handler in the onPause() method of fragment, but there is a problem of restoring the messages which i think is not possible. – Yashwanth Kumar Nov 10 '11 at 12:49

4 Answers4

170

Although the Android operating system does not appear to have a mechanism that sufficiently addresses your problem I believe this pattern does provide a relatively simple to implement workaround.

The following class is a wrapper around android.os.Handler that buffers up messages when an activity is paused and plays them back on resume.

Ensure any code that you have which asynchronously changes a fragment state (e.g. commit, dismiss) is only called from a message in the handler.

Derive your handler from the PauseHandler class.

Whenever your activity receives an onPause() call PauseHandler.pause() and for onResume() call PauseHandler.resume().

Replace your implementation of the Handler handleMessage() with processMessage().

Provide a simple implementation of storeMessage() which always returns true.

/**
 * Message Handler class that supports buffering up of messages when the
 * activity is paused i.e. in the background.
 */
public abstract class PauseHandler extends Handler {

    /**
     * Message Queue Buffer
     */
    final Vector<Message> messageQueueBuffer = new Vector<Message>();

    /**
     * Flag indicating the pause state
     */
    private boolean paused;

    /**
     * Resume the handler
     */
    final public void resume() {
        paused = false;

        while (messageQueueBuffer.size() > 0) {
            final Message msg = messageQueueBuffer.elementAt(0);
            messageQueueBuffer.removeElementAt(0);
            sendMessage(msg);
        }
    }

    /**
     * Pause the handler
     */
    final public void pause() {
        paused = true;
    }

    /**
     * Notification that the message is about to be stored as the activity is
     * paused. If not handled the message will be saved and replayed when the
     * activity resumes.
     * 
     * @param message
     *            the message which optional can be handled
     * @return true if the message is to be stored
     */
    protected abstract boolean storeMessage(Message message);

    /**
     * Notification message to be processed. This will either be directly from
     * handleMessage or played back from a saved message when the activity was
     * paused.
     * 
     * @param message
     *            the message to be handled
     */
    protected abstract void processMessage(Message message);

    /** {@inheritDoc} */
    @Override
    final public void handleMessage(Message msg) {
        if (paused) {
            if (storeMessage(msg)) {
                Message msgCopy = new Message();
                msgCopy.copyFrom(msg);
                messageQueueBuffer.add(msgCopy);
            }
        } else {
            processMessage(msg);
        }
    }
}

Below is a simple example of how the PausedHandler class can be used.

On the click of a button a delayed message is sent to the handler.

When the handler receives the message (on the UI thread) it displays a DialogFragment.

If the PausedHandler class was not being used an IllegalStateException would be shown if the home button was pressed after pressing the test button to launch the dialog.

public class FragmentTestActivity extends Activity {

    /**
     * Used for "what" parameter to handler messages
     */
    final static int MSG_WHAT = ('F' << 16) + ('T' << 8) + 'A';
    final static int MSG_SHOW_DIALOG = 1;

    int value = 1;

    final static class State extends Fragment {

        static final String TAG = "State";
        /**
         * Handler for this activity
         */
        public ConcreteTestHandler handler = new ConcreteTestHandler();

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

        @Override
        public void onResume() {
            super.onResume();

            handler.setActivity(getActivity());
            handler.resume();
        }

        @Override
        public void onPause() {
            super.onPause();

            handler.pause();
        }

        public void onDestroy() {
            super.onDestroy();
            handler.setActivity(null);
        }
    }

    /**
     * 2 second delay
     */
    final static int DELAY = 2000;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        if (savedInstanceState == null) {
            final Fragment state = new State();
            final FragmentManager fm = getFragmentManager();
            final FragmentTransaction ft = fm.beginTransaction();
            ft.add(state, State.TAG);
            ft.commit();
        }

        final Button button = (Button) findViewById(R.id.popup);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                final FragmentManager fm = getFragmentManager();
                State fragment = (State) fm.findFragmentByTag(State.TAG);
                if (fragment != null) {
                    // Send a message with a delay onto the message looper
                    fragment.handler.sendMessageDelayed(
                            fragment.handler.obtainMessage(MSG_WHAT, MSG_SHOW_DIALOG, value++),
                            DELAY);
                }
            }
        });
    }

    public void onSaveInstanceState(Bundle bundle) {
        super.onSaveInstanceState(bundle);
    }

    /**
     * Simple test dialog fragment
     */
    public static class TestDialog extends DialogFragment {

        int value;

        /**
         * Fragment Tag
         */
        final static String TAG = "TestDialog";

        public TestDialog() {
        }

        public TestDialog(int value) {
            this.value = value;
        }

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

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            final View inflatedView = inflater.inflate(R.layout.dialog, container, false);
            TextView text = (TextView) inflatedView.findViewById(R.id.count);
            text.setText(getString(R.string.count, value));
            return inflatedView;
        }
    }

    /**
     * Message Handler class that supports buffering up of messages when the
     * activity is paused i.e. in the background.
     */
    static class ConcreteTestHandler extends PauseHandler {

        /**
         * Activity instance
         */
        protected Activity activity;

        /**
         * Set the activity associated with the handler
         * 
         * @param activity
         *            the activity to set
         */
        final void setActivity(Activity activity) {
            this.activity = activity;
        }

        @Override
        final protected boolean storeMessage(Message message) {
            // All messages are stored by default
            return true;
        };

        @Override
        final protected void processMessage(Message msg) {

            final Activity activity = this.activity;
            if (activity != null) {
                switch (msg.what) {

                case MSG_WHAT:
                    switch (msg.arg1) {
                    case MSG_SHOW_DIALOG:
                        final FragmentManager fm = activity.getFragmentManager();
                        final TestDialog dialog = new TestDialog(msg.arg2);

                        // We are on the UI thread so display the dialog
                        // fragment
                        dialog.show(fm, TestDialog.TAG);
                        break;
                    }
                    break;
                }
            }
        }
    }
}

I've added a storeMessage() method to the PausedHandler class in case any messages should be processed immediately even when the activity is paused. If a message is handled then false should be returned and the message will be discarded.

Sufian
  • 6,405
  • 16
  • 66
  • 120
quickdraw mcgraw
  • 2,246
  • 1
  • 15
  • 14
  • 27
    Nice solution, works a treat. Can't help thinking though that the framework should be handling this. – PJL Nov 16 '11 at 18:50
  • 1
    how to pass callback to DialogFragment? – Malachiasz Dec 20 '13 at 11:47
  • I'm not sure that I understand the question Malachiasz, please could you elaborate. – quickdraw mcgraw Mar 26 '14 at 11:19
  • This is a very elegant solution! Unless I am wrong, because the `resume` method uses `sendMessage(msg)` technically there could be other threads enqueuing message right before (or in between iterations of the loop), which means that the messages stored could be interleaved with new messages arriving. Not sure if it is a big deal. Maybe using `sendMessageAtFrontOfQueue` (and of course iterating backward) would solve this issue? – yan May 12 '14 at 00:27
  • does this still delete messages if we call `handler.removeCallbacksAndMessages(null)` ? – Zyoo Jun 11 '14 at 09:46
  • `If the PausedHandler class was not being used an IllegalStateException would be shown if the home button was pressed after pressing the test button to launch the dialog.` Could you explain this? I opened your solution after getting `IllegalStateException`s described [here](http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html) – Maksim Dmitriev Sep 09 '14 at 10:32
  • A fragment transation when the activity is in a paused state will cause an illegalstateexception. – quickdraw mcgraw Sep 17 '14 at 18:28
  • 4
    I think this approach may not always work - if the activity is destroyed by the OS the list of messages pending to be processes will be empty after on resume. – GaRRaPeTa Jun 08 '15 at 11:09
  • @quickdraw mcgraw Really elegant solution! But I'm a little bit confused. Why do you use thread-safe vector? It seems that PauseHandler class is already thread safe until it is used only from UI thread. Explain me please, should I use synchronization or not if will use PauseHandler only through UI thread? – Samik Sep 29 '15 at 09:44
  • Live sample code (I created because I wanted to see how it works) : https://github.com/KENJU/FragmentLifecycleSample – kenju Oct 06 '15 at 07:16
  • I'm implementing this PauseHandler but have run into a problem at the very final stage, launching the dialog box. I've posted a question here: http://stackoverflow.com/q/38553430/1977132 If anyone has any ideas I'd be very grateful! – user1977132 Jul 26 '16 at 09:35
  • To avoid **IllegalstateException** when doing _fragment transaction_ in `processMessage(Message msg)` we should move `handler.pause()` from `onPause` to `onSaveInstanceState` activity callback. – isabsent Feb 07 '17 at 11:01
10

A slightly simpler version of quickdraw's excellent PauseHandler is

/**
 * Message Handler class that supports buffering up of messages when the activity is paused i.e. in the background.
 */
public abstract class PauseHandler extends Handler {

    /**
     * Message Queue Buffer
     */
    private final List<Message> messageQueueBuffer = Collections.synchronizedList(new ArrayList<Message>());

    /**
     * Flag indicating the pause state
     */
    private Activity activity;

    /**
     * Resume the handler.
     */
    public final synchronized void resume(Activity activity) {
        this.activity = activity;

        while (messageQueueBuffer.size() > 0) {
            final Message msg = messageQueueBuffer.get(0);
            messageQueueBuffer.remove(0);
            sendMessage(msg);
        }
    }

    /**
     * Pause the handler.
     */
    public final synchronized void pause() {
        activity = null;
    }

    /**
     * Store the message if we have been paused, otherwise handle it now.
     *
     * @param msg   Message to handle.
     */
    @Override
    public final synchronized void handleMessage(Message msg) {
        if (activity == null) {
            final Message msgCopy = new Message();
            msgCopy.copyFrom(msg);
            messageQueueBuffer.add(msgCopy);
        } else {
            processMessage(activity, msg);
        }
    }

    /**
     * Notification message to be processed. This will either be directly from
     * handleMessage or played back from a saved message when the activity was
     * paused.
     *
     * @param activity  Activity owning this Handler that isn't currently paused.
     * @param message   Message to be handled
     */
    protected abstract void processMessage(Activity activity, Message message);

}

It does assume that you always want to store offline messages for replay. And provides the Activity as input to #processMessages so you don't need to manage it in the sub class.

William
  • 20,150
  • 8
  • 49
  • 91
  • Why are your `resume()` and `pause()`, and `handleMessage` `synchronized`? – Maksim Dmitriev Oct 29 '14 at 15:06
  • 5
    Because you don't want #pause to be called during #handleMessage and suddenly find that activity is null while you are using it in #handleMessage. It's a synchronisation across shared state. – William Oct 30 '14 at 18:31
  • @William Could you explain me please more details why do you need synchronization in a PauseHandler class ? It seems that this class works only in one thread, UI thread. I guess that #pause couldn't be called during #handleMessage because both of them works in UI thread. – Samik Sep 29 '15 at 09:49
  • @William are you sure? HandlerThread handlerThread = new HandlerThread("mHandlerNonMainThread"); handlerThread.start(); Looper looperNonMainThread = handlerThread.getLooper(); Handler handlerNonMainThread = new Handler(looperNonMainThread, new Callback() { public boolean handleMessage(Message msg) { return false; } }); – swooby May 05 '16 at 00:25
  • Sorry @swooby I don;t follow. Am I sure about what? And what is the purpose of the code snippet you posted? – William May 05 '16 at 08:46
  • @William Are you sure that you don't have to "worry if the Thread for a local Handler and the Activity framework are the same or different". Your resume, pause, and handleMessage methods could be called by a thread other than the main thread. – swooby May 05 '16 at 23:33
2

Here is a slightly different way to approach the problem of doing Fragment commits in a callback function and avoiding the IllegalStateException issue.

First create a custom runnable interface.

public interface MyRunnable {
    void run(AppCompatActivity context);
}

Next, create a fragment for processing the MyRunnable objects. If the MyRunnable object was created after the Activity was paused, for e.g. if the screen is rotated, or the user presses the home button, it is put in a queue for later processing with a new context. The queue survives any configuration changes because setRetain instance is set to true. The method runProtected runs on UI thread to avoid a race condition with the isPaused flag.

public class PauseHandlerFragment extends Fragment {

    private AppCompatActivity context;
    private boolean isPaused = true;
    private Vector<MyRunnable> buffer = new Vector<>();

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        this.context = (AppCompatActivity)context;
    }

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

    @Override
    public void onPause() {
        isPaused = true;
        super.onPause();
    }

    @Override
    public void onResume() {
        isPaused = false;
        playback();
        super.onResume();
    }

    private void playback() {
        while (buffer.size() > 0) {
            final MyRunnable runnable = buffer.elementAt(0);
            buffer.removeElementAt(0);
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    //execute run block, providing new context, incase 
                    //Android re-creates the parent activity
                    runnable.run(context);
                }
            });
        }
    }
    public final void runProtected(final MyRunnable runnable) {
        context.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if(isPaused) {
                    buffer.add(runnable);
                } else {
                    runnable.run(context);
                }
            }
        });
    }
}

Finally, the fragment may be used in a main application as follows:

public class SomeActivity extends AppCompatActivity implements SomeListener {
    PauseHandlerFragment mPauseHandlerFragment;

    static class Storyboard {
        public static String PAUSE_HANDLER_FRAGMENT_TAG = "phft";
    }

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        ...

        //register pause handler 
        FragmentManager fm = getSupportFragmentManager();
        mPauseHandlerFragment = (PauseHandlerFragment) fm.
            findFragmentByTag(Storyboard.PAUSE_HANDLER_FRAGMENT_TAG);
        if(mPauseHandlerFragment == null) {
            mPauseHandlerFragment = new PauseHandlerFragment();
            fm.beginTransaction()
                .add(mPauseHandlerFragment, Storyboard.PAUSE_HANDLER_FRAGMENT_TAG)
                .commit();
        }

    }

    // part of SomeListener interface
    public void OnCallback(final String data) {
        mPauseHandlerFragment.runProtected(new MyRunnable() {
            @Override
            public void run(AppCompatActivity context) {
                //this block of code should be protected from IllegalStateException
                FragmentManager fm = context.getSupportFragmentManager();
                ...
            }
         });
    }
}
Rua109
  • 21
  • 2
0

In my projects I use the observer design pattern to solve this. In Android, broadcast receivers and intents are an implemenation of this pattern.

What I do is create a BroadcastReceiver which I register in fragment's/activity's onResume and unregister in fragment's/activity's onPause. In BroadcastReceiver's method onReceive I put all code that needs to run as result of - the BroadcastReceiver - receiving an Intent(message) that was sent to your app in general. To increase selectivity on what type of intents your fragment can receive you can use an intent filter as in the example below.

An advantage of this approach is that the Intent(message) can be sent from everywhere whithin your app(a dialog that opened on top of your fragment, an async task, another fragment etc.). Parameters can even passed as intent extras.

Another advantage is that this approach is compatible with any Android API version, since BroadcastReceivers and Intents have been introduced on API level 1.

Your are not required to setup any special permissions on your app's manifest file except if you plan to use sendStickyBroadcast(where you need to add BROADCAST_STICKY).

public class MyFragment extends Fragment { 

    public static final String INTENT_FILTER = "gr.tasos.myfragment.refresh";

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {

        // this always runs in UI Thread 
        @Override
        public void onReceive(Context context, Intent intent) {
            // your UI related code here

            // you can receiver data login with the intent as below
            boolean parameter = intent.getExtras().getBoolean("parameter");
        }
    };

    public void onResume() {
        super.onResume();
        getActivity().registerReceiver(mReceiver, new IntentFilter(INTENT_FILTER));

    };

    @Override
    public void onPause() {
        getActivity().unregisterReceiver(mReceiver);
        super.onPause();
    }

    // send a broadcast that will be "caught" once the receiver is up
    protected void notifyFragment() {
        Intent intent = new Intent(SelectCategoryFragment.INTENT_FILTER);
        // you can send data to receiver as intent extras
        intent.putExtra("parameter", true);
        getActivity().sendBroadcast(intent);
    }

}
dangel
  • 1,506
  • 15
  • 10
  • 3
    If sendBroadcast() in notifyFragment() is called during the Pause state, unregisterReceiver() will have already been called and thus no receiver will be around to catch that intent. Won't the Android system then discard the intent if there is no code to immediately handle it? – Steve B Mar 23 '15 at 05:12
  • i think green robots eventbus sticky posts are like this, cool. – j2emanue Aug 28 '17 at 02:52