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

AndroidAndroid ActivitySynchronizationModal Dialog

Android Problem Overview


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
https://stackoverflow.com/questions/2028697/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.

Android Solutions


Solution 1 - Android

I got a modal Dialog while using:

setCancelable(false);

on the DialogFragment (not on the DialogBuilder).

Solution 2 - Android

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

Solution 3 - Android

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;
	}

Solution 4 - Android

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.

Solution 5 - Android

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.

Solution 6 - Android

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.

Solution 7 - Android

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 https://stackoverflow.com/questions/4994263/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;
     }
}

Solution 8 - Android

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.

Solution 9 - Android

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!

Solution 10 - Android

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();
	}
}

Solution 11 - Android

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionfifthView Question on Stackoverflow
Solution 1 - AndroidmeisesView Answer on Stackoverflow
Solution 2 - AndroidStephanView Answer on Stackoverflow
Solution 3 - Androidart926View Answer on Stackoverflow
Solution 4 - AndroidfifthView Answer on Stackoverflow
Solution 5 - AndroidPeri HartmanView Answer on Stackoverflow
Solution 6 - Androiduser502187View Answer on Stackoverflow
Solution 7 - AndroidCpnCrunchView Answer on Stackoverflow
Solution 8 - AndroidxandyView Answer on Stackoverflow
Solution 9 - AndroidRambo VIView Answer on Stackoverflow
Solution 10 - AndroidDCSView Answer on Stackoverflow
Solution 11 - Androidme_View Answer on Stackoverflow