1// Copyright 2018-present 650 Industries. All rights reserved.
2
3#import <ExpoModulesCore/EXDefines.h>
4#import <ExpoModulesCore/EXTaskConsumerInterface.h>
5
6#import <EXTaskManager/EXTask.h>
7#import <EXTaskManager/EXTaskService.h>
8
9#import <UMAppLoader/UMAppLoaderProvider.h>
10#import <UMAppLoader/UMAppRecordInterface.h>
11
12@interface EXTaskService ()
13
14// Array of task requests that are being executed.
15@property (nonatomic, strong) NSMutableArray<EXTaskExecutionRequest *> *requests;
16
17// Table of registered tasks. Schema: { "<appId>": { "<taskName>": EXTask } }
18@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, EXTask *> *> *tasks;
19
20// Dictionary with app records of running background apps. Schema: { "<appId>": EXAppRecordInterface }
21@property (nonatomic, strong) NSMutableDictionary<NSString *, id<UMAppRecordInterface>> *appRecords;
22
23// MapTable with task managers of running (foregrounded) apps. Schema: { "<appId>": EXTaskManagerInterface }
24@property (nonatomic, strong) NSMapTable<NSString *, id<EXTaskManagerInterface>> *taskManagers;
25
26// Same as above but for headless (backgrounded) apps.
27@property (nonatomic, strong) NSMapTable<NSString *, id<EXTaskManagerInterface>> *headlessTaskManagers;
28
29// Dictionary with events queues storing event bodies that should be passed to the manager as soon as it's available.
30// Schema: { "<appId>": [<eventBodies...>] }
31@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableArray<NSDictionary *> *> *eventsQueues;
32
33// Storing events per app. Schema: { "<appId>": [<eventIds...>] }
34@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableArray<NSString *> *> *events;
35
36@end
37
38@implementation EXTaskService
39
40EX_REGISTER_SINGLETON_MODULE(TaskService)
41
42- (instancetype)init
43{
44  if (self = [super init]) {
45    _tasks = [NSMutableDictionary new];
46    _requests = [NSMutableArray new];
47    _appRecords = [NSMutableDictionary new];
48    _taskManagers = [NSMapTable strongToWeakObjectsMapTable];
49    _headlessTaskManagers = [NSMapTable strongToWeakObjectsMapTable];
50    _eventsQueues = [NSMutableDictionary new];
51    _events = [NSMutableDictionary new];
52  }
53  return self;
54}
55
56# pragma mark - EXTaskServiceInterface
57
58/**
59 *  Returns boolean value whether the task with given name is already registered for given appId.
60 */
61- (BOOL)hasRegisteredTaskWithName:(nonnull NSString *)taskName forAppId:(nonnull NSString *)appId
62{
63  id<EXTaskInterface> task = [self _getTaskWithName:taskName forAppId:appId];
64  return task != nil;
65}
66
67/**
68 *  Creates a new task, registers it and saves to the config stored in user defaults.
69 *  It can throw an exception if given consumer class doesn't conform to EXTaskConsumerInterface protocol
70 *  or another task with the same name and appId is already registered.
71 */
72- (void)registerTaskWithName:(NSString *)taskName
73                       appId:(NSString *)appId
74                      appUrl:(NSString *)appUrl
75               consumerClass:(Class)consumerClass
76                     options:(NSDictionary *)options
77{
78  Class unversionedConsumerClass = [self _unversionedClassFromClass:consumerClass];
79
80  // Given consumer class doesn't conform to EXTaskConsumerInterface protocol
81  if (![unversionedConsumerClass conformsToProtocol:@protocol(EXTaskConsumerInterface)]) {
82    NSString *reason = @"Invalid `consumer` argument. It must be a class that conforms to EXTaskConsumerInterface protocol.";
83    @throw [NSException exceptionWithName:@"E_INVALID_TASK_CONSUMER" reason:reason userInfo:nil];
84  }
85
86  id<EXTaskInterface> task = [self _getTaskWithName:taskName forAppId:appId];
87
88  if (task && [task.consumer isMemberOfClass:unversionedConsumerClass]) {
89    // Task already exists. Let's just update its options.
90    [task setOptions:options];
91
92    if ([task.consumer respondsToSelector:@selector(setOptions:)]) {
93      [task.consumer setOptions:options];
94    }
95  } else {
96    task = [self _internalRegisterTaskWithName:taskName
97                                         appId:appId
98                                        appUrl:appUrl
99                                 consumerClass:unversionedConsumerClass
100                                       options:options];
101  }
102  [self _addTaskToConfig:task];
103}
104
105/**
106 *  Unregisters task with given name and for given appId. Also removes the task from the config.
107 */
108- (void)unregisterTaskWithName:(NSString *)taskName
109                      forAppId:(NSString *)appId
110                 consumerClass:(Class)consumerClass
111{
112  EXTask *task = (EXTask *)[self _getTaskWithName:taskName forAppId:appId];
113
114  if (!task) {
115    NSString *reason = [NSString stringWithFormat:@"Task '%@' not found for app ID '%@'.", taskName, appId];
116    @throw [NSException exceptionWithName:@"E_TASK_NOT_FOUND" reason:reason userInfo:nil];
117  }
118
119  if (consumerClass != nil && ![task.consumer isMemberOfClass:[self _unversionedClassFromClass:consumerClass]]) {
120    NSString *reason = [NSString stringWithFormat:@"Invalid task consumer. Cannot unregister task with name '%@' because it is associated with different consumer class.", taskName];
121    @throw [NSException exceptionWithName:@"E_INVALID_TASK_CONSUMER" reason:reason userInfo:nil];
122  }
123
124  NSMutableDictionary *appTasks = [[self _getTasksForAppId:appId] mutableCopy];
125
126  [appTasks removeObjectForKey:taskName];
127
128  if (appTasks.count == 0) {
129    [_tasks removeObjectForKey:appId];
130  } else {
131    [_tasks setObject:appTasks forKey:appId];
132  }
133
134  if ([task.consumer respondsToSelector:@selector(didUnregister)]) {
135    [task.consumer didUnregister];
136  }
137  [self _removeTaskFromConfig:task.name appId:task.appId];
138}
139
140/**
141 *  Unregisters all tasks associated with the specific app.
142 */
143- (void)unregisterAllTasksForAppId:(NSString *)appId
144{
145  NSDictionary *appTasks = _tasks[appId];
146
147  if (appTasks) {
148    // Call `didUnregister` on task consumers
149    for (EXTask *task in [appTasks allValues]) {
150      if ([task.consumer respondsToSelector:@selector(didUnregister)]) {
151        [task.consumer didUnregister];
152      }
153    }
154
155    [_tasks removeObjectForKey:appId];
156
157    // Remove the app from the config in user defaults.
158    [self _removeFromConfigAppWithId:appId];
159  }
160}
161
162- (BOOL)taskWithName:(NSString *)taskName
163            forAppId:(NSString *)appId
164  hasConsumerOfClass:(Class)consumerClass
165{
166  id<EXTaskInterface> task = [self _getTaskWithName:taskName forAppId:appId];
167  Class unversionedConsumerClass = [self _unversionedClassFromClass:consumerClass];
168  return task ? [task.consumer isMemberOfClass:unversionedConsumerClass] : NO;
169}
170
171- (NSDictionary *)getOptionsForTaskName:(NSString *)taskName
172                               forAppId:(NSString *)appId
173{
174  id<EXTaskInterface> task = [self _getTaskWithName:taskName forAppId:appId];
175  return task.options;
176}
177
178- (NSArray *)getRegisteredTasksForAppId:(NSString *)appId
179{
180  NSDictionary<NSString *, id<EXTaskInterface>> *tasks = [self _getTasksForAppId:appId];
181  NSMutableArray *results = [NSMutableArray new];
182
183  for (NSString *taskName in tasks) {
184    id<EXTaskInterface> task = tasks[taskName];
185
186    if (task != nil) {
187      [results addObject:@{
188        @"taskName": taskName,
189        @"taskType": task.consumer.taskType,
190        @"options": task.options,
191      }];
192    }
193  }
194  return results;
195}
196
197- (void)notifyTaskWithName:(NSString *)taskName
198                  forAppId:(NSString *)appId
199     didFinishWithResponse:(NSDictionary *)response
200{
201  id<EXTaskInterface> task = [self _getTaskWithName:taskName forAppId:appId];
202  NSString *eventId = response[@"eventId"];
203  id result = response[@"result"];
204
205  if ([task.consumer respondsToSelector:@selector(normalizeTaskResult:)]) {
206    result = @([task.consumer normalizeTaskResult:result]);
207  }
208  if ([task.consumer respondsToSelector:@selector(didFinish)]) {
209    [task.consumer didFinish];
210  }
211
212  // Inform requests about finished tasks
213  for (EXTaskExecutionRequest *request in [_requests copy]) {
214    if ([request isIncludingTask:task]) {
215      [request task:task didFinishWithResult:result];
216    }
217  }
218
219  // Remove event and maybe invalidate related app record
220  NSMutableArray *appEvents = _events[appId];
221
222  if (appEvents) {
223    [appEvents removeObject:eventId];
224
225    if (appEvents.count == 0) {
226      [self->_events removeObjectForKey:appId];
227
228      // Invalidate app record but after 1 seconds delay so we can still take batched events.
229      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
230        if (!self->_events[appId]) {
231          [self _invalidateAppWithId:appId];
232        }
233      });
234    }
235  }
236}
237
238- (void)setTaskManager:(id<EXTaskManagerInterface>)taskManager
239              forAppId:(NSString *)appId
240               withUrl:(NSString *)appUrl
241{
242  // Determine in which table the task manager will be stored.
243  // Having two tables for them is to prevent race condition problems,
244  // when both foreground and background apps are launching at the same time.
245  BOOL isHeadless = [taskManager isRunningInHeadlessMode];
246  NSMapTable *taskManagersTable = isHeadless ? _headlessTaskManagers : _taskManagers;
247
248  // Set task manager in appropriate table.
249  [taskManagersTable setObject:taskManager forKey:appId];
250
251  // Execute events waiting for the task manager.
252  NSMutableArray *appEventQueue = _eventsQueues[appId];
253
254  if (appEventQueue) {
255    for (NSDictionary *body in appEventQueue) {
256      [taskManager executeWithBody:body];
257    }
258  }
259
260  // Remove events queue for that app.
261  [_eventsQueues removeObjectForKey:appId];
262
263  if (!isHeadless) {
264    // Maybe update app url in user defaults. It might change only in non-headless mode.
265    [self _maybeUpdateAppUrl:appUrl forAppId:appId];
266  }
267}
268
269# pragma mark - EXTaskDelegate
270
271- (void)executeTask:(nonnull id<EXTaskInterface>)task
272           withData:(nullable NSDictionary *)data
273          withError:(nullable NSError *)error
274{
275  id<EXTaskManagerInterface> taskManager = [self _taskManagerForAppId:task.appId];
276  NSDictionary *executionInfo = [self _executionInfoForTask:task];
277  NSDictionary *body = @{
278    @"executionInfo": executionInfo,
279    @"data": data ?: @{},
280    @"error": EXNullIfNil([self _exportError:error]),
281  };
282
283  NSLog(@"EXTaskService: Executing task '%@' for app '%@'.", task.name, task.appId);
284
285  // Save an event so we can keep tracking events for this app
286  NSMutableArray *appEvents = _events[task.appId] ?: [NSMutableArray new];
287  [appEvents addObject:executionInfo[@"eventId"]];
288  [_events setObject:appEvents forKey:task.appId];
289
290  if (taskManager != nil) {
291    // Task manager is initialized and can execute events
292    [taskManager executeWithBody:body];
293    return;
294  }
295
296  if (_appRecords[task.appId] == nil) {
297    // No app record yet - let's spin it up!
298    [self _loadAppWithId:task.appId appUrl:task.appUrl];
299  }
300
301  // App record for that app exists, but it's not fully loaded as its task manager is not there yet.
302  // We need to add event's body to the queue from which events will be executed once the task manager is ready.
303  NSMutableArray *appEventsQueue = _eventsQueues[task.appId] ?: [NSMutableArray new];
304  [appEventsQueue addObject:body];
305  [_eventsQueues setObject:appEventsQueue forKey:task.appId];
306  return;
307}
308
309# pragma mark - statics
310
311+ (BOOL)hasBackgroundModeEnabled:(nonnull NSString *)backgroundMode
312{
313  NSArray *backgroundModes = [[NSBundle mainBundle] infoDictionary][@"UIBackgroundModes"];
314  return backgroundModes != nil && [backgroundModes containsObject:backgroundMode];
315}
316
317# pragma mark - AppDelegate handlers
318
319- (void)applicationDidFinishLaunchingWithOptions:(NSDictionary *)launchOptions
320{
321  [self _restoreTasks];
322
323  EXTaskLaunchReason launchReason = [self _launchReasonForLaunchOptions:launchOptions];
324  [self runTasksWithReason:launchReason userInfo:launchOptions completionHandler:nil];
325}
326
327- (void)runTasksWithReason:(EXTaskLaunchReason)launchReason
328                  userInfo:(nullable NSDictionary *)userInfo
329         completionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
330{
331  [self _runTasksSupportingLaunchReason:launchReason userInfo:userInfo callback:^(NSArray * _Nonnull results) {
332    if (!completionHandler) {
333      return;
334    }
335    BOOL wasCompletionCalled = NO;
336
337    // Iterate through the array of results. If there is at least one "NewData" or "Failed" result,
338    // then just call completionHandler immediately with that value, otherwise return "NoData".
339    for (NSNumber *result in results) {
340      UIBackgroundFetchResult fetchResult = [result intValue];
341
342      if (fetchResult == UIBackgroundFetchResultNewData || fetchResult == UIBackgroundFetchResultFailed) {
343        completionHandler(fetchResult);
344        wasCompletionCalled = YES;
345        break;
346      }
347    }
348    if (!wasCompletionCalled) {
349      completionHandler(UIBackgroundFetchResultNoData);
350    }
351  }];
352}
353
354# pragma mark - internals
355
356
357/**
358 *  Returns the task object for given name and appId.
359 */
360- (id<EXTaskInterface>)_getTaskWithName:(NSString *)taskName
361                               forAppId:(NSString *)appId
362{
363  return [self _getTasksForAppId:appId][taskName];
364}
365
366/**
367 *  Returns dictionary of tasks for given appId. Dictionary in which the keys are the names for tasks,
368 *  while the values are the task objects.
369 */
370- (NSDictionary<NSString *, EXTask *> *)_getTasksForAppId:(NSString *)appId
371{
372  return _tasks[appId];
373}
374
375/**
376 *  Internal method that creates a task and registers it. It doesn't save anything to user defaults!
377 */
378- (EXTask *)_internalRegisterTaskWithName:(nonnull NSString *)taskName
379                                    appId:(nonnull NSString *)appId
380                                   appUrl:(nonnull NSString *)appUrl
381                            consumerClass:(Class)consumerClass
382                                  options:(nullable NSDictionary *)options
383{
384  NSMutableDictionary *appTasks = [[self _getTasksForAppId:appId] mutableCopy] ?: [NSMutableDictionary new];
385  EXTask *task = [[EXTask alloc] initWithName:taskName
386                                        appId:appId
387                                       appUrl:appUrl
388                                consumerClass:consumerClass
389                                      options:options
390                                     delegate:self];
391
392  [appTasks setObject:task forKey:task.name];
393  [_tasks setObject:appTasks forKey:appId];
394  [task.consumer didRegisterTask:task];
395  return task;
396}
397
398/**
399 *  Modifies existing config of registered task with given task.
400 */
401- (void)_addTaskToConfig:(nonnull id<EXTaskInterface>)task
402{
403  NSMutableDictionary *dict = [[self _dictionaryWithRegisteredTasks] mutableCopy] ?: [NSMutableDictionary new];
404  NSMutableDictionary *appDict = [dict[task.appId] mutableCopy] ?: [NSMutableDictionary new];
405  NSMutableDictionary *tasks = [appDict[@"tasks"] mutableCopy] ?: [NSMutableDictionary new];
406  NSDictionary *taskDict = [self _dictionaryFromTask:task];
407
408  [tasks setObject:taskDict forKey:task.name];
409  [appDict setObject:tasks forKey:@"tasks"];
410  if (task.appUrl) {
411    [appDict setObject:task.appUrl forKey:@"appUrl"];
412  }
413  [dict setObject:appDict forKey:task.appId];
414  [self _saveConfigWithDictionary:dict];
415}
416
417/**
418 *  Removes given task from the config of registered tasks.
419 */
420- (void)_removeTaskFromConfig:(NSString *)taskName appId:(NSString *)appId
421{
422  NSMutableDictionary *dict = [[self _dictionaryWithRegisteredTasks] mutableCopy];
423  NSMutableDictionary *appDict = [dict[appId] mutableCopy];
424  NSMutableDictionary *tasks = [appDict[@"tasks"] mutableCopy];
425
426  if (tasks != nil) {
427    [tasks removeObjectForKey:taskName];
428
429    if ([tasks count] > 0) {
430      [appDict setObject:tasks forKey:@"tasks"];
431      [dict setObject:appDict forKey:appId];
432    } else {
433      [dict removeObjectForKey:appId];
434    }
435    [self _saveConfigWithDictionary:dict];
436  }
437}
438
439- (void)_removeFromConfigAppWithId:(nonnull NSString *)appId
440{
441  NSMutableDictionary *dict = [[self _dictionaryWithRegisteredTasks] mutableCopy];
442
443  if (dict[appId]) {
444    [dict removeObjectForKey:appId];
445    [self _saveConfigWithDictionary:dict];
446  }
447}
448
449/**
450 *  Saves given dictionary to user defaults, as a config with registered tasks.
451 */
452- (void)_saveConfigWithDictionary:(nonnull NSDictionary *)dict
453{
454  NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
455  [userDefaults setObject:dict forKey:NSStringFromClass([self class])];
456  [userDefaults synchronize];
457}
458
459- (void)_iterateTasksUsingBlock:(void(^)(id<EXTaskInterface> task))block
460{
461  for (NSString *appId in _tasks) {
462    NSDictionary *appTasks = [self _getTasksForAppId:appId];
463
464    for (NSString *taskName in appTasks) {
465      id<EXTaskInterface> task = [self _getTaskWithName:taskName forAppId:appId];
466      block(task);
467    }
468  }
469}
470
471/**
472 *  Returns NSDictionary with registered tasks.
473 *  Schema: {
474 *    "<appId>": {
475 *      "appUrl": "url to the bundle",
476 *      "tasks": {
477 *        "<taskName>": {
478 *          "name": "task's name",
479 *          "consumerClass": "name of consumer class, e.g. EXLocationTaskConsumer",
480 *          "consumerVersion": 1,
481 *          "options": {},
482 *        },
483 *      }
484 *    }
485 *  }
486 */
487- (nullable NSDictionary *)_dictionaryWithRegisteredTasks
488{
489  NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
490  return [userDefaults dictionaryForKey:NSStringFromClass([self class])];
491}
492
493/**
494 *  Returns NSDictionary representing single task.
495 */
496- (nullable NSDictionary *)_dictionaryFromTask:(id<EXTaskInterface>)task
497{
498  return @{
499    @"name": task.name,
500    @"consumerClass": [self _unversionedClassNameFromClass:task.consumer.class],
501    @"consumerVersion": @([self _consumerVersion:task.consumer.class]),
502    @"options": EXNullIfNil([task options]),
503  };
504}
505
506- (void)_runTasksSupportingLaunchReason:(EXTaskLaunchReason)launchReason
507                               userInfo:(nullable NSDictionary *)userInfo
508                               callback:(void(^)(NSArray * _Nonnull results))callback
509{
510  __block EXTaskExecutionRequest *request;
511
512  request = [[EXTaskExecutionRequest alloc] initWithCallback:^(NSArray * _Nonnull results) {
513    if (callback != nil) {
514      callback(results);
515    }
516
517    [self->_requests removeObject:request];
518    request = nil;
519  }];
520
521  [_requests addObject:request];
522
523  [self _iterateTasksUsingBlock:^(id<EXTaskInterface> task) {
524    if ([task.consumer.class respondsToSelector:@selector(supportsLaunchReason:)] && [task.consumer.class supportsLaunchReason:launchReason]) {
525      [self _addTask:task toRequest:request withInfo:userInfo];
526    }
527  }];
528
529  // Evaluate request immediately if no tasks were added.
530  [request maybeEvaluate];
531}
532
533- (void)_loadAppWithId:(nonnull NSString *)appId
534                appUrl:(nonnull NSString *)appUrl
535{
536  id<UMAppLoaderInterface> appLoader = [[UMAppLoaderProvider sharedInstance] createAppLoader:@"react-native-experience"];
537
538  if (appLoader != nil && appUrl != nil) {
539    __block id<UMAppRecordInterface> appRecord;
540
541    NSLog(@"EXTaskService: Loading headless app '%@' with url '%@'.", appId, appUrl);
542
543    appRecord = [appLoader loadAppWithUrl:appUrl options:nil callback:^(BOOL success, NSError *error) {
544      if (!success) {
545        NSLog(@"EXTaskService: Loading app '%@' from url '%@' failed. Error description: %@", appId, appUrl, error.description);
546        [self->_events removeObjectForKey:appId];
547        [self->_eventsQueues removeObjectForKey:appId];
548        [self->_appRecords removeObjectForKey:appId];
549
550        // Host unreachable? Unregister all tasks for that app.
551        [self unregisterAllTasksForAppId:appId];
552      }
553    }];
554
555    [_appRecords setObject:appRecord forKey:appId];
556  }
557}
558
559/**
560 *  Returns task manager for given appId. Task managers initialized in non-headless contexts have precedence over headless one.
561 */
562- (id<EXTaskManagerInterface>)_taskManagerForAppId:(NSString *)appId
563{
564  id<EXTaskManagerInterface> taskManager = [_taskManagers objectForKey:appId];
565  return taskManager ?: [_headlessTaskManagers objectForKey:appId];
566}
567
568/**
569 *  Updates appUrl for the app with given appId if necessary.
570 *  Url to the app might change over time, especially in development.
571 */
572- (void)_maybeUpdateAppUrl:(NSString *)appUrl
573                  forAppId:(NSString *)appId
574{
575  NSMutableDictionary *dict = [[self _dictionaryWithRegisteredTasks] mutableCopy];
576  NSMutableDictionary *appDict = [dict[appId] mutableCopy];
577
578  if (appDict != nil && ![appDict[@"appUrl"] isEqualToString:appUrl]) {
579    appDict[@"appUrl"] = appUrl;
580    dict[appId] = appDict;
581    [self _saveConfigWithDictionary:dict];
582  }
583}
584
585- (void)_restoreTasks
586{
587  NSDictionary *config = [self _dictionaryWithRegisteredTasks];
588
589  if (config) {
590    // Log restored config so it's debuggable
591    NSLog(@"EXTaskService: Restoring tasks configuration: %@", config.description);
592
593    for (NSString *appId in config) {
594      NSDictionary *appConfig = config[appId];
595      NSDictionary *tasksConfig = appConfig[@"tasks"];
596      NSString *appUrl = appConfig[@"appUrl"];
597
598      for (NSString *taskName in tasksConfig) {
599        NSDictionary *taskConfig = tasksConfig[taskName];
600        NSString *consumerClassName = taskConfig[@"consumerClass"];
601        Class consumerClass = NSClassFromString(consumerClassName);
602
603        if (consumerClass != nil) {
604          NSUInteger currentConsumerVersion = [self _consumerVersion:consumerClass];
605          NSUInteger previousConsumerVersion = [taskConfig[@"consumerVersion"] unsignedIntegerValue];
606
607          // Check whether the current consumer class is compatible with the saved version
608          if (currentConsumerVersion == previousConsumerVersion) {
609            [self _internalRegisterTaskWithName:taskName
610                                          appId:appId
611                                         appUrl:appUrl
612                                  consumerClass:consumerClass
613                                        options:taskConfig[@"options"]];
614          } else {
615            EXLogWarn(
616                      @"EXTaskService: Task consumer '%@' has version '%d' that is not compatible with the saved version '%d'.",
617                      consumerClassName,
618                      currentConsumerVersion,
619                      previousConsumerVersion
620                      );
621            [self _removeTaskFromConfig:taskName appId:appId];
622          }
623        } else {
624          EXLogWarn(@"EXTaskService: Cannot restore task '%@' because consumer class doesn't exist.", taskName);
625          [self _removeTaskFromConfig:taskName appId:appId];
626        }
627      }
628    }
629  }
630}
631
632- (void)_addTask:(id<EXTaskInterface>)task toRequest:(EXTaskExecutionRequest *)request withInfo:(nullable NSDictionary *)info
633{
634  [request addTask:task];
635
636
637  // Inform the consumer that the task can be executed from then on.
638  // Some types of background tasks (like background fetch) may execute the task immediately.
639  if ([[task consumer] respondsToSelector:@selector(didBecomeReadyToExecuteWithData:)]) {
640    [[task consumer] didBecomeReadyToExecuteWithData:info ?: @{}];
641  }
642}
643
644- (NSDictionary *)_executionInfoForTask:(nonnull id<EXTaskInterface>)task
645{
646  NSString *appState = [self _exportAppState:[[UIApplication sharedApplication] applicationState]];
647  return @{
648    @"eventId": [[NSUUID UUID] UUIDString],
649    @"taskName": task.name,
650    @"appState": appState,
651  };
652}
653
654- (void)_invalidateAppWithId:(NSString *)appId
655{
656  id<UMAppRecordInterface> appRecord = _appRecords[appId];
657
658  if (appRecord) {
659    [appRecord invalidate];
660    [_appRecords removeObjectForKey:appId];
661    [_headlessTaskManagers removeObjectForKey:appId];
662  }
663}
664
665- (nullable NSDictionary *)_exportError:(nullable NSError *)error
666{
667  if (error == nil) {
668    return nil;
669  }
670  return @{
671    @"code": @(error.code),
672    @"message": error.description,
673  };
674}
675
676- (EXTaskLaunchReason)_launchReasonForLaunchOptions:(nullable NSDictionary *)launchOptions
677{
678  if (launchOptions == nil) {
679    return EXTaskLaunchReasonUser;
680  }
681  if (launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey]) {
682    return EXTaskLaunchReasonBluetoothCentrals;
683  }
684  if (launchOptions[UIApplicationLaunchOptionsBluetoothPeripheralsKey]) {
685    return EXTaskLaunchReasonBluetoothPeripherals;
686  }
687  if (launchOptions[UIApplicationLaunchOptionsLocationKey]) {
688    return EXTaskLaunchReasonLocation;
689  }
690  if (launchOptions[UIApplicationLaunchOptionsNewsstandDownloadsKey]) {
691    return EXTaskLaunchReasonNewsstandDownloads;
692  }
693  if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) {
694    return EXTaskLaunchReasonRemoteNotification;
695  }
696  return EXTaskLaunchReasonUnrecognized;
697}
698
699- (NSString *)_exportAppState:(UIApplicationState)appState
700{
701  switch (appState) {
702    case UIApplicationStateActive:
703      return @"active";
704    case UIApplicationStateInactive:
705      return @"inactive";
706    case UIApplicationStateBackground:
707      return @"background";
708  }
709}
710
711/**
712 *  Returns task consumer's version. Defaults to 0 if `taskConsumerVersion` is not implemented.
713 */
714- (NSUInteger)_consumerVersion:(Class)consumerClass
715{
716  if (consumerClass && [consumerClass respondsToSelector:@selector(taskConsumerVersion)]) {
717    return [consumerClass taskConsumerVersion];
718  }
719  return 0;
720}
721
722/**
723 *  Method that unversions class names, so we can always use unversioned task consumer classes.
724 */
725- (NSString *)_unversionedClassNameFromClass:(Class)versionedClass
726{
727  NSString *versionedClassName = NSStringFromClass(versionedClass);
728  NSRegularExpression *regexp = [NSRegularExpression regularExpressionWithPattern:@"^ABI\\d+_\\d+_\\d+" options:0 error:nil];
729
730  return [regexp stringByReplacingMatchesInString:versionedClassName
731                                          options:0
732                                            range:NSMakeRange(0, versionedClassName.length)
733                                     withTemplate:@""];
734}
735
736/**
737 *  Returns unversioned class from versioned one.
738 */
739- (Class)_unversionedClassFromClass:(Class)versionedClass
740{
741  NSString *unversionedClassName = [self _unversionedClassNameFromClass:versionedClass];
742  return NSClassFromString(unversionedClassName);
743}
744
745@end
746