3

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!

MMG
  • 3,226
  • 5
  • 16
  • 43
Thomas
  • 130
  • 15
  • https://stackoverflow.com/a/44188192/3192693 check this out. You need to check activity state and dialog state for avoiding this kind of errors. – Monster Brain Jul 14 '20 at 05:42
  • @Monster Brain thanks for your advice, but you may not read my code since I have already checked the activity and dialog state in the code. But it still crashes. – Thomas Jul 15 '20 at 11:30
  • Have you tried dismissAllowingStateLoss() – Zee Jul 15 '20 at 13:22
  • Is the error occuring in the DismissProcessDialog or onDestroy ? – Monster Brain Jul 15 '20 at 14:22
  • I think it should occur in DismissProcessDialog but I cannot get it from the crash log which doesn't show any code by me. I just add onDestroy recently, before that there is some crash log. So I guess it shoud come from DismissProcessDialog. – Thomas Jul 15 '20 at 14:36
  • @Zee This is not DialogFragment, but I'd like to have a try. – Thomas Jul 15 '20 at 14:41
  • @Thomas oops! Sorry my bad. – Zee Jul 15 '20 at 14:48
  • Notice the the stacktrace does not show the `dismiss` call which means (according to the Dialog source) it was invoked on a different looper - this then means the `onDestroy` can be processed before the internally scheduled looper task gets invoked which means the dialog may be dismissed twice. Not sure why you dismiss it in the `onDestroy` so can't advise but I'd set the mainDialog to null prior invoking dismiss (make a copy of the variable) in the `DismissProcessDialog` –  Jul 19 '20 at 02:24
  • Just use Rx or Coroutines for multithreading, its really easy to to implement "loader" while doing background work with them and working with threads in general. About implementing loader: if you want want your layout to be not clickable while doing work 1) Add FrameLayout (could be any other ) that covers the whole screen and set android:clickable="true" and android:visibility="gone". you can add a background color to it with low alpha 2) Add ProgressBar centered in parent layout android:visibility="gone" – Rinat Diushenov Jul 19 '20 at 16:24
  • 3) Just before starting work on seperate thread set visibilities of both FrameLayout and ProgressBar to visible and after the work is done set them to gone again. Not sure if its good practice,but its really easy this way. Further, you could extract FrameLayout and ProgressBar into seperate layout file to reuse them via or create custom viewGroup. – Rinat Diushenov Jul 19 '20 at 16:24

4 Answers4

1

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.

Kamil Z
  • 181
  • 12
0

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.

Spectre
  • 56
  • 3
  • 1) handler.post will make sure it runs on UI thread, otherwise, it will crash at once... 2) there is a condition to break the loop which you might not see in the if condition check. – Thomas Jul 19 '20 at 02:00
0

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.

MMG
  • 3,226
  • 5
  • 16
  • 43
  • I agree to disable clickable for other components, but I didn't find a good way to do this since lots of components need to be handled... – Thomas Jul 20 '20 at 11:57
  • In your XML, put two layouts, one of them has just a progressbar and another one is your main layout. When data is loading layout with progressbar is shown and main layout clickability is set to false. @Thomas – MMG Jul 20 '20 at 12:36
0

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

Bram Stoker
  • 1,202
  • 11
  • 14