55
votes

These days I'm working on simulating modal dialog in Android. I've googled a lot, there's much discussions but sadly there's not much options to get it modal. Here's some background,
Dialogs, Modal Dialogs and Blockin
Dialogs / AlertDialogs: How to "block execution" while dialog is up (.NET-style)

There's no straight way to get modal behavior, then I came up with 3 possible solutions,
1. Use a dialog-themed activity, like this thread said, but I still can't make main activity truly wait for dialog-activity return. Main activity turned to stop status and got restarted then.
2. Build one worker thread, and use thread synchronization. However, it's a huge refactoring job for my app, now I have a single main activity and a service both in main UI thread.
3. Take over event handling within a loop when there is a modal dialog up, and quit loop when dialog gets closed. Actually it's the way to build a real modal dialog like what it exactly does in Windows. I still haven't prototyped this way.

I'd still like to simulate it with a dialog-themed activity,
1. start dialog-activity by startActivityForResult()
2. get result from onActivityResult()
Here's some source

public class MainActivity extends Activity {

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

    MyView v = new MyView(this);
    setContentView(v);
}

private final int RESULT_CODE_ALERT = 1;
private boolean mAlertResult = false;
public boolean startAlertDialog() {
    Intent it = new Intent(this, DialogActivity.class);
    it.putExtra("AlertInfo", "This is an alert");
    startActivityForResult(it, RESULT_CODE_ALERT);

    // I want to wait right here
    return mAlertResult;
}

@Override
protected void onActivityResult (int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
    case RESULT_CODE_ALERT:
        Bundle ret = data.getExtras();
        mAlertResult = ret.getBoolean("AlertResult");
        break;
    }
}
}

The caller of startAlertDialog will block execution and expect returned result. But startAlertDialog returned immediately of course, and main activity went into STOP status while DialogActivity was up.

So the question is, how to make main activity really wait for result?
Thanks.

11
This post may help you. stackoverflow.com/questions/2028697/…Daniel
I can't really believe Android sucks on this very simple things.LEMUEL ADANE
The first link in your question explains that Android does have model dialogs (but not blocking ones). Your question be a lot clearer if you changed your terminology to reflect what you were really asking for - thread blocking.LarsH
Use a broadcast receiver that calls the next method in the chain... dead end the code until the method is inoked.me_
Shame for Google that Android does not support modal dialogs. I believe they should solve this problem. We as programmers should persuade Google to do the right things rather than to re-invent a modal like dialog. The modal dialog is already there and implemented by Microsoft in Windows. Why not implemented by Google? Because Google engineers are lazy ;-)saeed khalafinejad

11 Answers

71
votes

I got a modal Dialog while using:

setCancelable(false);

on the DialogFragment (not on the DialogBuilder).

16
votes

It is not possible the way you planned. First, you are not allowed to block the UI thread. Your application will be terminated. Second, need to handle the lifecycle methods that are called when another activity is started with startActivity (your original acitvity will be paused while the other activity is running). Third, you probably could somehow hack it by using startAlertDialog() not from the UI thread, with thread synchronization (like Object.wait()) and some AlertDialog. However, I strongly encourage you to not do this. It is ugly, will certainly break and it's just not the way things are intended to work.

Redesign your approach to capture the asynchronous nature of these events. If you want for example some dialog which asks the user for a decsision (like accepting the ToS or not) and do special actions based on that decision create a dialog like this:

AlertDialog dialog = new AlertDialog.Builder(context).setMessage(R.string.someText)
                .setPositiveButton(android.R.string.ok, new OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                        // Do stuff if user accepts
                    }
                }).setNegativeButton(android.R.string.cancel, new OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                        // Do stuff when user neglects.
                    }
                }).setOnCancelListener(new OnCancelListener() {

                    @Override
                    public void onCancel(DialogInterface dialog) {
                        dialog.dismiss();
                        // Do stuff when cancelled
                    }
                }).create();
dialog.show();

Then have two methods handling positive or negative feedback accordingly (i.e. proceeding with some operation or finishing the activity or whatever makes sense).

8
votes

Developers of Android and iOS decided that they are powerful and smart enough to reject Modal Dialog conception (that was on market for many-many years already and didn't bother anyone before), unfortunately for us.

Here is my solution, it works great:

    int pressedButtonID;
    private final Semaphore dialogSemaphore = new Semaphore(0, true);
    final Runnable mMyDialog = new Runnable()
    {
        public void run()
        {
            AlertDialog errorDialog = new AlertDialog.Builder( [your activity object here] ).create();
            errorDialog.setMessage("My dialog!");
            errorDialog.setButton("My Button1", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    pressedButtonID = MY_BUTTON_ID1;
                    dialogSemaphore.release();
                    }
                });
            errorDialog.setButton2("My Button2", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    pressedButtonID = MY_BUTTON_ID2;
                    dialogSemaphore.release();
                    }
                });
            errorDialog.setCancelable(false);
            errorDialog.show();
        }
    };

    public int ShowMyModalDialog()  //should be called from non-UI thread
    {
        pressedButtonID = MY_BUTTON_INVALID_ID;
        runOnUiThread(mMyDialog);
        try
        {
            dialogSemaphore.acquire();
        }
        catch (InterruptedException e)
        {
        }
        return pressedButtonID;
    }
6
votes

Finally I ended up with a really straight and simple solution.

People who's familiar with Win32 programming possibly knows how to implement a modal dialog. Generally it runs a nested message loop (by GetMessage/PostMessage) when there is a modal dialog up. So, I tried to implement my own modal dialog in this traditional way.

At the first, android didn't provide interfaces to inject into ui thread message loop, or I didn't find one. When I looked into source, Looper.loop(), I found it's exactly what I wanted. But still, MessageQueue/Message haven't provided public interfaces. Fortunately, we have reflection in java. Basically, I just copied exactly what Looper.loop() did, it blocked workflow and still properly handled events. I haven't tested nested modal dialog, but theoretically it would work.

Here's my source code,

public class ModalDialog {

private boolean mChoice = false;        
private boolean mQuitModal = false;     

private Method mMsgQueueNextMethod = null;
private Field mMsgTargetFiled = null;

public ModalDialog() {
}

public void showAlertDialog(Context context, String info) {
    if (!prepareModal()) {
        return;
    }

    // build alert dialog
    AlertDialog.Builder builder = new AlertDialog.Builder(context);
    builder.setMessage(info);
    builder.setCancelable(false);
    builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            ModalDialog.this.mQuitModal = true;
            dialog.dismiss();
        }
    });

    AlertDialog alert = builder.create();
    alert.show();

    // run in modal mode
    doModal();
}

public boolean showConfirmDialog(Context context, String info) {
    if (!prepareModal()) {
        return false;
    }

    // reset choice
    mChoice = false;

    AlertDialog.Builder builder = new AlertDialog.Builder(context);
    builder.setMessage(info);
    builder.setCancelable(false);
    builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            ModalDialog.this.mQuitModal = true;
            ModalDialog.this.mChoice = true;
            dialog.dismiss();
        }
    });

    builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            ModalDialog.this.mQuitModal = true;
            ModalDialog.this.mChoice = false;
            dialog.cancel();
        }
    });

    AlertDialog alert = builder.create();
    alert.show();

    doModal();
    return mChoice;
}

private boolean prepareModal() {
    Class<?> clsMsgQueue = null;
    Class<?> clsMessage = null;

    try {
        clsMsgQueue = Class.forName("android.os.MessageQueue");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
        return false;
    }

    try {
        clsMessage = Class.forName("android.os.Message");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
        return false;
    }

    try {
        mMsgQueueNextMethod = clsMsgQueue.getDeclaredMethod("next", new Class[]{});
    } catch (SecurityException e) {
        e.printStackTrace();
        return false;
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
        return false;
    }

    mMsgQueueNextMethod.setAccessible(true);

    try {
        mMsgTargetFiled = clsMessage.getDeclaredField("target");
    } catch (SecurityException e) {
        e.printStackTrace();
        return false;
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
        return false;
    }

    mMsgTargetFiled.setAccessible(true);
    return true;
}

private void doModal() {
    mQuitModal = false;

    // get message queue associated with main UI thread
    MessageQueue queue = Looper.myQueue();
    while (!mQuitModal) {
        // call queue.next(), might block
        Message msg = null;
        try {
            msg = (Message)mMsgQueueNextMethod.invoke(queue, new Object[]{});
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        if (null != msg) {
            Handler target = null;
            try {
                target = (Handler)mMsgTargetFiled.get(msg);
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

            if (target == null) {
                // No target is a magic identifier for the quit message.
                mQuitModal = true;
            }

            target.dispatchMessage(msg);
            msg.recycle();
        }
    }
}
}

Hopefully this would help.

6
votes

This works for me: create an Activity as your dialog. Then,

  1. Add this to your manifest for the activity:

    android:theme="@android:style/Theme.Dialog"

  2. Add this to onCreate of your activity

    setFinishOnTouchOutside (false);

  3. Override onBackPressed in your activity:

    @Override public void onBackPressed() { // prevent "back" from leaving this activity }

The first gives the activity the dialog look. The latter two make it behave like a modal dialog.

2
votes

As hackbod and others have pointed out, Android deliberately doesn't provide a method for doing nested event loops. I understand the reasons for this, but there are certain situations that require them. In our case we have our own virtual machine running on various platforms and we wanted to port it to Android. Internally there a lot of places where it requires a nested event loop, and it isn't really feasible to rewrite the whole thing just for Android. Anyway, here is a solution (basically taken from How can I do non-blocking events processing on Android?, but I have added a timeout):

private class IdleHandler implements MessageQueue.IdleHandler
{
    private Looper _looper;
    private int _timeout;
    protected IdleHandler(Looper looper, int timeout)
    {
        _looper = looper;
        _timeout = timeout;
    }

    public boolean queueIdle()
    {
        _uiEventsHandler = new Handler(_looper);
        if (_timeout > 0)
        {
            _uiEventsHandler.postDelayed(_uiEventsTask, _timeout);
        }
        else
        {
            _uiEventsHandler.post(_uiEventsTask);
        }
        return(false);
    }
};

private boolean _processingEventsf = false;
private Handler _uiEventsHandler = null;

private Runnable _uiEventsTask = new Runnable()
{
    public void run() {
    Looper looper = Looper.myLooper();
    looper.quit();
    _uiEventsHandler.removeCallbacks(this);
    _uiEventsHandler = null;
    }
};

public void processEvents(int timeout)
{
    if (!_processingEventsf)
    {
        Looper looper = Looper.myLooper();
        looper.myQueue().addIdleHandler(new IdleHandler(looper, timeout));
        _processingEventsf = true;
        try
        {
            looper.loop();
        } catch (RuntimeException re)
        {
            // We get an exception when we try to quit the loop.
        }
        _processingEventsf = false;
     }
}
2
votes

I have a similar solution like fifth, but its a little bit simpler and doesn't need reflection. My thinking was, why not use an exception to exit the looper. So my custom looper reads as follows:

1) The exception that is thrown:

final class KillException extends RuntimeException {
}

2) The custom looper:

public final class KillLooper implements Runnable {
    private final static KillLooper DEFAULT = new KillLooper();

    private KillLooper() {
    }

    public static void loop() {
        try {
            Looper.loop();
        } catch (KillException x) {
            /* */
        }
    }

    public static void quit(View v) {
        v.post(KillLooper.DEFAULT);
    }

    public void run() {
        throw new KillException();
    }

}

The use of the custom looper is quite simple. Suppose you have a dialog foo, then simply do the following where you want to call the dialog foo modally:

a) When calling into foo:

foo.show();
KillLooper.loop();

Inside the dialog foo, when you want to exit, you simply call the quit method of the custom looper. This looks as follows:

b) When exiting from foo:

dismiss();
KillLooper.quit(getContentView());

I have recently seen some problems with 5.1.1 Android, do not call a modal dialog from main menu, instead post an event that calls the modal dialog. Without posting the main menu will stall, and I have seen Looper::pollInner() SIGSEGVs in my app.

1
votes

It's not difficult.

Assume you have a flag on your owner activity (named waiting_for_result), whenever your activity is resumed:

public void onResume(){
    if (waiting_for_result) {
        // Start the dialog Activity
    }
}

This guaranteed the owner activity, unless the modal dialog is dismissed, whenever it try to get focus will pass to the modal dialog activity.

1
votes

One solution is :

  1. Put all code for each selected button into the listener of each button.
  2. alert.show(); must be the last code line in the function calling the Alert. Any code after this line will not wait to close the Alert, but will execute immediately.

Hope Help!

1
votes

I am not sure if this is 100% modal, as you can click on some other component to close the dialog box, but I got confused with the loops constructs and so I offer this as another possibility. It worked nicely for me, so I would like to share the idea. You can create and open the dialog box in one method and then close it in the callback method and the program will wait for the dialog reply before executing the callback method. If you then run the rest of the callback method in a new thread, the dialog box will also close first, before the rest of the code is executed. The only thing you need to do is to have a global dialog box variable, so that different methods can acccess it. So something like the following can work:

public class MyActivity extends ...
{
    /** Global dialog reference */
    private AlertDialog okDialog;

    /** Show the dialog box */
    public void showDialog(View view) 
    {
        // prepare the alert box
        AlertDialog.Builder alertBox = new AlertDialog.Builder(...);

        ...

        // set a negative/no button and create a listener
        alertBox.setNegativeButton("No", new DialogInterface.OnClickListener() {
            // do something when the button is clicked
            public void onClick(DialogInterface arg0, int arg1) {
                //no reply or do nothing;
            }
        });

        // set a positive/yes button and create a listener
        alertBox.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
            // do something when the button is clicked
            public void onClick(DialogInterface arg0, int arg1) {
                callbackMethod(params);
            }
        });

        //show the dialog
        okDialog = alertBox.create();
        okDialog.show();
    }


    /** The yes reply method */
    private void callbackMethod(params)
    {
        //first statement closes the dialog box
        okDialog.dismiss();

        //the other statements run in a new thread
        new Thread() {
            public void run() {
                try {
                    //statements or even a runOnUiThread
                }
                catch (Exception ex) {
                    ...
                }
            }
        }.start();
    }
}
0
votes

Use a BroadcastReceiver that calls the next method required in the activity.

Dead-end the activity code at dialogFragment.show(fragmentTransaction, TAG); and continue it in onReceive()--i'm not 100% positive but I would lay money that startActivityForResult(); is based on exactly this concept.

Until that method is invoked from the receiver, the code will stand in wait for user interaction without ANR.

DialogFragment's onCreateView Method

private static final String ACTION_CONTINUE = "com.package.name.action_continue";

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
    View v = inflater.inflate(R.layout.fragment_dialog, container, false);
        Button ok_button = v.findViewById(R.id.dialog_ok_button);
        ok_button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent();
                i.setAction(ACTION_CONTINUE);
                getActivity().getApplicationContext().sendBroadcast(i);
                dismiss();
            }
        });


    return v;
}

This method depends on building a DialogFrament extension class and calling an instance of that class through the activity.

However...

Simple, clear, easy and truly modal.