Search code examples
iosxamarin.formsfile-uploadbackground-processnsurlsession

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


We are working on a Xamarin Forms app that is supposed to upload photos to API in the background. The app is being custom-made for a client by their request, so they will set their phones to whatever permissions need to be set.

Below works fine if the charging cable is plugged in.

I am using BGTaskScheduler (iOS13+) and queuing both types of tasks (BGProcessingTaskRequest and BGAppRefreshTaskRequest) so that if the cable 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.

"Low power mode" setting on iOS is off.

I have watched Background execution demystified and I am setting session configuration.Discretionary = false;

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


Solution

  • Following works without charging cable:

    public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
    {
       
        public Action BackgroundSessionCompletionHandler { get; set; }
    
        public static string UploadTaskId { get; } = "XXX.upload";
        public static NSString UploadSuccessNotificationName { get; } = new NSString($"{UploadTaskId}.success");
        public static string RefreshTaskId { get; } = "XXX.refresh";
        public static NSString RefreshSuccessNotificationName { get; } = new NSString($"{RefreshTaskId}.success");
    
        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 bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
        {
            AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(url);
            return true;
        }
    
        public override void HandleEventsForBackgroundUrl(UIApplication application, string sessionIdentifier, Action completionHandler)
        {
            Console.WriteLine("HandleEventsForBackgroundUrl");
            BackgroundSessionCompletionHandler = completionHandler;
        }
    
        public override void OnActivated(UIApplication application)
        {
            Console.WriteLine("OnActivated");
            var uploadService = new UploadService();
            uploadService.EnqueueUpload();
        }
    
        public override void OnResignActivation(UIApplication application)
        {
            Console.WriteLine("OnResignActivation");
        }
    
        private void HandleAppRefresh(BGAppRefreshTask task)
        {
            task.ExpirationHandler = () =>
            {
                Console.WriteLine("BGAppRefreshTask ExpirationHandler");
    
                var refresh = new BGAppRefreshTaskRequest(RefreshTaskId);
                BGTaskScheduler.Shared.Submit(refresh, out NSError refreshError);
    
                if (refreshError != null)
                    Console.WriteLine($"BGAppRefreshTask ExpirationHandler Could not schedule BGAppRefreshTask: {refreshError}");
            };
    
            HandleUpload(task);
        }
    
        public override void DidEnterBackground(UIApplication application) => ScheduleUpload();
    
        private void HandleUpload(BGTask task)
        {
            Console.WriteLine("HandleUpload");
            var uploadService = new UploadService();
            uploadService.EnqueueUpload();
            task.SetTaskCompleted(true);
        }
    
        private void ScheduleUpload()
        {
            Console.WriteLine("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}");
        }
    }
    

    then Upload service:

    public class UploadService : IUploadService
    {
        private const string uploadUrlString = "https://Yadyyadyyada";
    
        public async void EnqueueUpload()
        {
            var accountsTask = await App.PCA.GetAccountsAsync();
            var authResult = await App.PCA.AcquireTokenSilent(App.Scopes, accountsTask.First())
                                          .ExecuteAsync();
    
                    try
                    {
                        var uploadDto = new object();
    
                        var message = new HttpRequestMessage(HttpMethod.Post, uploadUrlString);
                        message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authResult.AccessToken);
                        message.Content = new StringContent(JsonConvert.SerializeObject(uploadDto), Encoding.UTF8, "application/json");
    
                        var response = await httpClient.SendAsync(message);
                        if (response.IsSuccessStatusCode)
                        {
                            var json = await response.Content.ReadAsStringAsync();
                        }
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"EnqueueUpload {e.Message}");
                    }
        }
    }