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
 |