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?
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}");
}
}
}