Best practise for Android dialog - how to avoid app crash for dismiss dialog

Viewed 796

I use an AlertDialog in my app, when I need to do some network operation, The dialog will show up some text to users and block users to interact with the app until the network operation is finished. When the network operation is finished, I will call dismiss to dismiss the dialog and then users can interact with app again. My code is something like the following:

//pseudo-code runs in UI/main thread
private void fun() {
    //business code here...
    
    mainExecutor.execute(new Runnable() {
        @Override
        public void run() {
            int timeLeftSec = 10;
            while (true)
            {
                final int timeLeft = timeLeftSec--;
                final String msg = "Time left ";
                mainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        ShowDialog(msg + " " + timeLeft + " s");
                    }
                });
                
                if (CheckIfNetworkOpDone() || timeLeft < 0) {
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            DismissProcessDialog();
                        }
                    });
                    break;
                } else {
                    try {
                        Thread.sleep(1000);
                    }catch (Exception e)
                    {
                        //
                    }
                }
            }
        }
    }
}

private void ShowDialog(String info)
{
    if (mainDialog == null || mainDialogTextView == null)
    {
        View dialogView = LayoutInflater.from(this).inflate(R.layout.processbar_dialog, null);

        if (mainDialog == null)
        {
            mainDialog = new AlertDialog.Builder(this).create();
            mainDialog.setView(dialogView);
            mainDialog.setCancelable(false);
            mainDialog.setCanceledOnTouchOutside(false);
        }
        if (mainDialogTextView == null)
        {
            mainDialogTextView = dialogView.findViewById(R.id.dialogTextView);
        }
    }

    mainDialogTextView.setText(info);
    mainDialog.show();
}

private void DismissProcessDialog()
{
    if (mainDialog != null)
    {
        try {
            Activity activity = (Activity) mainActivityContext;

            if (activity == null || activity.isDestroyed() || activity.isFinishing())
            {
                return;
            }

            Context context = ((ContextWrapper)(mainDialog.getContext())).getBaseContext();
            if (!(context instanceof Activity ))
            {
                return;
            }
            activity = (Activity) context;
            if (activity == null || activity.isDestroyed() || activity.isFinishing())
            {
                return;
            }

            if (mainDialog != null && mainDialog.isShowing())
            {
                mainDialog.dismiss();
            }
        }
        catch (Exception e)
        {
            //
        }
    }
}

    @Override
    protected void onDestroy() {
        if (mainDialog != null && mainDialog.isShowing())
        {
            mainDialog.dismiss();
        }
        mainDialog = null;

        super.onDestroy();
    }

But google play backend shows there is a log of crashes for dismiss. The exception is:

java.lang.IllegalArgumentException: 
  at android.view.WindowManagerGlobal.findViewLocked (WindowManagerGlobal.java:517)
  at android.view.WindowManagerGlobal.removeView (WindowManagerGlobal.java:426)
  at android.view.WindowManagerImpl.removeViewImmediate (WindowManagerImpl.java:126)
  at android.app.Dialog.dismissDialog (Dialog.java:389)
  at android.app.-$$Lambda$oslF4K8Uk6v-6nTRoaEpCmfAptE.run (Unknown Source:2)
  at android.os.Handler.handleCallback (Handler.java:883)
  at android.os.Handler.dispatchMessage (Handler.java:100)
  at android.os.Looper.loop (Looper.java:214)
  at android.app.ActivityThread.main (ActivityThread.java:7356)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:930)

ProgressDialog is depreciated and I use AlertDialog instead. The reason why I don't use ProgressBar is, I want to blcok user until network operation is finished. I find user can still press buttons when ProgressBar is showing.

Is there any best practice on how to fix this kind of crashes or how to deal with this scenario to show dialog correctly?

Any suggestions will be appreciated. Thanks!

4 Answers

Have you considered managing user interaction with the application?

Sometimes I use these two methods defined in my utils class.

public class Utils {
    public static void disableUserInteraction(Activity activity) {
        activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
    }

    public static void enableUserInteraction(Activity activity) {
        activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
    }
}

and I call them like this:

 Utils.disableUserInteraction(activity);

The above methods set the appropriate flag so that user interaction is ignored when needed.

To me it looks like there's two issues, you're calling dismiss dialog from a background thread. You should always make UI calls from the UI thread (runOnUi{...} will help with this). Second of all you are running a loop with while (true). This is extremely bad practice, you should be passing a condition here that can be ended inside of your loop. This will run indefinitely, so currently the first pass makes the network call. Say it finishes instantly, the 2nd pass tried to dismiss the dialog. Then the 3rd pass meets the same conditions now you have findViewLocked in the crash log because its trying to access the same object from two background threads.

It's more reasonable to use progressbar instead of alertdialog here. If you want to prevent user interacting with app when it is loading, you can make all components in the screen except of progressbar invisible or you can keep them visible but change their focusability or clickability.

Your exception can occur when a user leaves (or rotates) your app during your network operation and your Activity (or Fragment) gets destroyed (or recreated). Then there is no dialog anymore to dismiss and the exception is thrown, see this answer.

While the above answer solves your issue, it makes the mistake of holding a reference to the Activity in the background task. What I did in the past was let my Activity implement a callback-interface and pass a WeakReference of this callback to the background task instead. Then the background task had to check whether the callback (Activity) was still there.

The above solution still has the problem that you have to re-start the task for every new Activity created on rotation (or any other config change). The modern approach would be to let the Activity (or Fragment) observe a LiveData in your ViewModel instead. See the official documentation.

Also it's recommended to use a DialogFragment, because that one is restored on re-creation. A normal AlertDialog disappears when your rotate your screen (or switch to your app after it has been destroyed).

Related