mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			430 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Objective-C
		
	
			
		
		
	
	
			430 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Objective-C
		
	
| //
 | |
| //  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| #import "OWSBackgroundTask.h"
 | |
| #import <SessionUtilitiesKit/SessionUtilitiesKit.h>
 | |
| #import <SessionUtilitiesKit/SessionUtilitiesKit-Swift.h>
 | |
| 
 | |
| NS_ASSUME_NONNULL_BEGIN
 | |
| 
 | |
| typedef void (^BackgroundTaskExpirationBlock)(void);
 | |
| typedef NSNumber *OWSTaskId;
 | |
| 
 | |
| // This class can be safely accessed and used from any thread.
 | |
| @interface OWSBackgroundTaskManager ()
 | |
| 
 | |
| // This property should only be accessed while synchronized on this instance.
 | |
| @property (nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
 | |
| 
 | |
| // This property should only be accessed while synchronized on this instance.
 | |
| @property (nonatomic) NSMutableDictionary<OWSTaskId, BackgroundTaskExpirationBlock> *expirationMap;
 | |
| 
 | |
| // This property should only be accessed while synchronized on this instance.
 | |
| @property (nonatomic) unsigned long long idCounter;
 | |
| 
 | |
| // Note that this flag is set a little early in "will resign active".
 | |
| //
 | |
| // This property should only be accessed while synchronized on this instance.
 | |
| @property (nonatomic) BOOL isAppActive;
 | |
| 
 | |
| // We use this timer to provide continuity and reduce churn,
 | |
| // so that if one OWSBackgroundTask ends right before another
 | |
| // begins, we use a single uninterrupted background that
 | |
| // spans their lifetimes.
 | |
| //
 | |
| // This property should only be accessed while synchronized on this instance.
 | |
| @property (nonatomic, nullable) NSTimer *continuityTimer;
 | |
| 
 | |
| @end
 | |
| 
 | |
| #pragma mark -
 | |
| 
 | |
| @implementation OWSBackgroundTaskManager
 | |
| 
 | |
| + (instancetype)sharedManager
 | |
| {
 | |
|     static OWSBackgroundTaskManager *sharedMyManager = nil;
 | |
|     static dispatch_once_t onceToken;
 | |
|     dispatch_once(&onceToken, ^{
 | |
|         sharedMyManager = [[self alloc] initDefault];
 | |
|     });
 | |
|     return sharedMyManager;
 | |
| }
 | |
| 
 | |
| - (instancetype)initDefault
 | |
| {
 | |
|     self = [super init];
 | |
| 
 | |
|     if (!self) {
 | |
|         return self;
 | |
|     }
 | |
| 
 | |
|     self.backgroundTaskId = UIBackgroundTaskInvalid;
 | |
|     self.expirationMap = [NSMutableDictionary new];
 | |
|     self.idCounter = 0;
 | |
|     self.isAppActive = [OWSCurrentAppContext isMainAppAndActive];
 | |
| 
 | |
|     return self;
 | |
| }
 | |
| 
 | |
| - (void)dealloc
 | |
| {
 | |
|     [[NSNotificationCenter defaultCenter] removeObserver:self];
 | |
| }
 | |
| 
 | |
| - (void)observeNotifications
 | |
| {
 | |
|     if (![OWSCurrentAppContext isMainApp]) {
 | |
|         return;
 | |
|     }
 | |
|     [[NSNotificationCenter defaultCenter] addObserver:self
 | |
|                                              selector:@selector(applicationDidBecomeActive:)
 | |
|                                                  name:NSNotification.sessionDidBecomeActive
 | |
|                                                object:nil];
 | |
|     [[NSNotificationCenter defaultCenter] addObserver:self
 | |
|                                              selector:@selector(applicationWillResignActive:)
 | |
|                                                  name:NSNotification.sessionWillResignActive
 | |
|                                                object:nil];
 | |
| }
 | |
| 
 | |
| - (void)applicationDidBecomeActive:(UIApplication *)application
 | |
| {
 | |
|     @synchronized(self)
 | |
|     {
 | |
|         self.isAppActive = YES;
 | |
| 
 | |
|         [self ensureBackgroundTaskState];
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (void)applicationWillResignActive:(UIApplication *)application
 | |
| {
 | |
|     @synchronized(self)
 | |
|     {
 | |
|         self.isAppActive = NO;
 | |
| 
 | |
|         [self ensureBackgroundTaskState];
 | |
|     }
 | |
| }
 | |
| 
 | |
| // This method registers a new task with this manager.  We only bother
 | |
| // requesting a background task from iOS if the app is inactive (or about
 | |
| // to become inactive), so this will often not start a background task.
 | |
| //
 | |
| // Returns nil if adding this task _should have_ started a
 | |
| // background task, but the background task couldn't be begun.
 | |
| // In that case expirationBlock will not be called.
 | |
| - (nullable OWSTaskId)addTaskWithExpirationBlock:(BackgroundTaskExpirationBlock)expirationBlock
 | |
| {
 | |
|     OWSTaskId _Nullable taskId;
 | |
| 
 | |
|     @synchronized(self)
 | |
|     {
 | |
|         self.idCounter = self.idCounter + 1;
 | |
|         taskId = @(self.idCounter);
 | |
|         self.expirationMap[taskId] = expirationBlock;
 | |
| 
 | |
|         if (![self ensureBackgroundTaskState]) {
 | |
|             [self.expirationMap removeObjectForKey:taskId];
 | |
|             return nil;
 | |
|         }
 | |
| 
 | |
|         [self.continuityTimer invalidate];
 | |
|         self.continuityTimer = nil;
 | |
| 
 | |
|         return taskId;
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (void)removeTask:(OWSTaskId)taskId
 | |
| {
 | |
|     @synchronized(self)
 | |
|     {
 | |
|         [self.expirationMap removeObjectForKey:taskId];
 | |
| 
 | |
|         // This timer will ensure that we keep the background task active (if necessary)
 | |
|         // for an extra fraction of a second to provide continuity between tasks.
 | |
|         // This makes it easier and safer to use background tasks, since most code
 | |
|         // should be able to ensure background tasks by "narrowly" wrapping
 | |
|         // their core logic with a OWSBackgroundTask and not worrying about "hand off"
 | |
|         // between OWSBackgroundTasks.
 | |
|         [self.continuityTimer invalidate];
 | |
|         self.continuityTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.25f
 | |
|                                                                     target:self
 | |
|                                                                   selector:@selector(timerDidFire)
 | |
|                                                                   userInfo:nil
 | |
|                                                                    repeats:NO];
 | |
| 
 | |
|         [self ensureBackgroundTaskState];
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Begins or end a background task if necessary.
 | |
| - (BOOL)ensureBackgroundTaskState
 | |
| {
 | |
|     if (![OWSCurrentAppContext isMainApp]) {
 | |
|         // We can't create background tasks in the SAE, but pretend that we succeeded.
 | |
|         return YES;
 | |
|     }
 | |
| 
 | |
|     @synchronized(self)
 | |
|     {
 | |
|         // We only want to have a background task if we are:
 | |
|         // a) "not active" AND
 | |
|         // b1) there is one or more active instance of OWSBackgroundTask OR...
 | |
|         // b2) ...there _was_ an active instance recently.
 | |
|         BOOL shouldHaveBackgroundTask = (!self.isAppActive && (self.expirationMap.count > 0 || self.continuityTimer));
 | |
|         BOOL hasBackgroundTask = self.backgroundTaskId != UIBackgroundTaskInvalid;
 | |
| 
 | |
|         if (shouldHaveBackgroundTask == hasBackgroundTask) {
 | |
|             // Current state is correct.
 | |
|             return YES;
 | |
|         } else if (shouldHaveBackgroundTask) {
 | |
|             return [self startBackgroundTask];
 | |
|         } else {
 | |
|             // Need to end background task.
 | |
|             UIBackgroundTaskIdentifier backgroundTaskId = self.backgroundTaskId;
 | |
|             self.backgroundTaskId = UIBackgroundTaskInvalid;
 | |
|             [OWSCurrentAppContext endBackgroundTask:backgroundTaskId];
 | |
|             return YES;
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Returns NO if the background task cannot be begun.
 | |
| - (BOOL)startBackgroundTask
 | |
| {
 | |
|     @synchronized(self)
 | |
|     {
 | |
|         self.backgroundTaskId = [OWSCurrentAppContext beginBackgroundTaskWithExpirationHandler:^{
 | |
|             // Supposedly [UIApplication beginBackgroundTaskWithExpirationHandler]'s handler
 | |
|             // will always be called on the main thread, but in practice we've observed
 | |
|             // otherwise.
 | |
|             //
 | |
|             // See:
 | |
|             // https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio)
 | |
| 
 | |
|             [self backgroundTaskExpired];
 | |
|         }];
 | |
| 
 | |
|         // If the background task could not begin, return NO to indicate that.
 | |
|         if (self.backgroundTaskId == UIBackgroundTaskInvalid) {
 | |
|             return NO;
 | |
|         }
 | |
|         return YES;
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (void)backgroundTaskExpired
 | |
| {
 | |
|     UIBackgroundTaskIdentifier backgroundTaskId;
 | |
|     NSDictionary<OWSTaskId, BackgroundTaskExpirationBlock> *expirationMap;
 | |
| 
 | |
|     @synchronized(self)
 | |
|     {
 | |
|         backgroundTaskId = self.backgroundTaskId;
 | |
|         self.backgroundTaskId = UIBackgroundTaskInvalid;
 | |
| 
 | |
|         expirationMap = [self.expirationMap copy];
 | |
|         [self.expirationMap removeAllObjects];
 | |
|     }
 | |
| 
 | |
|     // Supposedly [UIApplication beginBackgroundTaskWithExpirationHandler]'s handler
 | |
|     // will always be called on the main thread, but in practice we've observed
 | |
|     // otherwise.  OWSBackgroundTask's API guarantees that completionBlock will
 | |
|     // always be called on the main thread, so we use DispatchSyncMainThreadSafe()
 | |
|     // to ensure that.  We thereby ensure that we don't end the background task
 | |
|     // until all of the completion blocks have completed.
 | |
|     [Threading dispatchSyncMainThreadSafe:^{
 | |
|         for (BackgroundTaskExpirationBlock expirationBlock in expirationMap.allValues) {
 | |
|             expirationBlock();
 | |
|         }
 | |
|         if (backgroundTaskId != UIBackgroundTaskInvalid) {
 | |
|             // Apparently we need to "end" even expired background tasks.
 | |
|             [OWSCurrentAppContext endBackgroundTask:backgroundTaskId];
 | |
|         }
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (void)timerDidFire
 | |
| {
 | |
|     @synchronized(self)
 | |
|     {
 | |
|         [self.continuityTimer invalidate];
 | |
|         self.continuityTimer = nil;
 | |
| 
 | |
|         [self ensureBackgroundTaskState];
 | |
|     }
 | |
| }
 | |
| 
 | |
| @end
 | |
| 
 | |
| #pragma mark -
 | |
| 
 | |
| @interface OWSBackgroundTask ()
 | |
| 
 | |
| @property (nonatomic, readonly) NSString *label;
 | |
| 
 | |
| // This property should only be accessed while synchronized on this instance.
 | |
| @property (nonatomic, nullable) OWSTaskId taskId;
 | |
| 
 | |
| // This property should only be accessed while synchronized on this instance.
 | |
| @property (nonatomic, nullable) BackgroundTaskCompletionBlock completionBlock;
 | |
| 
 | |
| @end
 | |
| 
 | |
| #pragma mark -
 | |
| 
 | |
| @implementation OWSBackgroundTask
 | |
| 
 | |
| + (OWSBackgroundTask *)backgroundTaskWithLabelStr:(const char *)labelStr
 | |
| {
 | |
|     NSString *label = [NSString stringWithFormat:@"%s", labelStr];
 | |
|     return [[OWSBackgroundTask alloc] initWithLabel:label completionBlock:nil];
 | |
| }
 | |
| 
 | |
| + (OWSBackgroundTask *)backgroundTaskWithLabelStr:(const char *)labelStr
 | |
|                                   completionBlock:(BackgroundTaskCompletionBlock)completionBlock
 | |
| {
 | |
| 
 | |
|     NSString *label = [NSString stringWithFormat:@"%s", labelStr];
 | |
|     return [[OWSBackgroundTask alloc] initWithLabel:label completionBlock:completionBlock];
 | |
| }
 | |
| 
 | |
| + (OWSBackgroundTask *)backgroundTaskWithLabel:(NSString *)label
 | |
| {
 | |
|     return [[OWSBackgroundTask alloc] initWithLabel:label completionBlock:nil];
 | |
| }
 | |
| 
 | |
| + (OWSBackgroundTask *)backgroundTaskWithLabel:(NSString *)label
 | |
|                                completionBlock:(BackgroundTaskCompletionBlock)completionBlock
 | |
| {
 | |
|     return [[OWSBackgroundTask alloc] initWithLabel:label completionBlock:completionBlock];
 | |
| }
 | |
| 
 | |
| - (instancetype)initWithLabel:(NSString *)label completionBlock:(BackgroundTaskCompletionBlock _Nullable)completionBlock
 | |
| {
 | |
|     self = [super init];
 | |
| 
 | |
|     if (!self) {
 | |
|         return self;
 | |
|     }
 | |
| 
 | |
|     _label = label;
 | |
|     self.completionBlock = completionBlock;
 | |
| 
 | |
|     [self startBackgroundTask];
 | |
| 
 | |
|     return self;
 | |
| }
 | |
| 
 | |
| - (void)dealloc
 | |
| {
 | |
|     [self endBackgroundTask];
 | |
| }
 | |
| 
 | |
| - (void)startBackgroundTask
 | |
| {
 | |
|     __weak typeof(self) weakSelf = self;
 | |
|     self.taskId = [OWSBackgroundTaskManager.sharedManager addTaskWithExpirationBlock:^{
 | |
|         [Threading dispatchMainThreadSafe:^{
 | |
|             OWSBackgroundTask *strongSelf = weakSelf;
 | |
|             if (!strongSelf) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             // Make a local copy of completionBlock to ensure that it is called
 | |
|             // exactly once.
 | |
|             BackgroundTaskCompletionBlock _Nullable completionBlock = nil;
 | |
| 
 | |
|             @synchronized(strongSelf)
 | |
|             {
 | |
|                 if (!strongSelf.taskId) {
 | |
|                     return;
 | |
|                 }
 | |
|                 strongSelf.taskId = nil;
 | |
| 
 | |
|                 completionBlock = strongSelf.completionBlock;
 | |
|                 strongSelf.completionBlock = nil;
 | |
|             }
 | |
| 
 | |
|             if (completionBlock) {
 | |
|                 completionBlock(BackgroundTaskState_Expired);
 | |
|             }
 | |
|         }];
 | |
|     }];
 | |
| 
 | |
|     // If a background task could not be begun, call the completion block.
 | |
|     if (!self.taskId) {
 | |
| 
 | |
|         // Make a local copy of completionBlock to ensure that it is called
 | |
|         // exactly once.
 | |
|         BackgroundTaskCompletionBlock _Nullable completionBlock;
 | |
|         @synchronized(self)
 | |
|         {
 | |
|             completionBlock = self.completionBlock;
 | |
|             self.completionBlock = nil;
 | |
|         }
 | |
|         if (completionBlock) {
 | |
|             [Threading dispatchMainThreadSafe:^{
 | |
|                 completionBlock(BackgroundTaskState_CouldNotStart);
 | |
|             }];
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (void)cancel
 | |
| {
 | |
|     // Make a local copy of this state, since this method is called by `dealloc`.
 | |
|     BackgroundTaskCompletionBlock _Nullable completionBlock;
 | |
| 
 | |
|     @synchronized(self)
 | |
|     {
 | |
|         if (!self.taskId) {
 | |
|             return;
 | |
|         }
 | |
|         [OWSBackgroundTaskManager.sharedManager removeTask:self.taskId];
 | |
|         self.taskId = nil;
 | |
| 
 | |
|         completionBlock = self.completionBlock;
 | |
|         self.completionBlock = nil;
 | |
|     }
 | |
| 
 | |
|     // endBackgroundTask must be called on the main thread.
 | |
|     [Threading dispatchMainThreadSafe:^{
 | |
|         if (completionBlock) {
 | |
|             completionBlock(BackgroundTaskState_Cancelled);
 | |
|         }
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (void)endBackgroundTask
 | |
| {
 | |
|     // Make a local copy of this state, since this method is called by `dealloc`.
 | |
|     BackgroundTaskCompletionBlock _Nullable completionBlock;
 | |
| 
 | |
|     @synchronized(self)
 | |
|     {
 | |
|         if (!self.taskId) {
 | |
|             return;
 | |
|         }
 | |
|         [OWSBackgroundTaskManager.sharedManager removeTask:self.taskId];
 | |
|         self.taskId = nil;
 | |
| 
 | |
|         completionBlock = self.completionBlock;
 | |
|         self.completionBlock = nil;
 | |
|     }
 | |
| 
 | |
|     // endBackgroundTask must be called on the main thread.
 | |
|     [Threading dispatchMainThreadSafe:^{
 | |
|         if (completionBlock) {
 | |
|             completionBlock(BackgroundTaskState_Success);
 | |
|         }
 | |
|     }];
 | |
| }
 | |
| 
 | |
| @end
 | |
| 
 | |
| NS_ASSUME_NONNULL_END
 |