2
votes

Problem

Hi, I'm creating an Android and iOS module for Titanium which has a sendLog method which sends some arbitrary JSON data to a server and returns an URL if it matches some predefined filters. The URL should be opened in a modal Dialog with a webview.

I've written native iOS and Android libraries and wrapped them as Titanium modules. On iOS everything works fine but on Android I can't open the Dialog (see error stack trace below). Right now there's one log message that always triggers the same webpage for testing purposes. On Android it just fails silently.

Test case

var mupets = require("be.iminds.mupets");
mupets.initialize("wappr", "http://tocker.iminds.be:3000/log/report.json", 1, 100, 3);
var esmLog = { 
    bar: "foo"
};
mupets.sendLog("es-test-01",JSON.stringify(esmLog));

After this piece of code (after about 10 seconds max), the module should show a native dialog with the following webpage inside: http://tocker.iminds.be:3000/es/sheets/test-01/index.html

Instead, this is the error I get consistently:

Logs

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1411)
at android.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1429)
at android.app.BackStackRecord.commitInternal(BackStackRecord.java:687)
at android.app.BackStackRecord.commit(BackStackRecord.java:663)
at android.app.DialogFragment.show(DialogFragment.java:256)
at be.iminds.mupets_client_android.logging.plugins.OutHttp.getEsm(OutHttp.java:122)
at be.iminds.mupets_client_android.logging.plugins.OutHttp$1.success(OutHttp.java:78)
at be.iminds.mupets_client_android.HttpClient$1$1.onResponse(HttpClient.java:76)
at okhttp3.RealCall$AsyncCall.execute(RealCall.java:133)
at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)

Here is the Android code that's causing the error:

Activity activity = (Activity) context;
                EsmDialogFragment esmDialogFragment = EsmDialogFragment.newInstance(new EsmDialogListener() {
                    @Override
                    public void submit(String type, JsonObject result) {
                        Mupets.sendLog(type, result);
                        esmShown = false;
                    }

                    @Override
                    public void onCancel(JsonObject cancelled) {
                        super.onCancel(cancelled);
                        Mupets.sendLog("ESM_cancelled", cancelled);
                        esmShown = false;
                    }
                }, url, true);
                FragmentTransaction transaction = activity.getFragmentManager().beginTransaction();
                Fragment prev = activity.getFragmentManager().findFragmentByTag(EsmDialogFragment.ESM_DIALOG_FRAGMENT);
                if (prev != null) {
                    transaction.remove(prev);
                }
                transaction.addToBackStack(null);
                Log.v(TAG, "Pre-show fragment");
                esmDialogFragment.show(transaction, EsmDialogFragment.ESM_DIALOG_FRAGMENT);
                Log.v(TAG, "Post-show fragment");

Does Titanium either not allow the use of Fragments/or require you to call Dialog.show() at a specific point? The error talks about "...after onSaveInstanceState" but I don't see how I would call it before onSaveInstanceState if I'm not creating an activity and why the code works when I use it in a native Android app.

This is an Titanium example project with the module that should show the dialog after it opens: https://www.dropbox.com/s/0v77xd5gllv6kb3/testModule.zip?dl=1

5

5 Answers

5
votes

This is not a trivial problem, so there's no quick and easy fix that you can just copy/paste from an answer. The bottom line is that you will have to refactor some of your code.

You are trying to show a DialogFragment in response to an asynchronous operation - if that operation completes after onSaveInstanceState, the callback will try to show the dialog and induce an IllegalStateException.

The way to protect yourself from this issue is to not do UI stuff directly from callbacks. Instead, you need to hold off until you have a started or resumed Activity or Fragment, so that you can show the dialog safely.

An easy way to do this is to use sticky events, i.e. post a sticky event from your callback and subscribe for that type of sticky event in your UI component's onResume method.

If you don't want to use an event bus library, you can instead call your asynchronous method from a non-UI component that updates its internal state inside the callback, and then make the UI component check that state in onResume. If you use this approach, you need to be careful with managing your global state.

2
votes

I know this question already has a right answer but I want share my solution, to show DialogFragment you should override show() method of it and call commitAllowingStateLoss() on Transaction object. Here is example in Kotlin:

override fun show(manager: FragmentManager?, tag: String?) {
        try {
            val ft = manager?.beginTransaction()
            ft?.add(this, tag)
            ft?.commitAllowingStateLoss()
        } catch (ignored: IllegalStateException) {

        }

    }
2
votes

With reference to @Dennis comment, i have override the method show and it works.

@Override
public void show(FragmentManager manager, String tag) {
    FragmentTransaction fragmentTransaction = manager.beginTransaction();
    fragmentTransaction.add(this, TAG);
    fragmentTransaction.commitAllowingStateLoss();
}

Hope this helps

0
votes

Since dialogfragment does not have option to commitAllowingStateLoss, the simplest solution I used was to set a flag when onSaveInstance is called and reset it on onCreate and onRestoreInstance. Then before making a fragment transaction, check the flag to be sure its false. btw, this usually occurs in a asynchronous callback. The activity is already gone beyond onSaveInstance by the time background work has completed and callback has triggered.

0
votes

I have faced same issue and solved this issue by overriding show() in DialogFragment extended class.

public class MyDialogFragment extends DialogFragment {

    @Override
    public void show(FragmentManager manager, String tag) {
        try {
            FragmentTransaction ft = manager.beginTransaction();
            ft.add(this, tag);
            ft.commitAllowingStateLoss();
        } catch (IllegalStateException e) {
            Log.d("ABSDIALOGFRAG", "Exception", e);
        }
    }
}