Android: How to get a modal dialog or similar modal behavior?
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.
I got a modal Dialog while using:
setCancelable(false);
on the DialogFragment (not on the DialogBuilder).
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).
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;
}
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.
This works for me: create an Activity as your dialog. Then,
-
Add this to your manifest for the activity:
android:theme="@android:style/Theme.Dialog"
-
Add this to onCreate of your activity
setFinishOnTouchOutside (false);
-
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.