4

In a Xamarin Android application, I have an Activity that calls an async method (a network operation) in a RetainInstance fragment so that the operation doesn't stop on configuration changes. After the operation is complete, the UI is changed, a progress dialog is dismissed, a new fragment is inserted into the layout, etc.

It works correctly, even if the activity is destroyed and re-created on configuration changes. However, if the activity is paused when the async method completes, UI operations throw IllegalStateException: Can not perform this action after onSaveInstanceState exception. This happens if the user turns off the screen or switches to another application while the network operation is running.

Is there a way to make the async method continue normally if the activity is not paused. But if the activity is paused, wait until the activity is resumed before continuing?

Alternatively, what is the proper way to handle async operations that complete while the activity is paused?

The code:

using System;
using System.Threading.Tasks;

using Android.App;
using Android.OS;
using Android.Widget;

namespace AsyncDemo {
    [Activity(Label = "AsyncDemo", MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity {

        const string fragmentTag = "RetainedFragmentTag";
        const string customFragmentTag = "CustomFragmentTag";
        const string dialogTag = "DialogFragmentTag";

        protected override void OnCreate(Bundle savedInstanceState) {
            base.OnCreate(savedInstanceState);

            SetContentView(Resource.Layout.Main);

            var retainedFragment = FragmentManager.FindFragmentByTag(fragmentTag) as RetainedFragment;

            if (retainedFragment == null) {
                retainedFragment = new RetainedFragment();
                FragmentManager.BeginTransaction()
                    .Add(retainedFragment, fragmentTag)
                    .Commit();
            }

            Button button = FindViewById<Button>(Resource.Id.myButton);
            button.Click += delegate {
                button.Text = "Please wait...";

                var dialogFragment = new DialogFragment(); // Substitute for a progress dialog fragment
                FragmentManager.BeginTransaction()
                    .Add(dialogFragment, dialogTag)
                    .Commit();

                Console.WriteLine("Starting task");

                retainedFragment.doIt();
            };
        }

        void taskFinished() {
            Console.WriteLine("Task finished, updating the UI...");

            var button = FindViewById<Button>(Resource.Id.myButton);
            button.Text = "Task finished";

            var dialogFragment = FragmentManager.FindFragmentByTag(dialogTag) as DialogFragment;
            dialogFragment.Dismiss(); // This throws IllegalStateException

            var customFragment = new CustomFragment();
            FragmentManager.BeginTransaction()
                .Replace(Resource.Id.container, customFragment, customFragmentTag)
                .Commit(); // This also throws IllegalStateException
        }

        class RetainedFragment : Fragment {
            public override void OnCreate(Bundle savedInstanceState) {
                base.OnCreate(savedInstanceState);
                RetainInstance = true;
            }

            public void doIt() {
                doItAsync();    
            }

            public async Task doItAsync() {
                try {
                    await Task.Delay(3000); // substitute for the real operation
                    (Activity as MainActivity).taskFinished();
                } catch (Exception e) {
                    Console.WriteLine(e);
                }
            }

        }
    }
}

The log:

Starting task
Task finished, updating the UI...
Java.Lang.IllegalStateException: Exception of type 'Java.Lang.IllegalStateException' was thrown.
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000b] in /Users/builder/data/lanes/1978/f98871a9/source/mono/mcs/class/corlib/System.Runtime.ExceptionServices/ExceptionDispatchInfo.cs:61 
  at Android.Runtime.JNIEnv.CallVoidMethod (IntPtr jobject, IntPtr jmethod) [0x00062] in /Users/builder/data/lanes/1978/f98871a9/source/monodroid/src/Mono.Android/src/Runtime/JNIEnv.g.cs:554 
  at Android.App.DialogFragment.Dismiss () [0x00043] in /Users/builder/data/lanes/1978/f98871a9/source/monodroid/src/Mono.Android/platforms/android-22/src/generated/Android.App.DialogFragment.cs:284 
  at AsyncDemo.MainActivity.taskFinished () [0x00039] in /Users/csdvirg/workspaces/xamarin/AsyncDemo/AsyncDemo/MainActivity.cs:52 
  at AsyncDemo.MainActivity+RetainedFragment+<doItAsync>c__async0.MoveNext () [0x00094] in /Users/csdvirg/workspaces/xamarin/AsyncDemo/AsyncDemo/MainActivity.cs:73 
  --- End of managed exception stack trace ---
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
    at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1323)
    at android.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1341)
    at android.app.BackStackRecord.commitInternal(BackStackRecord.java:597)
    at android.app.BackStackRecord.commit(BackStackRecord.java:575)
    at android.app.DialogFragment.dismissInternal(DialogFragment.java:292)
    at android.app.DialogFragment.dismiss(DialogFragment.java:258)
    at mono.java.lang.RunnableImplementor.n_run(Native Method)
    at mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:29)
    at android.os.Handler.handleCallback(Handler.java:733)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:146)
    at android.app.ActivityThread.main(ActivityThread.java:5756)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:515)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1291)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1107)
    at dalvik.system.NativeStart.main(Native Method)
Anas Alweish
  • 2,818
  • 4
  • 30
  • 44
imgx64
  • 4,062
  • 5
  • 28
  • 44
  • Have you tried 'RunOnUiThread(() => { });'? – choper Sep 07 '15 at 08:31
  • @choper It's already running on the UI Thread as a side effect of `await`. But just to make sure, I used RunOnUiThread and it throws the same exception. – imgx64 Sep 07 '15 at 08:34
  • 1
    http://blogs.msdn.com/b/pfxteam/archive/2013/01/13/cooperatively-pausing-async-methods.aspx then go through this article, in it described how to pause async methods – choper Sep 07 '15 at 08:43
  • 1
    you can try to look on PauseToken http://stackoverflow.com/questions/19613444/a-pattern-to-pause-resume-an-async-task/21712588#21712588 – xakpc Sep 07 '15 at 08:45

2 Answers2

3

Based on @choper and @xakz comments, I used PauseTokenSource and it works perfectly now.

I modified RetainedFragment:

class RetainedFragment : Fragment {
    readonly PauseTokenSource pts = new PauseTokenSource();

    public override void OnCreate(Bundle savedInstanceState) {
        base.OnCreate(savedInstanceState);
        RetainInstance = true;
    }

    public override void OnPause() {
        base.OnPause();
        pts.IsPaused = true;
    }

    public override void OnResume() {
        base.OnResume();
        pts.IsPaused = false;
    }

    public void doIt() {
        doItAsync();    
    }

    public async Task doItAsync() {
        try {
            await Task.Delay(3000); // substitute for the real operation
            await pts.Token.WaitWhilePausedAsync();
            (Activity as MainActivity).taskFinished();
        } catch (Exception e) {
            Console.WriteLine(e);
        }
    }
}

PauseTokenSource implementation (pieced together from the blog post):

public class PauseTokenSource {

    internal static readonly Task s_completedTask = Task.FromResult(true);

    volatile TaskCompletionSource<bool> m_paused;

    #pragma warning disable 420
    public bool IsPaused { 
        get { return m_paused != null; } 
        set { 
            if (value) { 
                Interlocked.CompareExchange(
                    ref m_paused, new TaskCompletionSource<bool>(), null); 
            } else { 
                while (true) { 
                    var tcs = m_paused; 
                    if (tcs == null)
                        return; 
                    if (Interlocked.CompareExchange(ref m_paused, null, tcs) == tcs) { 
                        tcs.SetResult(true); 
                        break; 
                    } 
                } 
            } 
        } 
    }
    #pragma warning restore 420

    public PauseToken Token { get { return new PauseToken(this); } }

    internal Task WaitWhilePausedAsync() { 
        var cur = m_paused; 
        return cur != null ? cur.Task : s_completedTask; 
    }
}

public struct PauseToken {
    readonly PauseTokenSource m_source;

    internal PauseToken(PauseTokenSource source) {
        m_source = source;
    }

    public bool IsPaused { get { return m_source != null && m_source.IsPaused; } }

    public Task WaitWhilePausedAsync() { 
        return IsPaused ? 
            m_source.WaitWhilePausedAsync() : 
            PauseTokenSource.s_completedTask; 
    }
}
imgx64
  • 4,062
  • 5
  • 28
  • 44
0

Using Async as Sync is a wrong way. Use an event(an activity) and thread(a network operation) if you need hard control.