NSUrlSession upload task to AzureFunction with BGTaskScheduler does not get executed when iOS charger cable is unplugged on iOS 14.4

Matas Vaitkevicius 1 Reputation point
2021-03-18T12:08:40.03+00:00

I am working on a Xamarin Forms app that is supposed to upload photos to Azure Function in the background. It is being custom-made for a client, so they will set their phones to whatever permissions need to be set.

I am using BGTaskScheduler and queuing both types of tasks (BGProcessingTaskRequest and BGAppRefreshTaskRequest) so that if the cable is plugged in, it would fire off BGProcessingTaskRequest and if not it would wait for BGAppRefreshTaskRequest to get its processing time.

I have added RefreshTaskId and UploadTaskId to Info.plist

AppDelegate.cs in iOS project looks following:

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

        BGTaskScheduler.Shared.Register(UploadTaskId, null, task => HandleUpload(task as BGProcessingTask));
        BGTaskScheduler.Shared.Register(RefreshTaskId, null, task => HandleAppRefresh(task as BGAppRefreshTask));

        return base.FinishedLaunching(app, options);
    }

    public override void HandleEventsForBackgroundUrl(UIApplication application, string sessionIdentifier, Action completionHandler)
    {
        Console.WriteLine("HandleEventsForBackgroundUrl");
        BackgroundSessionCompletionHandler = completionHandler;
    }
    public override void OnActivated(UIApplication application)
    {
        Console.WriteLine("OnActivated");
    }

    public override void OnResignActivation(UIApplication application)
    {
        Console.WriteLine("OnResignActivation");
    }

    private void HandleAppRefresh(BGAppRefreshTask task)
    {
        HandleUpload(task);
    }

    public override void DidEnterBackground(UIApplication application)
    {
        ScheduleUpload();
    }

    private void HandleUpload(BGTask task)
    {
        var uploadService = new UploadService();
        uploadService.EnqueueUpload();
        task.SetTaskCompleted(true);
    }

    private void ScheduleUpload()
    {
        var upload = new BGProcessingTaskRequest(UploadTaskId)
        {
            RequiresNetworkConnectivity = true,
            RequiresExternalPower = false
        };

        BGTaskScheduler.Shared.Submit(upload, out NSError error);

        var refresh = new BGAppRefreshTaskRequest(RefreshTaskId);

        BGTaskScheduler.Shared.Submit(refresh, out NSError refreshError);

        if (error != null)
            Console.WriteLine($"Could not schedule BGProcessingTask: {error}");
        if (refreshError != null)
            Console.WriteLine($"Could not schedule BGAppRefreshTask: {refreshError}");
    }

The mechanism that does the upload UploadService is using NSUrlSession, it also writes a temporary file to use CreateUploadTask(request, NSUrl.FromFilename(tempFileName)) that is supposed to work in the background, whole mechanism looks following:

 public NSUrlSession uploadSession;

    public async void EnqueueUpload()
    {
        var accountsTask = await App.PCA.GetAccountsAsync();
        var authResult = await App.PCA.AcquireTokenSilent(App.Scopes, accountsTask.First())
                                      .ExecuteAsync();

        if (uploadSession == null)
            uploadSession = InitBackgroundSession(authResult.AccessToken);

        var datastore = DependencyService.Get<IDataStore<Upload>>();
        var uploads = await datastore.GetUnuploaded();

        foreach (var unUploaded in uploads)
        {
            try
            {
                string folder = unUploaded.Description;
                string subfolder = unUploaded.Category;

                if (string.IsNullOrEmpty(folder) || string.IsNullOrEmpty(subfolder))
                    continue;

                var uploadDto = new Dtos.Upload
                {
                    FolderName = folder,
                    SubFolderName = subfolder,
                    Image = GetImageAsBase64(unUploaded.ImagePath)
                };
                var documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
                var fileName = Path.GetFileName(unUploaded.ImagePath);
                var tempFileName = Path.Combine(documents, $"{fileName}.txt");
                string stringContent = await new StringContent(JsonConvert.SerializeObject(uploadDto), Encoding.UTF8, "application/json").ReadAsStringAsync();
                await File.WriteAllTextAsync(tempFileName, stringContent);

                using (var url = NSUrl.FromString(UploadUrlString))
                using (var request = new NSMutableUrlRequest(url)
                {
                    HttpMethod = "POST",

                })
                {
                    request.Headers.SetValueForKey(NSObject.FromObject("application/json"), new NSString("Content-type"));
                    try
                    {
                        uploadSession.CreateUploadTask(request, NSUrl.FromFilename(tempFileName));

                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"NSMutableUrlRequest failed {e.Message}");
                    }
                }
            }
            catch (Exception e)
            {
                if (e.Message.Contains("Could not find a part of the path"))
                {
                    await datastore.DeleteItemAsync(unUploaded.Id);
                    Console.WriteLine($"deleted");
                }

                Console.WriteLine($"uploadStore failed {e.Message}");
            }
        }
    }
    private string GetImageAsBase64(string path)
    {
        using (var reader = new StreamReader(path))
        using (MemoryStream ms = new MemoryStream())
        {
            reader.BaseStream.CopyTo(ms);
            return Convert.ToBase64String(ms.ToArray());
        }
    }

    public NSUrlSession InitBackgroundSession(string authToken = null, IDataStore<Upload> dataStore = null)
    {
        Console.WriteLine("InitBackgroundSession");
        using (var configuration = NSUrlSessionConfiguration.CreateBackgroundSessionConfiguration(Identifier))
        {
            configuration.AllowsCellularAccess = true;
            configuration.Discretionary = false;
            configuration.AllowsConstrainedNetworkAccess = true;
            configuration.AllowsExpensiveNetworkAccess = true;
            if (string.IsNullOrWhiteSpace(authToken) == false)
            {
                configuration.HttpAdditionalHeaders = NSDictionary.FromObjectsAndKeys(new string[] { $"Bearer {authToken}" }, new string[] { "Authorization" });
            }

            return NSUrlSession.FromConfiguration(configuration, new UploadDelegate(dataStore), null);
        }
    }
}

public class UploadDelegate : NSUrlSessionTaskDelegate, INSUrlSessionDelegate
{
    public IDataStore<Upload> Datastore { get; }

    public UploadDelegate(IDataStore<Upload> datastore)
    {
        this.Datastore = datastore;
    }
    public override void DidCompleteWithError(NSUrlSession session, NSUrlSessionTask task, NSError error)
    {
        Console.WriteLine(string.Format("DidCompleteWithError TaskId: {0}{1}", task.TaskIdentifier, (error == null ? "" : " Error: " + error.Description)));

        if (error == null)
        {
            ProcessCompletedTask(task);
        }
    }
    public void ProcessCompletedTask(NSUrlSessionTask sessionTask)
    {
        try
        {
            Console.WriteLine(string.Format("Task ID: {0}, State: {1}, Response: {2}", sessionTask.TaskIdentifier, sessionTask.State, sessionTask.Response));

            if (sessionTask.Response == null || sessionTask.Response.ToString() == "")
            {
                Console.WriteLine("ProcessCompletedTask no response...");
            }
            else
            {
                var resp = (NSHttpUrlResponse)sessionTask.Response;
                Console.WriteLine("ProcessCompletedTask got response...");
                if (sessionTask.State == NSUrlSessionTaskState.Completed && resp.StatusCode == 201)
                {
                    Console.WriteLine("201");
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("ProcessCompletedTask Ex: {0}", ex.Message);
        }
    }
    public override void DidBecomeInvalid(NSUrlSession session, NSError error)
    {
        Console.WriteLine("DidBecomeInvalid" + (error == null ? "undefined" : error.Description));
    }

    public override void DidFinishEventsForBackgroundSession(NSUrlSession session)
    {
        Console.WriteLine("DidFinishEventsForBackgroundSession");
    }
    public override void DidSendBodyData(NSUrlSession session, NSUrlSessionTask task, long bytesSent, long totalBytesSent, long totalBytesExpectedToSend)
    {
    }
}

Everything works if the iOS charger cable is plugged in, however, if it isn't - nothing fires. I have a network debugging set up with plenty of logging into the console, and I can see that nothing happens on iPhone.

I did watch https://developer.apple.com/videos/play/wwdc2020/10063/ and have set my Session to false

    using (var configuration = NSUrlSessionConfiguration.CreateBackgroundSessionConfiguration(Identifier))
    {
        configuration.Discretionary = false;

How do I make the NSUrlSession upload task to fire when iOS charger cable is unplugged on iOS 14.4?

Xamarin
Xamarin
A Microsoft open-source app platform for building Android and iOS apps with .NET and C#.
5,325 questions
0 comments No comments
{count} votes