当 iOS 充电器电缆在 iOS 14.4 上拔下时,NSUrlSession 照片上传任务到 BGTaskScheduler 内的 AzureFunction 不会执行

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

我们正在开发一个 Xamarin Forms 应用程序,它可以在后台将照片上传到 API。该应用程序是根据客户的要求为客户定制的,因此他们会将手机设置为需要设置的任何权限。

如果插入充电线,下面的工作正常。

我正在使用 BGTaskScheduler (iOS13+) 并对两种类型的任务(BGProcessingTaskRequestBGAppRefreshTaskRequest)进行排队,这样如果插入电缆它就会触发关闭 BGProcessingTaskRequest,如果没有,它将等待 BGAppRefreshTaskRequest 获得其处理时间。

我已将 RefreshTaskIdUploadTaskId 添加到 Info.plist

AppDelegate.cs 在 iOS 项目中看起来如下

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

上传 UploadService 的机制正在使用 NSUrlSession,它还写入了一个临时文件以使用 CreateUploadTask(request, NSUrl.FromFilename(tempFileName)) 应该在后台工作,整个机制如下所示:

 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)
    {
    }
}

如果插入 iOS 充电器电缆,一切正常,但是,如果没有,则不会触发。我有一个网络调试设置,大量登录到控制台,我可以看到 iPhone.

上没有任何反应

iOS 上的“低功耗模式”设置已关闭。

我已经看过Background execution demystified并且正在设置会话configuration.Discretionary = false;

如何在 iOS 14.4 上拔下 iOS 充电器电缆时触发 NSUrlSession 上传任务?

以下作品不带充电线:

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

然后上传服务:

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