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