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 |
|
40 | UM_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 |
|
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 |
|
69 |
|
70 |
|
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 |
|
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 |
|
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 |
|
359 |
|
360 | - (id<UMTaskInterface>)_getTaskWithName:(NSString *)taskName
|
361 | forAppId:(NSString *)appId
|
362 | {
|
363 | return [self _getTasksForAppId:appId][taskName];
|
364 | }
|
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
370 | - (NSDictionary<NSString *, EXTask *> *)_getTasksForAppId:(NSString *)appId
|
371 | {
|
372 | return _tasks[appId];
|
373 | }
|
374 |
|
375 |
|
376 |
|
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 |
|
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 |
|
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 |
|
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 |
|
471 |
|
472 |
|
473 |
|
474 |
|
475 |
|
476 |
|
477 |
|
478 |
|
479 |
|
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 |
|
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 |
|
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 |
|
568 |
|
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 |
|
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 |
|
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 |
|
735 |
|
736 | - (Class)_unversionedClassFromClass:(Class)versionedClass
|
737 | {
|
738 | NSString *unversionedClassName = [self _unversionedClassNameFromClass:versionedClass];
|
739 | return NSClassFromString(unversionedClassName);
|
740 | }
|
741 |
|
742 | @end
|