UNPKG

26.2 kBPlain TextView Raw
1// Copyright 2018-present 650 Industries. All rights reserved.
2
3#import <UMCore/UMDefines.h>
4
5#import <EXTaskManager/EXTask.h>
6#import <EXTaskManager/EXTaskService.h>
7#import <UMTaskManagerInterface/UMTaskConsumerInterface.h>
8
9#import <EXAppLoaderProvider/EXAppLoaderProvider.h>
10#import <EXAppLoaderProvider/EXAppRecordInterface.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<EXAppRecordInterface>> *appRecords;
22
23// MapTable with task managers of running (foregrounded) apps. Schema: { "<appId>": UMTaskManagerInterface }
24@property (nonatomic, strong) NSMapTable<NSString *, id<UMTaskManagerInterface>> *taskManagers;
25
26// Same as above but for headless (backgrounded) apps.
27@property (nonatomic, strong) NSMapTable<NSString *, id<UMTaskManagerInterface>> *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
40UM_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 - UMTaskServiceInterface
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<UMTaskInterface> 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 UMTaskConsumerInterface 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 UMTaskConsumerInterface protocol
81 if (![unversionedConsumerClass conformsToProtocol:@protocol(UMTaskConsumerInterface)]) {
82 NSString *reason = @"Invalid `consumer` argument. It must be a class that conforms to UMTaskConsumerInterface protocol.";
83 @throw [NSException exceptionWithName:@"E_INVALID_TASK_CONSUMER" reason:reason userInfo:nil];
84 }
85
86 id<UMTaskInterface> 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<UMTaskInterface> 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<UMTaskInterface> task = [self _getTaskWithName:taskName forAppId:appId];
175 return task.options;
176}
177
178- (NSArray *)getRegisteredTasksForAppId:(NSString *)appId
179{
180 NSDictionary<NSString *, id<UMTaskInterface>> *tasks = [self _getTasksForAppId:appId];
181 NSMutableArray *results = [NSMutableArray new];
182
183 for (NSString *taskName in tasks) {
184 id<UMTaskInterface> 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<UMTaskInterface> 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<UMTaskManagerInterface>)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<UMTaskInterface>)task
272 withData:(nullable NSDictionary *)data
273 withError:(nullable NSError *)error
274{
275 id<UMTaskManagerInterface> taskManager = [self _taskManagerForAppId:task.appId];
276 NSDictionary *executionInfo = [self _executionInfoForTask:task];
277 NSDictionary *body = @{
278 @"executionInfo": executionInfo,
279 @"data": data ?: @{},
280 @"error": UMNullIfNil([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 UMTaskLaunchReason launchReason = [self _launchReasonForLaunchOptions:launchOptions];
324 [self runTasksWithReason:launchReason userInfo:launchOptions completionHandler:nil];
325}
326
327- (void)runTasksWithReason:(UMTaskLaunchReason)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<UMTaskInterface>)_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<UMTaskInterface>)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 [appDict setObject:task.appUrl forKey:@"appUrl"];
411 [dict setObject:appDict forKey:task.appId];
412 [self _saveConfigWithDictionary:dict];
413}
414
415/**
416 * Removes given task from the config of registered tasks.
417 */
418- (void)_removeTaskFromConfig:(NSString *)taskName appId:(NSString *)appId
419{
420 NSMutableDictionary *dict = [[self _dictionaryWithRegisteredTasks] mutableCopy];
421 NSMutableDictionary *appDict = [dict[appId] mutableCopy];
422 NSMutableDictionary *tasks = [appDict[@"tasks"] mutableCopy];
423
424 if (tasks != nil) {
425 [tasks removeObjectForKey:taskName];
426
427 if ([tasks count] > 0) {
428 [appDict setObject:tasks forKey:@"tasks"];
429 [dict setObject:appDict forKey:appId];
430 } else {
431 [dict removeObjectForKey:appId];
432 }
433 [self _saveConfigWithDictionary:dict];
434 }
435}
436
437- (void)_removeFromConfigAppWithId:(nonnull NSString *)appId
438{
439 NSMutableDictionary *dict = [[self _dictionaryWithRegisteredTasks] mutableCopy];
440
441 if (dict[appId]) {
442 [dict removeObjectForKey:appId];
443 [self _saveConfigWithDictionary:dict];
444 }
445}
446
447/**
448 * Saves given dictionary to user defaults, as a config with registered tasks.
449 */
450- (void)_saveConfigWithDictionary:(nonnull NSDictionary *)dict
451{
452 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
453 [userDefaults setObject:dict forKey:NSStringFromClass([self class])];
454 [userDefaults synchronize];
455}
456
457- (void)_iterateTasksUsingBlock:(void(^)(id<UMTaskInterface> task))block
458{
459 for (NSString *appId in _tasks) {
460 NSDictionary *appTasks = [self _getTasksForAppId:appId];
461
462 for (NSString *taskName in appTasks) {
463 id<UMTaskInterface> task = [self _getTaskWithName:taskName forAppId:appId];
464 block(task);
465 }
466 }
467}
468
469/**
470 * Returns NSDictionary with registered tasks.
471 * Schema: {
472 * "<appId>": {
473 * "appUrl": "url to the bundle",
474 * "tasks": {
475 * "<taskName>": {
476 * "name": "task's name",
477 * "consumerClass": "name of consumer class, e.g. EXLocationTaskConsumer",
478 * "consumerVersion": 1,
479 * "options": {},
480 * },
481 * }
482 * }
483 * }
484 */
485- (nullable NSDictionary *)_dictionaryWithRegisteredTasks
486{
487 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
488 return [userDefaults dictionaryForKey:NSStringFromClass([self class])];
489}
490
491/**
492 * Returns NSDictionary representing single task.
493 */
494- (nullable NSDictionary *)_dictionaryFromTask:(id<UMTaskInterface>)task
495{
496 return @{
497 @"name": task.name,
498 @"consumerClass": [self _unversionedClassNameFromClass:task.consumer.class],
499 @"consumerVersion": @([self _consumerVersion:task.consumer.class]),
500 @"options": UMNullIfNil([task options]),
501 };
502}
503
504- (void)_runTasksSupportingLaunchReason:(UMTaskLaunchReason)launchReason
505 userInfo:(nullable NSDictionary *)userInfo
506 callback:(void(^)(NSArray * _Nonnull results))callback
507{
508 __block EXTaskExecutionRequest *request;
509
510 request = [[EXTaskExecutionRequest alloc] initWithCallback:^(NSArray * _Nonnull results) {
511 if (callback != nil) {
512 callback(results);
513 }
514
515 [self->_requests removeObject:request];
516 request = nil;
517 }];
518
519 [_requests addObject:request];
520
521 [self _iterateTasksUsingBlock:^(id<UMTaskInterface> task) {
522 if ([task.consumer.class respondsToSelector:@selector(supportsLaunchReason:)] && [task.consumer.class supportsLaunchReason:launchReason]) {
523 [self _addTask:task toRequest:request];
524 }
525 }];
526
527 // Evaluate request immediately if no tasks were added.
528 [request maybeEvaluate];
529}
530
531- (void)_loadAppWithId:(nonnull NSString *)appId
532 appUrl:(nonnull NSString *)appUrl
533{
534 id<EXAppLoaderInterface> appLoader = [[EXAppLoaderProvider sharedInstance] createAppLoader:@"react-native-experience"];
535
536 if (appLoader != nil && appUrl != nil) {
537 __block id<EXAppRecordInterface> appRecord;
538
539 NSLog(@"EXTaskService: Loading headless app '%@' with url '%@'.", appId, appUrl);
540
541 appRecord = [appLoader loadAppWithUrl:appUrl options:nil callback:^(BOOL success, NSError *error) {
542 if (!success) {
543 NSLog(@"EXTaskService: Loading app '%@' from url '%@' failed. Error description: %@", appId, appUrl, error.description);
544 [self->_events removeObjectForKey:appId];
545 [self->_eventsQueues removeObjectForKey:appId];
546 [self->_appRecords removeObjectForKey:appId];
547
548 // Host unreachable? Unregister all tasks for that app.
549 [self unregisterAllTasksForAppId:appId];
550 }
551 }];
552
553 [_appRecords setObject:appRecord forKey:appId];
554 }
555}
556
557/**
558 * Returns task manager for given appId. Task managers initialized in non-headless contexts have precedence over headless one.
559 */
560- (id<UMTaskManagerInterface>)_taskManagerForAppId:(NSString *)appId
561{
562 id<UMTaskManagerInterface> taskManager = [_taskManagers objectForKey:appId];
563 return taskManager ?: [_headlessTaskManagers objectForKey:appId];
564}
565
566/**
567 * Updates appUrl for the app with given appId if necessary.
568 * Url to the app might change over time, especially in development.
569 */
570- (void)_maybeUpdateAppUrl:(NSString *)appUrl
571 forAppId:(NSString *)appId
572{
573 NSMutableDictionary *dict = [[self _dictionaryWithRegisteredTasks] mutableCopy];
574 NSMutableDictionary *appDict = [dict[appId] mutableCopy];
575
576 if (appDict != nil && ![appDict[@"appUrl"] isEqualToString:appUrl]) {
577 appDict[@"appUrl"] = appUrl;
578 dict[appId] = appDict;
579 [self _saveConfigWithDictionary:dict];
580 }
581}
582
583- (void)_restoreTasks
584{
585 NSDictionary *config = [self _dictionaryWithRegisteredTasks];
586
587 if (config) {
588 // Log restored config so it's debuggable
589 NSLog(@"EXTaskService: Restoring tasks configuration: %@", config.description);
590
591 for (NSString *appId in config) {
592 NSDictionary *appConfig = config[appId];
593 NSDictionary *tasksConfig = appConfig[@"tasks"];
594 NSString *appUrl = appConfig[@"appUrl"];
595
596 for (NSString *taskName in tasksConfig) {
597 NSDictionary *taskConfig = tasksConfig[taskName];
598 NSString *consumerClassName = taskConfig[@"consumerClass"];
599 Class consumerClass = NSClassFromString(consumerClassName);
600
601 if (consumerClass != nil) {
602 NSUInteger currentConsumerVersion = [self _consumerVersion:consumerClass];
603 NSUInteger previousConsumerVersion = [taskConfig[@"consumerVersion"] unsignedIntegerValue];
604
605 // Check whether the current consumer class is compatible with the saved version
606 if (currentConsumerVersion == previousConsumerVersion) {
607 [self _internalRegisterTaskWithName:taskName
608 appId:appId
609 appUrl:appUrl
610 consumerClass:consumerClass
611 options:taskConfig[@"options"]];
612 } else {
613 UMLogWarn(
614 @"EXTaskService: Task consumer '%@' has version '%d' that is not compatible with the saved version '%d'.",
615 consumerClassName,
616 currentConsumerVersion,
617 previousConsumerVersion
618 );
619 [self _removeTaskFromConfig:taskName appId:appId];
620 }
621 } else {
622 UMLogWarn(@"EXTaskService: Cannot restore task '%@' because consumer class doesn't exist.", taskName);
623 [self _removeTaskFromConfig:taskName appId:appId];
624 }
625 }
626 }
627 }
628}
629
630- (void)_addTask:(id<UMTaskInterface>)task toRequest:(EXTaskExecutionRequest *)request
631{
632 [request addTask:task];
633
634 // Inform the consumer that the task can be executed from then on.
635 // Some types of background tasks (like background fetch) may execute the task immediately.
636 if ([[task consumer] respondsToSelector:@selector(didBecomeReadyToExecute)]) {
637 [[task consumer] didBecomeReadyToExecute];
638 }
639}
640
641- (NSDictionary *)_executionInfoForTask:(nonnull id<UMTaskInterface>)task
642{
643 NSString *appState = [self _exportAppState:[[UIApplication sharedApplication] applicationState]];
644 return @{
645 @"eventId": [[NSUUID UUID] UUIDString],
646 @"taskName": task.name,
647 @"appState": appState,
648 };
649}
650
651- (void)_invalidateAppWithId:(NSString *)appId
652{
653 id<EXAppRecordInterface> appRecord = _appRecords[appId];
654
655 if (appRecord) {
656 [appRecord invalidate];
657 [_appRecords removeObjectForKey:appId];
658 [_headlessTaskManagers removeObjectForKey:appId];
659 }
660}
661
662- (nullable NSDictionary *)_exportError:(nullable NSError *)error
663{
664 if (error == nil) {
665 return nil;
666 }
667 return @{
668 @"code": @(error.code),
669 @"message": error.description,
670 };
671}
672
673- (UMTaskLaunchReason)_launchReasonForLaunchOptions:(nullable NSDictionary *)launchOptions
674{
675 if (launchOptions == nil) {
676 return UMTaskLaunchReasonUser;
677 }
678 if (launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey]) {
679 return UMTaskLaunchReasonBluetoothCentrals;
680 }
681 if (launchOptions[UIApplicationLaunchOptionsBluetoothPeripheralsKey]) {
682 return UMTaskLaunchReasonBluetoothPeripherals;
683 }
684 if (launchOptions[UIApplicationLaunchOptionsLocationKey]) {
685 return UMTaskLaunchReasonLocation;
686 }
687 if (launchOptions[UIApplicationLaunchOptionsNewsstandDownloadsKey]) {
688 return UMTaskLaunchReasonNewsstandDownloads;
689 }
690 if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) {
691 return UMTaskLaunchReasonRemoteNotification;
692 }
693 return UMTaskLaunchReasonUnrecognized;
694}
695
696- (NSString *)_exportAppState:(UIApplicationState)appState
697{
698 switch (appState) {
699 case UIApplicationStateActive:
700 return @"active";
701 case UIApplicationStateInactive:
702 return @"inactive";
703 case UIApplicationStateBackground:
704 return @"background";
705 }
706}
707
708/**
709 * Returns task consumer's version. Defaults to 0 if `taskConsumerVersion` is not implemented.
710 */
711- (NSUInteger)_consumerVersion:(Class)consumerClass
712{
713 if (consumerClass && [consumerClass respondsToSelector:@selector(taskConsumerVersion)]) {
714 return [consumerClass taskConsumerVersion];
715 }
716 return 0;
717}
718
719/**
720 * Method that unversions class names, so we can always use unversioned task consumer classes.
721 */
722- (NSString *)_unversionedClassNameFromClass:(Class)versionedClass
723{
724 NSString *versionedClassName = NSStringFromClass(versionedClass);
725 NSRegularExpression *regexp = [NSRegularExpression regularExpressionWithPattern:@"^ABI\\d+_\\d+_\\d+" options:0 error:nil];
726
727 return [regexp stringByReplacingMatchesInString:versionedClassName
728 options:0
729 range:NSMakeRange(0, versionedClassName.length)
730 withTemplate:@""];
731}
732
733/**
734 * Returns unversioned class from versioned one.
735 */
736- (Class)_unversionedClassFromClass:(Class)versionedClass
737{
738 NSString *unversionedClassName = [self _unversionedClassNameFromClass:versionedClass];
739 return NSClassFromString(unversionedClassName);
740}
741
742@end