diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 6320e8dec..4efe59ca8 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -557,7 +557,7 @@ protocol CallServiceObserver: class { self.call = newCall - var backgroundTask = OWSBackgroundTask(label:"\(#function)", completionBlock: { [weak self] status in + var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "\(#function)", completionBlock: { [weak self] status in AssertIsOnMainThread() guard status == .expired else { @@ -1414,7 +1414,7 @@ protocol CallServiceObserver: class { public func handleFailedCall(failedCall: SignalCall?, error: CallError) { AssertIsOnMainThread() - if case CallError.assertionError(description:let description) = error { + if case CallError.assertionError(description: let description) = error { owsFail(description) } diff --git a/SignalMessaging/environment/AppSetup.m b/SignalMessaging/environment/AppSetup.m index 0dfa5da28..48f663d83 100644 --- a/SignalMessaging/environment/AppSetup.m +++ b/SignalMessaging/environment/AppSetup.m @@ -10,6 +10,7 @@ #import #import #import +#import #import #import @@ -25,6 +26,9 @@ NS_ASSUME_NONNULL_BEGIN static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ + // Order matters here. + [[OWSBackgroundTaskManager sharedManager] observeNotifications]; + [Environment setCurrent:[Release releaseEnvironment]]; id callMessageHandler = callMessageHandlerBlock(); diff --git a/SignalMessaging/environment/Release.m b/SignalMessaging/environment/Release.m index 671130afc..6b8c9b630 100644 --- a/SignalMessaging/environment/Release.m +++ b/SignalMessaging/environment/Release.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "Release.h" diff --git a/SignalServiceKit/src/Util/OWSBackgroundTask.h b/SignalServiceKit/src/Util/OWSBackgroundTask.h index 7b09ade5f..2f9bb0be9 100644 --- a/SignalServiceKit/src/Util/OWSBackgroundTask.h +++ b/SignalServiceKit/src/Util/OWSBackgroundTask.h @@ -1,7 +1,9 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // +NS_ASSUME_NONNULL_BEGIN + typedef NS_ENUM(NSUInteger, BackgroundTaskState) { BackgroundTaskState_Success, BackgroundTaskState_CouldNotStart, @@ -10,6 +12,19 @@ typedef NS_ENUM(NSUInteger, BackgroundTaskState) { typedef void (^BackgroundTaskCompletionBlock)(BackgroundTaskState backgroundTaskState); +// This class can be safely accessed and used from any thread. +@interface OWSBackgroundTaskManager : NSObject + +- (instancetype)init NS_UNAVAILABLE; + ++ (instancetype)sharedManager; + +- (void)observeNotifications; + +@end + +#pragma mark - + // This class makes it easier and safer to use background tasks. // // * Uses RAII (Resource Acquisition Is Initialization) pattern. @@ -41,3 +56,5 @@ typedef void (^BackgroundTaskCompletionBlock)(BackgroundTaskState backgroundTask completionBlock:(BackgroundTaskCompletionBlock)completionBlock; @end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/OWSBackgroundTask.m b/SignalServiceKit/src/Util/OWSBackgroundTask.m index f571aec31..d1829d88a 100644 --- a/SignalServiceKit/src/Util/OWSBackgroundTask.m +++ b/SignalServiceKit/src/Util/OWSBackgroundTask.m @@ -4,14 +4,294 @@ #import "OWSBackgroundTask.h" #import "AppContext.h" +#import "NSTimer+OWS.h" #import "Threading.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 *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 +{ + OWSAssertIsOnMainThread(); + + self = [super init]; + + if (!self) { + return self; + } + + self.backgroundTaskId = UIBackgroundTaskInvalid; + self.expirationMap = [NSMutableDictionary new]; + self.idCounter = 0; + self.isAppActive = CurrentAppContext().isMainAppAndActive; + + OWSSingletonAssert(); + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)observeNotifications +{ + if (!CurrentAppContext().isMainApp) { + return; + } + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidBecomeActive:) + name:OWSApplicationDidBecomeActiveNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillResignActive:) + name:OWSApplicationWillResignActiveNotification + object:nil]; +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + OWSAssertIsOnMainThread(); + + @synchronized(self) + { + self.isAppActive = YES; + + [self ensureBackgroundTaskState]; + } +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + OWSAssertIsOnMainThread(); + + @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 +{ + OWSAssert(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 +{ + OWSAssert(taskId); + + @synchronized(self) + { + OWSAssert(self.expirationMap[taskId] != nil); + + [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 (!CurrentAppContext().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) { + DDLogInfo(@"%@ Starting background task.", self.logTag); + return [self startBackgroundTask]; + } else { + // Need to end background task. + DDLogInfo(@"%@ Ending background task.", self.logTag); + UIBackgroundTaskIdentifier backgroundTaskId = self.backgroundTaskId; + self.backgroundTaskId = UIBackgroundTaskInvalid; + [CurrentAppContext() endBackgroundTask:backgroundTaskId]; + return YES; + } + } +} + +// Returns NO if the background task cannot be begun. +- (BOOL)startBackgroundTask +{ + OWSAssert(CurrentAppContext().isMainApp); + + @synchronized(self) + { + OWSAssert(self.backgroundTaskId == UIBackgroundTaskInvalid); + + self.backgroundTaskId = [CurrentAppContext() 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) + OWSAssert([NSThread isMainThread]); + + [self backgroundTaskExpired]; + }]; + + // If the background task could not begin, return NO to indicate that. + if (self.backgroundTaskId == UIBackgroundTaskInvalid) { + DDLogError(@"%@ background task could not be started.", self.logTag); + + return NO; + } + return YES; + } +} + +- (void)backgroundTaskExpired +{ + UIBackgroundTaskIdentifier backgroundTaskId; + NSDictionary *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. + DispatchSyncMainThreadSafe(^{ + for (BackgroundTaskExpirationBlock expirationBlock in expirationMap) { + expirationBlock(); + } + if (backgroundTaskId != UIBackgroundTaskInvalid) { + // Apparently we need to "end" even expired background tasks. + [CurrentAppContext() 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) UIBackgroundTaskIdentifier backgroundTaskId; +@property (nonatomic, nullable) OWSTaskId taskId; // This property should only be accessed while synchronized on this instance. @property (nonatomic, nullable) BackgroundTaskCompletionBlock completionBlock; @@ -76,24 +356,14 @@ - (void)startBackgroundTask { - // beginBackgroundTaskWithExpirationHandler must be called on the main thread. __weak typeof(self) weakSelf = self; - self.backgroundTaskId = [CurrentAppContext() beginBackgroundTaskWithExpirationHandler:^{ - // Supposedly [UIApplication beginBackgroundTaskWithExpirationHandler]'s handler - // will always be called on the main thread, but in practice we've observed - // otherwise. We use DispatchSyncMainThreadSafe() (note the sync) to ensure that - // this work is done on the main thread. - // - // See: https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio) - // - // Note the usage of OWSCAssert() to avoid capturing a reference to self. - OWSCAssert([NSThread isMainThread]); - - DispatchSyncMainThreadSafe(^{ + self.taskId = [OWSBackgroundTaskManager.sharedManager addTaskWithExpirationBlock:^{ + DispatchMainThreadSafe(^{ OWSBackgroundTask *strongSelf = weakSelf; if (!strongSelf) { return; } + DDLogVerbose(@"%@ task expired", strongSelf.logTag); // Make a local copy of completionBlock to ensure that it is called // exactly once. @@ -101,11 +371,11 @@ @synchronized(strongSelf) { - if (strongSelf.backgroundTaskId == UIBackgroundTaskInvalid) { + if (!strongSelf.taskId) { return; } DDLogInfo(@"%@ %@ background task expired.", strongSelf.logTag, strongSelf.label); - strongSelf.backgroundTaskId = UIBackgroundTaskInvalid; + strongSelf.taskId = nil; completionBlock = strongSelf.completionBlock; strongSelf.completionBlock = nil; @@ -118,9 +388,8 @@ }]; // If a background task could not be begun, call the completion block. - if (self.backgroundTaskId == UIBackgroundTaskInvalid) { - - DDLogInfo(@"%@ %@ background task could not be started.", self.logTag, self.label); + if (!self.taskId) { + DDLogError(@"%@ %@ background task could not be started.", self.logTag, self.label); // Make a local copy of completionBlock to ensure that it is called // exactly once. @@ -131,7 +400,9 @@ self.completionBlock = nil; } if (completionBlock) { - completionBlock(BackgroundTaskState_CouldNotStart); + DispatchMainThreadSafe(^{ + completionBlock(BackgroundTaskState_CouldNotStart); + }); } } } @@ -139,32 +410,28 @@ - (void)endBackgroundTask { // Make a local copy of this state, since this method is called by `dealloc`. - UIBackgroundTaskIdentifier backgroundTaskId; BackgroundTaskCompletionBlock _Nullable completionBlock; @synchronized(self) { - backgroundTaskId = self.backgroundTaskId; + if (!self.taskId) { + return; + } + [OWSBackgroundTaskManager.sharedManager removeTask:self.taskId]; + self.taskId = nil; + completionBlock = self.completionBlock; self.completionBlock = nil; } - if (backgroundTaskId == UIBackgroundTaskInvalid) { - OWSAssert(!completionBlock); - return; - } - // endBackgroundTask must be called on the main thread. DispatchMainThreadSafe(^{ - if (completionBlock) { completionBlock(BackgroundTaskState_Success); } - - if (backgroundTaskId != UIBackgroundTaskInvalid) { - [CurrentAppContext() endBackgroundTask:backgroundTaskId]; - } }); } @end + +NS_ASSUME_NONNULL_END