Android: How to get a modal dialog or similar modal behavior?
Asked Answered
M

13

58

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.

Manuelmanuela answered 25/5, 2011 at 6:47 Comment(5)
This post may help you. #2029197Tardif
I can't really believe Android sucks on this very simple things.Alienor
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.Deathwatch
Use a broadcast receiver that calls the next method in the chain... dead end the code until the method is inoked.Crisscross
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 ;-)Bullyrag
C
76

I got a modal Dialog while using:

setCancelable(false);

on the DialogFragment (not on the DialogBuilder).

Consistency answered 25/2, 2013 at 7:29 Comment(7)
Works like a charm. Example: final AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(false);Surprisal
This doesn't work. What it is meant with modal Dialog, is the fact, that the UI Thread stops working till the dialog is dismissed, which is not true with this codeGaytan
As mentioned this should be done on dialog fragment if your dialog is a separate fragment public class MyCustomDialog extends DialogFragment{ .... public Dialog onCreateDialog(Bundle savedInstanceState) { setCancelable(false); ...}Anticyclone
it is also valid extending the android base Dialog classMartamartaban
this does not halt execution of code, it prevents the dialog from being cancelled on back pressed.... the code can be truly halted by dead ending at dialogFragment.show(fragmentTransaction, TAG); and using an onClickListener to send a broadcast intent calling the method required after the dialog has been dismissed.Crisscross
The best answer! Works great!Homeland
This is the best answer.Lustral
I
19

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).

Intermezzo answered 25/5, 2011 at 8:58 Comment(6)
thanks. I completely agreed everything you presented above. Unfortunately, it's a MUST requirement for me, and the real situation is much more complicated, a long story. Modal behavior is basically conflict against design of Android, we all know, but... Anyway, I'm trying looking for somewhat elegant way to solve it :( is Solution 3 an option?Manuelmanuela
Is it possible to have rich controls (like buttons, textviews and editviews) in such a dialog? which can wait until user provides something as input, and focuses back to activity which was running.Akel
I take issue with the way this was worded "It is not possible the way you planned. First, you are not allowed to block the UI thread." -- one need not block the UI thread to display a Modal dialog -- Windows will kill your apps if they block the UI thread as well, but it doesn't stop them from having modal dialogs -- those apps continue to handle messages and be responsive while forcing the user to deal with the modal dialog before returning to the parent window (and no, the parent windows aren't blocking either -- they handle all of their events normally).Clubhaul
@BrainSlugs83: "Windows will kill your apps if they block the UI thread as well.." - No it wont.Blandishment
@BrainSlugs83: I think the problem was not with Stephan's wording but with the OP's. Stephan didn't say that modal dialogs require blocking; that's the OP.Deathwatch
The simplest way to answer: "Sorry dude it's impossible... It's violated something, or someone policy...". If you dont know the answer please don't answer at all. ThanksDefrost
D
8

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;
    }
Dowsabel answered 14/7, 2012 at 1:45 Comment(1)
Thanks, this did the trick for me. As for a use case: I need a modal dialog that blocks input because it's used for an assert triggered by C++. The dialog must block the UI thread because the thread might trigger more asserts.Engrave
M
6

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.

Manuelmanuela answered 1/6, 2011 at 8:19 Comment(7)
DO NOT DO THIS. This code is using private APIs. It is OBVIOUSLY using private APIs. Anybody doing this can expect their app to break.Eyla
This code is embarrassing. Nobody in their right mind should be using these sorts of techniques.Raouf
this code is very funny! 'fortunately we have reflection in java'! Is a joke yes? please say yesPanoptic
this just shows you that android framework is poorly designed in the first place. How can you not have a simple mechanism for modal dialogs???!Darnall
The main trouble with using another Activity as a dialog is that you can't distinguish between losing focus for the whole application and losing focus because another activity in your application is on top on onPause(). In that case, using a modal-like dialog may make sense. Especially if the client requirements have changed but you're not "enterprise" and don't have the budget for 3 months of refactoring...Lusatia
See my solution for a better way of doing this without using reflection.Prewar
Even though this is bad programming, I upvoted just for the proof of concept.Excommunicatory
K
6

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.

Krona answered 6/2, 2013 at 16:22 Comment(1)
this requires API level 11 as a minimumPiraeus
P
3

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.

Posset answered 31/5, 2015 at 21:18 Comment(0)
P
2

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;
     }
}
Prewar answered 12/9, 2013 at 16:20 Comment(2)
Can you explain what you mean by nested event loop?Griddlecake
It means you have two event loop calls on your callstack. Android provides it's own event loop (the main Looper, which is created automatically). If you want to have a modal dialog or similar, you need to create your own event loop, so you will end up with two nested event loops on the callstack.Prewar
A
1

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.

Affect answered 25/5, 2011 at 6:55 Comment(1)
I've updated first thread with some source. I didn't follow you here, is there something to do with onResume? thx.Manuelmanuela
E
1

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!

Ephemerality answered 24/8, 2012 at 22:7 Comment(1)
This worked for me. I was able to simulate modality by calling dialog.setOnDissmissListener.Clubhaul
P
1

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();
    }
}
Procrustes answered 27/1, 2016 at 12:32 Comment(0)
C
0

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.

Crisscross answered 27/7, 2018 at 2:41 Comment(0)
L
0

The following worked for me (code is in Kotlin but you can use IDE to convert to Java if needed). The key change that made the alert dialog was calling its setCancelOnTouchOutside method with an argument of false.

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    val builder = getContext()?.let { AlertDialog.Builder(it) }
    var ret: Dialog = super.onCreateDialog(savedInstanceState)
    // Get the layout inflater
    val inflater: LayoutInflater? = getActivity()?.getLayoutInflater()
    m_view = inflater?.inflate(R.layout.select_clinic_dialog, null)

    if (builder != null) {
        builder.setView(m_view) // Add action buttons
            .setPositiveButton(R.string.label_continue,
                DialogInterface.OnClickListener { dialog, id ->
                    val i = Intent(getContext(), PatientSearchActivity::class.java)
                    startActivity(i)
                    getActivity()?.finish()
                    dialog.dismiss()
                })
            .setNegativeButton(R.string.register_no,
                DialogInterface.OnClickListener { dialog, id -> val i = Intent(getContext(), LoginActivity::class.java)
                    startActivity(i)
                    getActivity()?.finish()
                    dialog.dismiss() })
        ret = builder.create()
        ret.setTitle(R.string.title_no_clinic_today)
        ret.setOnShowListener(OnShowListener {
            val positive: Button = ret.getButton(AlertDialog.BUTTON_NEGATIVE)
            positive.isFocusable = true
            positive.isFocusableInTouchMode = true
            positive.requestFocus()
        })
        ret.setCanceledOnTouchOutside(false)
    }
    return ret
}
Lustral answered 17/9, 2022 at 19:0 Comment(0)
B
0

Maybe what users looking at this question need is not a "modal" Dialog, but a new Activity with its own layout, where you can show the intended message/links/buttons/whatever and wait for user interaction.

Burchette answered 13/3, 2023 at 15:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.