Xamarin iOS seeing unexpected behavior with BackgroundTask

2020-02-15 c# xamarin.forms xamarin.ios background-process ios13

In trying to implement some autosave behavior into my Xamarin Cross Platform app, I've run into what feel like some incorrect behavior, and I'm trying to understand what's going on. This is on Xamarin.Forms v4.4 and testing while targeting iOS 13 (both with the simulator and a real device).

In my App.xaml.cs OnSleep method I wanted to add some logic to autosave user data using an API call. For arguments sake, let's say that in worse case scenarios this API save could take a full minute. From my reading, it seems to be pretty consistently stated that OnSleep is not guaranteed to run for and should not be used for anything that will be more than 5 to 10 seconds. This lead me to do some research on Background tasks, and implement a simple test case based on some blog posts and other answers I've seen on the forums.

My goal with all this, is to be able to autosave from the background, while not having my app terminated by the OS.

App.xaml.cs:

    protected override async void OnSleep()
        {
            MessagingCenter.Send(new RunBackgroundSaveMessage(), "RunBackgroundSaveMessage");
        }

And then I subscribe to and handle the message in my iOS specific project AppDelegate.cs:

    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            global::Xamarin.Forms.Forms.Init();
            LoadApplication(new App());

            MessagingCenter.Subscribe<RunBackgroundSaveMessage>(this, "RunBackgroundSaveMessage", InitiateBackgroundAutoSave);

            return base.FinishedLaunching(app, options);
        }

        private nint curTaskID = UIApplication.BackgroundTaskInvalid;
        private async void InitiateBackgroundAutoSave(RunBackgroundSaveMessage message)
        {
            Console.WriteLine("InitiateBackgrounding");
            DateTime StartDT = DateTime.Now;

            curTaskID = UIApplication.SharedApplication.BeginBackgroundTask(() =>
            {
                Console.WriteLine("BackgroundTimeExpiredCallback");
                Console.WriteLine(DateTime.Now.Subtract(StartDT).TotalSeconds);
                //End Task
                UIApplication.SharedApplication.EndBackgroundTask(curTaskID);
                curTaskID = UIApplication.BackgroundTaskInvalid;
            });
            Console.WriteLine("curTaskID: " + curTaskID);

            //DoWork
            while (DateTime.Now.Subtract(StartDT).TotalMinutes < 15)
            {
                Console.WriteLine("FROM IOS: " + DateTime.Now.Subtract(StartDT).TotalMinutes);
                Console.WriteLine("FROM IOS: " + "Running API");
                await Helpers.WebAPI.GetAPIInfoAsync();
                await Task.Delay(1000 * 15);
            }

            Console.WriteLine("EndBackgrounding");
            UIApplication.SharedApplication.EndBackgroundTask(curTaskID);
            curTaskID = UIApplication.BackgroundTaskInvalid;
        }

Here's where we get to what I don't understand. While none of the posts I've read indicate an issue with the length of time before the BackgroundTask expires, and all the documentation points to the fact that a task should have about 600 seconds of runtime once in the background, my backgroundTimeExpired callback function is always called about 26 seconds in, with 4 seconds left, which means the task really only has 30 seconds to run, not 600.

Now, the while loop I have there is not the final implementation of work I'd like to perform, but a placeholder to substitute in for something that takes a long time to run, in this case it calls a simple GET endpoint of an API every 15 seconds for 15 minutes. What I don't understand, is that that loop runs perfectly fine, with or without beginning background task. Everything I've read indicates that my code will cease to run once the app is in the background if I haven't sandwiched it with BeginBackgroundTask and EndBackgroundTask.

However, everything I've tested shows that even after the task has been ended, the loop runs for the full 15 minutes (as opposed to being suspended or causing the app to get terminated), additionally, if I don't bother with BeginBackgroundTask at all it seems to run just fine as well. I even took out the messaging, and just stuck the while loop into the OnSleep method, and even there, despite what I expected it ran for the full 15 minutes with the OS terminating my app.

And I have absolutely no idea what to think of this, as I find myself with working functionality, but not for the reasons I thought. Instead of working because I am successfully maintaining a BackgroundTask, it works because the OS just doesn't seem to care about suspending my app like I thought it would. Even though everything I've read seems to scream not to do so, it even seems like I could just use the OnSleep method by itself, and take basically as long as I need to.

Any explanation to help me understand what's going on here would be greatly appreciated, as I would love to be implementing code understanding why it works, and not just that it seems to work.

Thank you!

Answers

Related