diff --git a/Pods b/Pods index d5ef8e9a1a..e680d157ee 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit d5ef8e9a1a7dce57232dd534af17eddbca938427 +Subproject commit e680d157ee32f4079cc2e126068773cd8280f118 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index f406974082..247f5be564 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 34480B641FD0A98800BC14EF /* UIView+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B601FD0A98800BC14EF /* UIView+OWS.m */; }; 34480B671FD0AA9400BC14EF /* UIFont+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B651FD0AA9400BC14EF /* UIFont+OWS.m */; }; 34480B681FD0AA9400BC14EF /* UIFont+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = 34480B661FD0AA9400BC14EF /* UIFont+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */ = {isa = PBXBuildFile; fileRef = 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */; }; 344D6CEA20069E070042AF96 /* SelectRecipientViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 344D6CE620069E060042AF96 /* SelectRecipientViewController.h */; }; 344D6CEB20069E070042AF96 /* SelectRecipientViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 344D6CE720069E060042AF96 /* SelectRecipientViewController.m */; }; 344D6CEC20069E070042AF96 /* NewNonContactConversationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 344D6CE820069E070042AF96 /* NewNonContactConversationViewController.m */; }; @@ -712,6 +713,8 @@ 34480B601FD0A98800BC14EF /* UIView+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+OWS.m"; sourceTree = ""; }; 34480B651FD0AA9400BC14EF /* UIFont+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIFont+OWS.m"; sourceTree = ""; }; 34480B661FD0AA9400BC14EF /* UIFont+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIFont+OWS.h"; sourceTree = ""; }; + 344825C4211390C700DB4BD8 /* OWSOrphanDataCleaner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOrphanDataCleaner.h; sourceTree = ""; }; + 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOrphanDataCleaner.m; sourceTree = ""; }; 34491FC11FB0F78500B3E5A3 /* my */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = my; path = translations/my.lproj/Localizable.strings; sourceTree = ""; }; 344D6CE620069E060042AF96 /* SelectRecipientViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SelectRecipientViewController.h; path = SignalMessaging/contacts/SelectRecipientViewController.h; sourceTree = SOURCE_ROOT; }; 344D6CE720069E060042AF96 /* SelectRecipientViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SelectRecipientViewController.m; path = SignalMessaging/contacts/SelectRecipientViewController.m; sourceTree = SOURCE_ROOT; }; @@ -2203,6 +2206,8 @@ 340FC8CB20518C76007AEB0F /* OWSBackupJob.h */, 340FC8CC20518C76007AEB0F /* OWSBackupJob.m */, 34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */, + 344825C4211390C700DB4BD8 /* OWSOrphanDataCleaner.h */, + 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */, 34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */, 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */, 4579431C1E7C8CE9008ED0C0 /* Pastelog.h */, @@ -3357,6 +3362,7 @@ 34D1F0881F8678AA0066283D /* ConversationViewLayout.m in Sources */, 452314A01F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift in Sources */, 34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */, + 344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */, 45D2AC02204885170033C692 /* OWS2FAReminderViewController.swift in Sources */, 4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */, 34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */, diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 08de4d05c6..087db1f241 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -12,6 +12,7 @@ #import "NotificationsManager.h" #import "OWS2FASettingsViewController.h" #import "OWSBackup.h" +#import "OWSOrphanDataCleaner.h" #import "OWSScreenLockUI.h" #import "Pastelog.h" #import "PushManager.h" @@ -43,7 +44,6 @@ #import #import #import -#import #import #import #import @@ -149,7 +149,7 @@ static NSTimeInterval launchStartedAt; return YES; } - [AppVersion instance]; + [AppVersion sharedInstance]; [self startupLogging]; @@ -314,6 +314,7 @@ static NSTimeInterval launchStartedAt; return NO; } + OWSAssert(backgroundTask); backgroundTask = nil; return YES; @@ -325,7 +326,7 @@ static NSTimeInterval launchStartedAt; self.didAppLaunchFail = YES; // We perform a subset of the [application:didFinishLaunchingWithOptions:]. - [AppVersion instance]; + [AppVersion sharedInstance]; [self startupLogging]; self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; @@ -1061,7 +1062,7 @@ static NSTimeInterval launchStartedAt; [DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self]; - [AppVersion.instance mainAppLaunchDidComplete]; + [AppVersion.sharedInstance mainAppLaunchDidComplete]; [Environment.current.contactsManager loadSignalAccountsFromCache]; [Environment.current.contactsManager startObserving]; @@ -1090,7 +1091,7 @@ static NSTimeInterval launchStartedAt; // TODO: Orphan cleanup is somewhat expensive - not least in doing a bunch // of disk access. We might want to only run it "once per version" // or something like that in production. - [OWSOrphanedDataCleaner auditAndCleanupAsync:nil]; + [OWSOrphanDataCleaner auditOnLaunchIfNecessary]; #endif [OWSProfileManager.sharedManager fetchLocalUsersProfile]; diff --git a/Signal/src/Jobs/SyncPushTokensJob.swift b/Signal/src/Jobs/SyncPushTokensJob.swift index d1d396beef..1546e39c99 100644 --- a/Signal/src/Jobs/SyncPushTokensJob.swift +++ b/Signal/src/Jobs/SyncPushTokensJob.swift @@ -53,7 +53,7 @@ class SyncPushTokensJob: NSObject { shouldUploadTokens = true } - if AppVersion.instance().lastAppVersion != AppVersion.instance().currentAppVersion { + if AppVersion.sharedInstance().lastAppVersion != AppVersion.sharedInstance().currentAppVersion { Logger.info("\(self.TAG) Uploading due to fresh install or app upgrade.") shouldUploadTokens = true } @@ -79,7 +79,7 @@ class SyncPushTokensJob: NSObject { return runPromise } - // MARK - objc wrappers, since objc can't use swift parameterized types + // MARK: - objc wrappers, since objc can't use swift parameterized types @objc class func run(accountManager: AccountManager, preferences: OWSPreferences) -> AnyPromise { let promise: Promise = self.run(accountManager: accountManager, preferences: preferences) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIDiskUsage.m b/Signal/src/ViewControllers/DebugUI/DebugUIDiskUsage.m index 082db9d8e8..90c973b223 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIDiskUsage.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIDiskUsage.m @@ -3,10 +3,10 @@ // #import "DebugUIDiskUsage.h" +#import "OWSOrphanDataCleaner.h" #import "OWSTableViewController.h" #import "Signal-Swift.h" #import -#import #import #import #import @@ -28,11 +28,11 @@ NS_ASSUME_NONNULL_BEGIN items:@[ [OWSTableItem itemWithTitle:@"Audit & Log" actionBlock:^{ - [OWSOrphanedDataCleaner auditAsync]; + [OWSOrphanDataCleaner auditAndCleanup:NO]; }], [OWSTableItem itemWithTitle:@"Audit & Clean Up" actionBlock:^{ - [OWSOrphanedDataCleaner auditAndCleanupAsync:nil]; + [OWSOrphanDataCleaner auditAndCleanup:YES]; }], [OWSTableItem itemWithTitle:@"Save All Attachments" actionBlock:^{ diff --git a/Signal/src/util/MainAppContext.m b/Signal/src/util/MainAppContext.m index dffe0aa766..03d0e61bb0 100644 --- a/Signal/src/util/MainAppContext.m +++ b/Signal/src/util/MainAppContext.m @@ -8,6 +8,7 @@ #import #import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -15,6 +16,8 @@ NS_ASSUME_NONNULL_BEGIN @property (atomic) UIApplicationState reportedApplicationState; +@property (nonatomic, nullable) NSMutableArray *appActiveBlocks; + @end #pragma mark - @@ -54,6 +57,10 @@ NS_ASSUME_NONNULL_BEGIN name:UIApplicationWillTerminateNotification object:nil]; + // We can't use OWSSingletonAssert() since it uses the app context. + + self.appActiveBlocks = [NSMutableArray new]; + return self; } @@ -108,6 +115,8 @@ NS_ASSUME_NONNULL_BEGIN DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationDidBecomeActiveNotification object:nil]; + + [self runAppActiveBlocks]; } - (void)applicationWillTerminate:(NSNotification *)notification @@ -228,6 +237,49 @@ NS_ASSUME_NONNULL_BEGIN [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:value]; } +#pragma mark - + +- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block +{ + OWSAssert(block); + + DispatchMainThreadSafe(^{ + if (self.isMainAppAndActive) { + // App active blocks typically will be used to safely access the + // shared data container, so use a background task to protect this + // work. + OWSBackgroundTask *_Nullable backgroundTask = + [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + block(); + OWSAssert(backgroundTask); + backgroundTask = nil; + return; + } + + [self.appActiveBlocks addObject:block]; + }); +} + +- (void)runAppActiveBlocks +{ + OWSAssertIsOnMainThread(); + OWSAssert(self.isMainAppAndActive); + + // App active blocks typically will be used to safely access the + // shared data container, so use a background task to protect this + // work. + OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + + NSArray *appActiveBlocks = [self.appActiveBlocks copy]; + [self.appActiveBlocks removeAllObjects]; + for (AppActiveBlock block in appActiveBlocks) { + block(); + } + + OWSAssert(backgroundTask); + backgroundTask = nil; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/OWSOrphanDataCleaner.h b/Signal/src/util/OWSOrphanDataCleaner.h new file mode 100644 index 0000000000..55debda6e6 --- /dev/null +++ b/Signal/src/util/OWSOrphanDataCleaner.h @@ -0,0 +1,21 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +// Notes: +// +// * On disk, we only bother cleaning up files, not directories. +@interface OWSOrphanDataCleaner : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +// This is exposed for the debug UI. ++ (void)auditAndCleanup:(BOOL)shouldCleanup; + ++ (void)auditOnLaunchIfNecessary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/OWSOrphanDataCleaner.m b/Signal/src/util/OWSOrphanDataCleaner.m new file mode 100644 index 0000000000..b2e58e8e5b --- /dev/null +++ b/Signal/src/util/OWSOrphanDataCleaner.m @@ -0,0 +1,705 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSOrphanDataCleaner.h" +#import "DateUtil.h" +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSOrphanDataCleaner_Collection = @"OWSOrphanDataCleaner_Collection"; +NSString *const OWSOrphanDataCleaner_LastCleaningVersionKey = @"OWSOrphanDataCleaner_LastCleaningVersionKey"; +NSString *const OWSOrphanDataCleaner_LastCleaningDateKey = @"OWSOrphanDataCleaner_LastCleaningDateKey"; + +@interface OWSOrphanData : NSObject + +@property (nonatomic) NSSet *interactionIds; +@property (nonatomic) NSSet *attachmentIds; +@property (nonatomic) NSSet *filePaths; + +@end + +#pragma mark - + +@implementation OWSOrphanData + +@end + +#pragma mark - + +typedef void (^OrphanDataBlock)(OWSOrphanData *); + +@implementation OWSOrphanDataCleaner + +// Unlike CurrentAppContext().isMainAppAndActive, this method can be safely +// invoked off the main thread. ++ (BOOL)isMainAppAndActive +{ + return CurrentAppContext().reportedApplicationState == UIApplicationStateActive; +} + ++ (void)printPaths:(NSArray *)paths label:(NSString *)label +{ + for (NSString *path in [paths sortedArrayUsingSelector:@selector(compare:)]) { + DDLogDebug(@"%@ %@: %@", self.logTag, label, path); + } +} + ++ (long long)fileSizeOfFilePath:(NSString *)filePath +{ + NSError *error; + NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error][NSFileSize]; + if (error) { + if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == 260) { + DDLogWarn(@"%@ can't find size of missing file: %@", self.logTag, filePath); + } else { + OWSProdLogAndFail(@"%@ attributesOfItemAtPath: %@ error: %@", self.logTag, filePath, error); + } + return 0; + } + return fileSize.longLongValue; +} + ++ (nullable NSNumber *)fileSizeOfFilePathsSafe:(NSArray *)filePaths +{ + long long result = 0; + for (NSString *filePath in filePaths) { + if (!self.isMainAppAndActive) { + return nil; + } + result += [self fileSizeOfFilePath:filePath]; + } + return @(result); +} + ++ (nullable NSSet *)filePathsInDirectorySafe:(NSString *)dirPath +{ + NSMutableSet *filePaths = [NSMutableSet new]; + if (![[NSFileManager defaultManager] fileExistsAtPath:dirPath]) { + return filePaths; + } + NSError *error; + NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error]; + if (error) { + OWSProdLogAndFail(@"%@ contentsOfDirectoryAtPath error: %@", self.logTag, error); + return [NSSet new]; + } + for (NSString *fileName in fileNames) { + if (!self.isMainAppAndActive) { + return nil; + } + NSString *filePath = [dirPath stringByAppendingPathComponent:fileName]; + BOOL isDirectory; + [[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]; + if (isDirectory) { + NSSet *_Nullable dirPaths = [self filePathsInDirectorySafe:filePath]; + if (!dirPaths) { + return nil; + } + [filePaths unionSet:dirPaths]; + } else { + [filePaths addObject:filePath]; + } + } + return filePaths; +} + +// This method finds (but does not delete): +// +// * Orphan TSInteractions (with no thread). +// * Orphan TSAttachments (with no message). +// * Orphan attachment files (with no corresponding TSAttachment). +// * Orphan profile avatars. +// * Temporary files (all). +// +// It also finds (we don't clean these up). +// +// * Missing attachment files (cannot be cleaned up). +// These are attachments which have no file on disk. They should be extremely rare - +// the only cases I have seen are probably due to debugging. +// They can't be cleaned up - we don't want to delete the TSAttachmentStream or +// its corresponding message. Better that the broken message shows up in the +// conversation view. ++ (void)findOrphanDataWithRetries:(NSInteger)remainingRetries + databaseConnection:(YapDatabaseConnection *)databaseConnection + success:(OrphanDataBlock)success + failure:(dispatch_block_t)failure +{ + OWSAssert(databaseConnection); + + if (remainingRetries < 1) { + DDLogInfo(@"%@ Aborting orphan data search.", self.logTag); + dispatch_async(self.workQueue, ^{ + failure(); + }); + return; + } + + // Wait until the app is active... + [CurrentAppContext() runNowOrWhenMainAppIsActive:^{ + // ...but perform the work off the main thread. + dispatch_async(self.workQueue, ^{ + OWSOrphanData *_Nullable orphanData = [self findOrphanDataSync:databaseConnection]; + if (orphanData) { + success(orphanData); + } else { + [self findOrphanDataWithRetries:remainingRetries - 1 + databaseConnection:databaseConnection + success:success + failure:failure]; + } + }); + }]; +} + +// Returns nil on failure, usually indicating that the search +// aborted due to the app resigning active. This method is extremely careful to +// abort if the app resigns active, in order to avoid 0xdead10cc crashes. ++ (nullable OWSOrphanData *)findOrphanDataSync:(YapDatabaseConnection *)databaseConnection +{ + OWSAssert(databaseConnection); + + __block BOOL shouldAbort = NO; + + // LOG_ALL_FILE_PATHS can be used to determine if there are other kinds of files + // that we're not cleaning up. +//#define LOG_ALL_FILE_PATHS +#ifdef LOG_ALL_FILE_PATHS + { + NSString *documentDirPath = [OWSFileSystem appDocumentDirectoryPath]; + NSArray *_Nullable allDocumentFilePaths = + [self filePathsInDirectorySafe:documentDirPath].allObjects; + allDocumentFilePaths = [allDocumentFilePaths sortedArrayUsingSelector:@selector(compare:)]; + NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder]; + for (NSString *filePath in allDocumentFilePaths) { + if ([filePath hasPrefix:attachmentsFolder]) { + continue; + } + DDLogVerbose(@"%@ non-attachment file: %@", self.logTag, filePath); + } + } + { + NSString *documentDirPath = [OWSFileSystem appSharedDataDirectoryPath]; + NSArray *_Nullable allDocumentFilePaths = + [self filePathsInDirectorySafe:documentDirPath].allObjects; + allDocumentFilePaths = [allDocumentFilePaths sortedArrayUsingSelector:@selector(compare:)]; + NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder]; + for (NSString *filePath in allDocumentFilePaths) { + if ([filePath hasPrefix:attachmentsFolder]) { + continue; + } + DDLogVerbose(@"%@ non-attachment file: %@", self.logTag, filePath); + } + } +#endif + + // We treat _all_ temp files as orphan files. This is safe + // because temp files only need to be retained for the + // a single launch of the app. Since our "date threshold" + // for deletion is relative to the current launch time, + // all temp files currently in use should be safe. + NSString *temporaryDirectory = NSTemporaryDirectory(); + NSArray *_Nullable tempFilePaths = [self filePathsInDirectorySafe:temporaryDirectory].allObjects; + if (!tempFilePaths || !self.isMainAppAndActive) { + return nil; + } + +#ifdef LOG_ALL_FILE_PATHS + { + NSDateFormatter *dateFormatter = [NSDateFormatter new]; + [dateFormatter setDateStyle:NSDateFormatterLongStyle]; + [dateFormatter setTimeStyle:NSDateFormatterLongStyle]; + + tempFilePaths = [tempFilePaths sortedArrayUsingSelector:@selector(compare:)]; + for (NSString *filePath in tempFilePaths) { + NSError *error; + NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error]; + if (!attributes || error) { + OWSProdLogAndFail(@"%@ Could not get attributes of file at: %@", self.logTag, filePath); + continue; + } + DDLogVerbose(@"%@ temp file: %@, %@", + self.logTag, + filePath, + [dateFormatter stringFromDate:attributes.fileModificationDate]); + } + } +#endif + + NSString *legacyAttachmentsDirPath = TSAttachmentStream.legacyAttachmentsDirPath; + NSString *sharedDataAttachmentsDirPath = TSAttachmentStream.sharedDataAttachmentsDirPath; + NSSet *_Nullable legacyAttachmentFilePaths = [self filePathsInDirectorySafe:legacyAttachmentsDirPath]; + if (!legacyAttachmentFilePaths || !self.isMainAppAndActive) { + return nil; + } + NSSet *_Nullable sharedDataAttachmentFilePaths = + [self filePathsInDirectorySafe:sharedDataAttachmentsDirPath]; + if (!sharedDataAttachmentFilePaths || !self.isMainAppAndActive) { + return nil; + } + + NSString *legacyProfileAvatarsDirPath = OWSUserProfile.legacyProfileAvatarsDirPath; + NSString *sharedDataProfileAvatarsDirPath = OWSUserProfile.sharedDataProfileAvatarsDirPath; + NSSet *_Nullable legacyProfileAvatarsFilePaths = + [self filePathsInDirectorySafe:legacyProfileAvatarsDirPath]; + if (!legacyProfileAvatarsFilePaths || !self.isMainAppAndActive) { + return nil; + } + NSSet *_Nullable sharedDataProfileAvatarFilePaths = + [self filePathsInDirectorySafe:sharedDataProfileAvatarsDirPath]; + if (!sharedDataProfileAvatarFilePaths || !self.isMainAppAndActive) { + return nil; + } + + NSMutableSet *allOnDiskFilePaths = [NSMutableSet new]; + [allOnDiskFilePaths unionSet:legacyAttachmentFilePaths]; + [allOnDiskFilePaths unionSet:sharedDataAttachmentFilePaths]; + [allOnDiskFilePaths unionSet:legacyProfileAvatarsFilePaths]; + [allOnDiskFilePaths unionSet:sharedDataProfileAvatarFilePaths]; + [allOnDiskFilePaths addObjectsFromArray:tempFilePaths]; + + NSSet *profileAvatarFilePaths = [OWSUserProfile allProfileAvatarFilePaths]; + + if (!self.isMainAppAndActive) { + return nil; + } + + NSNumber *_Nullable totalFileSize = [self fileSizeOfFilePathsSafe:allOnDiskFilePaths.allObjects]; + + if (!totalFileSize || !self.isMainAppAndActive) { + return nil; + } + + NSUInteger fileCount = allOnDiskFilePaths.count; + + // Attachments + __block int attachmentStreamCount = 0; + NSMutableSet *allAttachmentFilePaths = [NSMutableSet new]; + NSMutableSet *allAttachmentIds = [NSMutableSet new]; + // Threads + __block NSSet *threadIds; + // Messages + NSMutableSet *orphanInteractionIds = [NSMutableSet new]; + NSMutableSet *messageAttachmentIds = [NSMutableSet new]; + NSMutableSet *quotedReplyThumbnailAttachmentIds = [NSMutableSet new]; + NSMutableSet *contactShareAvatarAttachmentIds = [NSMutableSet new]; + [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + [transaction + enumerateKeysAndObjectsInCollection:TSAttachmentStream.collection + usingBlock:^(NSString *key, TSAttachment *attachment, BOOL *stop) { + if (!self.isMainAppAndActive) { + shouldAbort = YES; + *stop = YES; + return; + } + if (![attachment isKindOfClass:[TSAttachmentStream class]]) { + return; + } + [allAttachmentIds addObject:attachment.uniqueId]; + + TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; + attachmentStreamCount++; + NSString *_Nullable filePath = [attachmentStream filePath]; + if (filePath) { + [allAttachmentFilePaths addObject:filePath]; + } else { + OWSProdLogAndFail(@"%@ attachment has no file path.", self.logTag); + } + + NSString *_Nullable thumbnailPath = [attachmentStream thumbnailPath]; + if (thumbnailPath.length > 0) { + [allAttachmentFilePaths addObject:thumbnailPath]; + } + }]; + + if (shouldAbort) { + return; + } + + threadIds = [NSSet setWithArray:[transaction allKeysInCollection:TSThread.collection]]; + + [transaction + enumerateKeysAndObjectsInCollection:TSMessage.collection + usingBlock:^(NSString *key, TSInteraction *interaction, BOOL *stop) { + if (!self.isMainAppAndActive) { + shouldAbort = YES; + *stop = YES; + return; + } + if (interaction.uniqueThreadId.length < 1 + || ![threadIds containsObject:interaction.uniqueThreadId]) { + [orphanInteractionIds addObject:interaction.uniqueId]; + } + + if (![interaction isKindOfClass:[TSMessage class]]) { + return; + } + + TSMessage *message = (TSMessage *)interaction; + if (message.attachmentIds.count > 0) { + [messageAttachmentIds addObjectsFromArray:message.attachmentIds]; + } + + TSQuotedMessage *_Nullable quotedMessage = message.quotedMessage; + if (quotedMessage) { + [quotedReplyThumbnailAttachmentIds + addObjectsFromArray:quotedMessage.thumbnailAttachmentStreamIds]; + } + + OWSContact *_Nullable contactShare = message.contactShare; + if (contactShare && contactShare.avatarAttachmentId) { + [contactShareAvatarAttachmentIds + addObject:contactShare.avatarAttachmentId]; + } + }]; + }]; + if (shouldAbort) { + return nil; + } + + DDLogDebug(@"%@ fileCount: %zu", self.logTag, fileCount); + DDLogDebug(@"%@ totalFileSize: %lld", self.logTag, totalFileSize.longLongValue); + DDLogDebug(@"%@ attachmentStreams: %d", self.logTag, attachmentStreamCount); + DDLogDebug(@"%@ attachmentStreams with file paths: %zu", self.logTag, allAttachmentFilePaths.count); + + NSMutableSet *orphanFilePaths = [allOnDiskFilePaths mutableCopy]; + [orphanFilePaths minusSet:allAttachmentFilePaths]; + [orphanFilePaths minusSet:profileAvatarFilePaths]; + NSMutableSet *missingAttachmentFilePaths = [allAttachmentFilePaths mutableCopy]; + [missingAttachmentFilePaths minusSet:allOnDiskFilePaths]; + + DDLogDebug(@"%@ orphan file paths: %zu", self.logTag, orphanFilePaths.count); + DDLogDebug(@"%@ missing attachment file paths: %zu", self.logTag, missingAttachmentFilePaths.count); + + [self printPaths:orphanFilePaths.allObjects label:@"orphan file paths"]; + [self printPaths:missingAttachmentFilePaths.allObjects label:@"missing attachment file paths"]; + + DDLogDebug(@"%@ attachmentIds: %zu", self.logTag, allAttachmentIds.count); + DDLogDebug(@"%@ messageAttachmentIds: %zu", self.logTag, messageAttachmentIds.count); + DDLogDebug(@"%@ quotedReplyThumbnailAttachmentIds: %zu", self.logTag, quotedReplyThumbnailAttachmentIds.count); + DDLogDebug(@"%@ contactShareAvatarAttachmentIds: %zu", self.logTag, contactShareAvatarAttachmentIds.count); + + NSMutableSet *orphanAttachmentIds = [allAttachmentIds mutableCopy]; + [orphanAttachmentIds minusSet:messageAttachmentIds]; + [orphanAttachmentIds minusSet:quotedReplyThumbnailAttachmentIds]; + [orphanAttachmentIds minusSet:contactShareAvatarAttachmentIds]; + NSMutableSet *missingAttachmentIds = [messageAttachmentIds mutableCopy]; + [missingAttachmentIds minusSet:allAttachmentIds]; + + DDLogDebug(@"%@ orphan attachmentIds: %zu", self.logTag, orphanAttachmentIds.count); + DDLogDebug(@"%@ missing attachmentIds: %zu", self.logTag, missingAttachmentIds.count); + DDLogDebug(@"%@ orphan interactions: %zu", self.logTag, orphanInteractionIds.count); + + OWSOrphanData *result = [OWSOrphanData new]; + result.interactionIds = [orphanInteractionIds copy]; + result.attachmentIds = [orphanAttachmentIds copy]; + result.filePaths = [orphanFilePaths copy]; + return result; +} + ++ (void)auditOnLaunchIfNecessary +{ + OWSAssertIsOnMainThread(); + + // In production, do not audit or clean up. +#ifndef DEBUG + return; +#endif + + OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; + YapDatabaseConnection *databaseConnection = primaryStorage.dbReadWriteConnection; + + __block NSString *_Nullable lastCleaningVersion; + __block NSDate *_Nullable lastCleaningDate; + [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + lastCleaningVersion = [transaction stringForKey:OWSOrphanDataCleaner_LastCleaningVersionKey + inCollection:OWSOrphanDataCleaner_Collection]; + lastCleaningDate = [transaction dateForKey:OWSOrphanDataCleaner_LastCleaningDateKey + inCollection:OWSOrphanDataCleaner_Collection]; + }]; + + // Only clean up once per app version. + NSString *currentAppVersion = AppVersion.sharedInstance.currentAppVersion; + if (lastCleaningVersion && [lastCleaningVersion isEqualToString:currentAppVersion]) { + DDLogVerbose(@"%@ skipping orphan data cleanup; already done on %@.", self.logTag, currentAppVersion); + return; + } + + // Only clean up once per day. + if (lastCleaningDate && [DateUtil dateIsToday:lastCleaningDate]) { + DDLogVerbose(@"%@ skipping orphan data cleanup; already done today.", self.logTag); + return; + } + + // If we want to be cautious, we can disable orphan deletion using + // flag - the cleanup will just be a dry run with logging. + BOOL shouldRemoveOrphans = NO; + [self auditAndCleanup:shouldRemoveOrphans databaseConnection:databaseConnection]; +} + ++ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans +{ + OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; + YapDatabaseConnection *databaseConnection = primaryStorage.dbReadWriteConnection; + + [self auditAndCleanup:shouldRemoveOrphans databaseConnection:databaseConnection]; +} + +// We use the lowest priority possible. ++ (dispatch_queue_t)workQueue +{ + return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); +} + ++ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans databaseConnection:(YapDatabaseConnection *)databaseConnection +{ + OWSAssertIsOnMainThread(); + OWSAssert(databaseConnection); + + if (!AppReadiness.isAppReady) { + OWSProdLogAndFail(@"%@ can't audit orphan data until app is ready.", self.logTag); + return; + } + if (!CurrentAppContext().isMainApp) { + OWSProdLogAndFail(@"%@ can't audit orphan data in app extensions.", self.logTag); + return; + } + + // Orphan cleanup has two risks: + // + // * As a long-running process that involves access to the + // shared data container, it could cause 0xdead10cc. + // * It could accidentally delete data still in use, + // e.g. a profile avatar which has been saved to disk + // but whose OWSUserProfile hasn't been saved yet. + // + // To prevent 0xdead10cc, the cleaner continually checks + // whether the app has resigned active. If so, it aborts. + // Each phase (search, re-search, processing) retries N times, + // then gives up until the next app launch. + // + // To prevent accidental data deletion, we take the following + // measures: + // + // * Only cleanup data of the following types (which should + // include all relevant app data): profile avatar, + // attachment, temporary files (including temporary + // attachments). + // * We don't delete any data created more recently than N seconds + // _before_ when the app launched. This prevents any stray data + // currently in use by the app from being accidentally cleaned + // up. + const NSInteger kMaxRetries = 3; + [self findOrphanDataWithRetries:kMaxRetries + databaseConnection:databaseConnection + success:^(OWSOrphanData *orphanData) { + [self processOrphans:orphanData + remainingRetries:kMaxRetries + databaseConnection:databaseConnection + shouldRemoveOrphans:shouldRemoveOrphans + success:^{ + DDLogInfo(@"%@ Completed orphan data cleanup.", self.logTag); + + [databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [transaction setObject:AppVersion.sharedInstance.currentAppVersion + forKey:OWSOrphanDataCleaner_LastCleaningVersionKey + inCollection:OWSOrphanDataCleaner_Collection]; + [transaction setDate:[NSDate new] + forKey:OWSOrphanDataCleaner_LastCleaningDateKey + inCollection:OWSOrphanDataCleaner_Collection]; + }]; + } + failure:^{ + DDLogInfo(@"%@ Aborting orphan data cleanup.", self.logTag); + }]; + } + failure:^{ + DDLogInfo(@"%@ Aborting orphan data cleanup.", self.logTag); + }]; +} + +// Returns NO on failure, usually indicating that orphan processing +// aborted due to the app resigning active. This method is extremely careful to +// abort if the app resigns active, in order to avoid 0xdead10cc crashes. ++ (void)processOrphans:(OWSOrphanData *)orphanData + remainingRetries:(NSInteger)remainingRetries + databaseConnection:(YapDatabaseConnection *)databaseConnection + shouldRemoveOrphans:(BOOL)shouldRemoveOrphans + success:(dispatch_block_t)success + failure:(dispatch_block_t)failure +{ + OWSAssert(databaseConnection); + OWSAssert(orphanData); + + if (remainingRetries < 1) { + DDLogInfo(@"%@ Aborting orphan data audit.", self.logTag); + dispatch_async(self.workQueue, ^{ + failure(); + }); + return; + } + + // Wait until the app is active... + [CurrentAppContext() runNowOrWhenMainAppIsActive:^{ + // ...but perform the work off the main thread. + dispatch_async(self.workQueue, ^{ + if ([self processOrphansSync:orphanData + databaseConnection:databaseConnection + shouldRemoveOrphans:shouldRemoveOrphans]) { + success(); + return; + } else { + [self processOrphans:orphanData + remainingRetries:remainingRetries - 1 + databaseConnection:databaseConnection + shouldRemoveOrphans:shouldRemoveOrphans + success:success + failure:failure]; + } + }); + }]; +} + +// Returns NO on failure, usually indicating that orphan processing +// aborted due to the app resigning active. This method is extremely careful to +// abort if the app resigns active, in order to avoid 0xdead10cc crashes. ++ (BOOL)processOrphansSync:(OWSOrphanData *)orphanData + databaseConnection:(YapDatabaseConnection *)databaseConnection + shouldRemoveOrphans:(BOOL)shouldRemoveOrphans +{ + OWSAssert(databaseConnection); + OWSAssert(orphanData); + + __block BOOL shouldAbort = NO; + + // We need to avoid cleaning up new attachments and files that are still in the process of + // being created/written, so we don't clean up anything recent. + const NSTimeInterval kMinimumOrphanAgeSeconds = CurrentAppContext().isRunningTests ? 0.f : 15 * kMinuteInterval; + NSDate *appLaunchTime = AppVersion.sharedInstance.appLaunchTime; + NSTimeInterval thresholdTimestamp = appLaunchTime.timeIntervalSince1970 - kMinimumOrphanAgeSeconds; + NSDate *thresholdDate = [NSDate dateWithTimeIntervalSince1970:thresholdTimestamp]; + [databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + NSUInteger interactionsRemoved = 0; + for (NSString *interactionId in orphanData.interactionIds) { + if (!self.isMainAppAndActive) { + shouldAbort = YES; + return; + } + TSInteraction *_Nullable interaction = + [TSInteraction fetchObjectWithUniqueID:interactionId transaction:transaction]; + if (!interaction) { + // This could just be a race condition, but it should be very unlikely. + DDLogWarn(@"%@ Could not load interaction: %@", self.logTag, interactionId); + continue; + } + // Don't delete interactions which were created in the last N minutes. + NSDate *creationDate = [NSDate ows_dateWithMillisecondsSince1970:interaction.timestamp]; + if ([creationDate isAfterDate:thresholdDate]) { + DDLogInfo(@"%@ Skipping orphan interaction due to age: %f", + self.logTag, + fabs(creationDate.timeIntervalSinceNow)); + continue; + } + DDLogInfo(@"%@ Removing orphan message: %@", self.logTag, interaction.uniqueId); + interactionsRemoved++; + if (!shouldRemoveOrphans) { + continue; + } + [interaction removeWithTransaction:transaction]; + } + DDLogInfo(@"%@ Deleted orphan interactions: %zu", self.logTag, interactionsRemoved); + + NSUInteger attachmentsRemoved = 0; + for (NSString *attachmentId in orphanData.attachmentIds) { + if (!self.isMainAppAndActive) { + shouldAbort = YES; + return; + } + TSAttachment *_Nullable attachment = + [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction]; + if (!attachment) { + // This can happen on launch since we sync contacts/groups, especially if you have a lot of attachments + // to churn through, it's likely it's been deleted since starting this job. + DDLogWarn(@"%@ Could not load attachment: %@", self.logTag, attachmentId); + continue; + } + if (![attachment isKindOfClass:[TSAttachmentStream class]]) { + continue; + } + TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; + // Don't delete attachments which were created in the last N minutes. + NSDate *creationDate = attachmentStream.creationTimestamp; + if ([creationDate isAfterDate:thresholdDate]) { + DDLogInfo(@"%@ Skipping orphan attachment due to age: %f", + self.logTag, + fabs(creationDate.timeIntervalSinceNow)); + continue; + } + DDLogInfo(@"%@ Removing orphan attachmentStream: %@", self.logTag, attachmentStream.uniqueId); + attachmentsRemoved++; + if (!shouldRemoveOrphans) { + continue; + } + [attachmentStream removeWithTransaction:transaction]; + } + DDLogInfo(@"%@ Deleted orphan attachments: %zu", self.logTag, attachmentsRemoved); + }]; + + if (shouldAbort) { + return nil; + } + + NSUInteger filesRemoved = 0; + NSArray *filePaths = [orphanData.filePaths.allObjects sortedArrayUsingSelector:@selector(compare:)]; + for (NSString *filePath in filePaths) { + if (!self.isMainAppAndActive) { + return nil; + } + + NSError *error; + NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error]; + if (!attributes || error) { + OWSProdLogAndFail(@"%@ Could not get attributes of file at: %@", self.logTag, filePath); + continue; + } + // Don't delete files which were created in the last N minutes. + NSDate *creationDate = attributes.fileModificationDate; + if ([creationDate isAfterDate:thresholdDate]) { + DDLogInfo(@"%@ Skipping orphan attachment file due to age: %f", + self.logTag, + fabs([creationDate timeIntervalSinceNow])); + continue; + } + DDLogInfo(@"%@ Deleting orphan attachment file: %@", self.logTag, filePath); + filesRemoved++; + if (!shouldRemoveOrphans) { + continue; + } + [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; + if (error) { + OWSProdLogAndFail(@"%@ Could not remove orphan file at: %@", self.logTag, filePath); + } + } + DDLogInfo(@"%@ Deleted orphan attachment files: %zu", self.logTag, filesRemoved); + + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/environment/AppSetup.m b/SignalMessaging/environment/AppSetup.m index bf9f372612..4064b9f2a1 100644 --- a/SignalMessaging/environment/AppSetup.m +++ b/SignalMessaging/environment/AppSetup.m @@ -58,6 +58,7 @@ NS_ASSUME_NONNULL_BEGIN migrationCompletion(); + OWSAssert(backgroundTask); backgroundTask = nil; }]; }]; diff --git a/SignalMessaging/environment/Environment.m b/SignalMessaging/environment/Environment.m index 7756b2061b..f8d2300940 100644 --- a/SignalMessaging/environment/Environment.m +++ b/SignalMessaging/environment/Environment.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "Environment.h" diff --git a/SignalMessaging/environment/VersionMigrations.m b/SignalMessaging/environment/VersionMigrations.m index b9312489b5..42ed27e45d 100644 --- a/SignalMessaging/environment/VersionMigrations.m +++ b/SignalMessaging/environment/VersionMigrations.m @@ -40,8 +40,8 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert([Environment current]); OWSAssert(completion); - NSString *previousVersion = AppVersion.instance.lastAppVersion; - NSString *currentVersion = AppVersion.instance.currentAppVersion; + NSString *previousVersion = AppVersion.sharedInstance.lastAppVersion; + NSString *currentVersion = AppVersion.sharedInstance.currentAppVersion; DDLogInfo(@"%@ Checking migrations. currentVersion: %@, lastRanVersion: %@", self.logTag, diff --git a/SignalMessaging/profiles/OWSUserProfile.h b/SignalMessaging/profiles/OWSUserProfile.h index 1d68916aa6..4453a5dd0a 100644 --- a/SignalMessaging/profiles/OWSUserProfile.h +++ b/SignalMessaging/profiles/OWSUserProfile.h @@ -69,8 +69,11 @@ extern NSString *const kLocalProfileUniqueId; + (NSString *)profileAvatarFilepathWithFilename:(NSString *)filename; + (nullable NSError *)migrateToSharedData; ++ (NSString *)legacyProfileAvatarsDirPath; ++ (NSString *)sharedDataProfileAvatarsDirPath; + (NSString *)profileAvatarsDirPath; + (void)resetProfileStorage; ++ (NSSet *)allProfileAvatarFilePaths; @end diff --git a/SignalMessaging/profiles/OWSUserProfile.m b/SignalMessaging/profiles/OWSUserProfile.m index 180ab4925e..daa4648d2e 100644 --- a/SignalMessaging/profiles/OWSUserProfile.m +++ b/SignalMessaging/profiles/OWSUserProfile.m @@ -12,6 +12,7 @@ #import #import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -437,6 +438,33 @@ NSString *const kLocalProfileUniqueId = @"kLocalProfileUniqueId"; } } ++ (NSSet *)allProfileAvatarFilePaths +{ + NSString *profileAvatarsDirPath = self.profileAvatarsDirPath; + NSMutableSet *profileAvatarFilePaths = [NSMutableSet new]; + + [OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + [OWSUserProfile + enumerateCollectionObjectsWithTransaction:transaction + usingBlock:^(id object, BOOL *stop) { + if (![object isKindOfClass:[OWSUserProfile class]]) { + OWSProdLogAndFail(@"%@ unexpected object in user profiles: %@", + self.logTag, + [object class]); + return; + } + OWSUserProfile *userProfile = object; + if (!userProfile.avatarFileName) { + return; + } + NSString *filePath = [profileAvatarsDirPath + stringByAppendingPathComponent:userProfile.avatarFileName]; + [profileAvatarFilePaths addObject:filePath]; + }]; + }]; + return [profileAvatarFilePaths copy]; +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/views/OWSAlerts.swift b/SignalMessaging/views/OWSAlerts.swift index 344bff32f6..2c3fdfcee2 100644 --- a/SignalMessaging/views/OWSAlerts.swift +++ b/SignalMessaging/views/OWSAlerts.swift @@ -88,7 +88,7 @@ import Foundation // Don't show the nag to users who have just launched // the app for the first time. - guard AppVersion.instance().lastAppVersion != nil else { + guard AppVersion.sharedInstance().lastAppVersion != nil else { return } diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentsProcessor.m b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentsProcessor.m index 4ceab32f1b..e82355fccd 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentsProcessor.m +++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentsProcessor.m @@ -122,7 +122,8 @@ static const CGFloat kAttachmentDownloadProgressTheta = 0.001f; { OWSAssert(transaction); - __block OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + __block OWSBackgroundTask *_Nullable backgroundTask = + [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; [self setAttachment:attachment isDownloadingInMessage:message transaction:transaction]; @@ -132,6 +133,7 @@ static const CGFloat kAttachmentDownloadProgressTheta = 0.001f; [self setAttachment:attachment didFailInMessage:message error:error]; failureHandler(error); + OWSAssert(backgroundTask); backgroundTask = nil; }); }; @@ -144,6 +146,7 @@ static const CGFloat kAttachmentDownloadProgressTheta = 0.001f; [message touch]; } + OWSAssert(backgroundTask); backgroundTask = nil; }); }; diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h index 4a3d7ceff6..87304fbab6 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h @@ -62,7 +62,10 @@ NS_ASSUME_NONNULL_BEGIN - (nullable NSString *)readOversizeText; + (void)deleteAttachments; + + (NSString *)attachmentsFolder; ++ (NSString *)legacyAttachmentsDirPath; ++ (NSString *)sharedDataAttachmentsDirPath; - (BOOL)shouldHaveImageSize; - (CGSize)imageSize; diff --git a/SignalServiceKit/src/Messages/Interactions/TSInteraction.m b/SignalServiceKit/src/Messages/Interactions/TSInteraction.m index ab41fd09d4..7f04d4cbb0 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInteraction.m +++ b/SignalServiceKit/src/Messages/Interactions/TSInteraction.m @@ -110,10 +110,6 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value) #pragma mark Date operations -- (uint64_t)millisecondsTimestamp { - return self.timestamp; -} - - (NSDate *)dateForSorting { return [NSDate ows_dateWithMillisecondsSince1970:self.timestampForSorting]; diff --git a/SignalServiceKit/src/Messages/OWSBatchMessageProcessor.m b/SignalServiceKit/src/Messages/OWSBatchMessageProcessor.m index fe9506f5af..3e0f5d1769 100644 --- a/SignalServiceKit/src/Messages/OWSBatchMessageProcessor.m +++ b/SignalServiceKit/src/Messages/OWSBatchMessageProcessor.m @@ -355,12 +355,13 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo return; } - OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; NSArray *processedJobs = [self processJobs:batchJobs]; [self.finder removeJobsWithIds:processedJobs.uniqueIds]; + OWSAssert(backgroundTask); backgroundTask = nil; DDLogVerbose(@"%@ completed %lu/%lu jobs. %lu jobs left.", diff --git a/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m b/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m index ef2c4767e8..5abcc2d8df 100644 --- a/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m +++ b/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m @@ -137,6 +137,7 @@ void AssertIsOnDisappearingMessagesQueue() DDLogDebug(@"%@ Removed %lu expired messages", self.logTag, (unsigned long)expirationCount); + OWSAssert(backgroundTask); backgroundTask = nil; return expirationCount; } @@ -223,8 +224,8 @@ void AssertIsOnDisappearingMessagesQueue() OWSAssert(timestampForSorting > 0); OWSAssert(transaction); - __block OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; - + OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + // Become eventually consistent in the case that the remote changed their settings at the same time. // Also in case remote doesn't support expiring messages OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration = @@ -256,6 +257,7 @@ void AssertIsOnDisappearingMessagesQueue() createdInExistingGroup:createdInExistingGroup]; [infoMessage saveWithTransaction:transaction]; + OWSAssert(backgroundTask); backgroundTask = nil; } diff --git a/SignalServiceKit/src/Messages/OWSMessageReceiver.m b/SignalServiceKit/src/Messages/OWSMessageReceiver.m index 427925ac22..0c922e46f1 100644 --- a/SignalServiceKit/src/Messages/OWSMessageReceiver.m +++ b/SignalServiceKit/src/Messages/OWSMessageReceiver.m @@ -307,7 +307,8 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin return; } - __block OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + __block OWSBackgroundTask *_Nullable backgroundTask = + [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; [self processJob:job completion:^(BOOL success) { @@ -317,6 +318,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin success ? @"decrypted" : @"failed to decrypt", (unsigned long)[OWSMessageDecryptJob numberOfKeysInCollection]); [self drainQueueWorkStep]; + OWSAssert(backgroundTask); backgroundTask = nil; }]; } diff --git a/SignalServiceKit/src/Network/WebSockets/TSSocketManager.m b/SignalServiceKit/src/Network/WebSockets/TSSocketManager.m index 2960f725a2..2f27cccc6f 100644 --- a/SignalServiceKit/src/Network/WebSockets/TSSocketManager.m +++ b/SignalServiceKit/src/Network/WebSockets/TSSocketManager.m @@ -732,7 +732,8 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ if ([message.path isEqualToString:@"/api/v1/message"] && [message.verb isEqualToString:@"PUT"]) { - __block OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + __block OWSBackgroundTask *_Nullable backgroundTask = + [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @try { @@ -743,6 +744,7 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ if (!decryptedPayload) { DDLogWarn(@"%@ Failed to decrypt incoming payload or bad HMAC", self.logTag); [self sendWebSocketMessageAcknowledgement:message]; + OWSAssert(backgroundTask); backgroundTask = nil; return; } @@ -762,6 +764,7 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ dispatch_async(dispatch_get_main_queue(), ^{ [self sendWebSocketMessageAcknowledgement:message]; + OWSAssert(backgroundTask); backgroundTask = nil; }); }); diff --git a/SignalServiceKit/src/Storage/OWSIncomingMessageFinder.m b/SignalServiceKit/src/Storage/OWSIncomingMessageFinder.m index 42b1142006..e1e522076c 100644 --- a/SignalServiceKit/src/Storage/OWSIncomingMessageFinder.m +++ b/SignalServiceKit/src/Storage/OWSIncomingMessageFinder.m @@ -52,7 +52,7 @@ NSString *const OWSIncomingMessageFinderColumnSourceDeviceId = @"OWSIncomingMess - (YapDatabaseConnection *)dbConnection { - @synchronized (self) { + @synchronized(self) { if (!_dbConnection) { _dbConnection = [self.primaryStorage newDatabaseConnection]; } diff --git a/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.h b/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.h deleted file mode 100644 index 1b4f8fb2f3..0000000000 --- a/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.h +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -NS_ASSUME_NONNULL_BEGIN - -// Notes: -// -// * On disk, we only bother cleaning up files, not directories. -// * For code simplicity, we don't guarantee that everything is -// cleaned up in a single pass. If an interaction is cleaned up, -// it's attachments might not be cleaned up until the next pass. -// If an attachment is cleaned up, it's file on disk might not -// be cleaned up until the next pass. -@interface OWSOrphanedDataCleaner : NSObject - -- (instancetype)init NS_UNAVAILABLE; - -+ (void)auditAsync; - -// completion, if present, will be invoked on the main thread. -+ (void)auditAndCleanupAsync:(void (^_Nullable)(void))completion; - -+ (NSSet *)filePathsInAttachmentsFolder; - -+ (long long)fileSizeOfFilePaths:(NSArray *)filePaths; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m b/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m deleted file mode 100644 index 64360c25c0..0000000000 --- a/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m +++ /dev/null @@ -1,286 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "OWSOrphanedDataCleaner.h" -#import "NSDate+OWS.h" -#import "OWSContact.h" -#import "OWSPrimaryStorage.h" -#import "TSAttachmentStream.h" -#import "TSInteraction.h" -#import "TSMessage.h" -#import "TSQuotedMessage.h" -#import "TSThread.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@implementation OWSOrphanedDataCleaner - -+ (void)auditAsync -{ - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [OWSOrphanedDataCleaner auditAndCleanup:NO completion:nil]; - }); -} - -+ (void)auditAndCleanupAsync:(void (^_Nullable)(void))completion -{ - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [OWSOrphanedDataCleaner auditAndCleanup:YES completion:completion]; - }); -} - -// This method finds and optionally cleans up: -// -// * Orphan messages (with no thread). -// * Orphan attachments (with no message). -// * Orphan attachment files (with no attachment). -// * Missing attachment files (cannot be cleaned up). -// These are attachments which have no file on disk. They should be extremely rare - -// the only cases I have seen are probably due to debugging. -// They can't be cleaned up - we don't want to delete the TSAttachmentStream or -// its corresponding message. Better that the broken message shows up in the -// conversation view. -+ (void)auditAndCleanup:(BOOL)shouldCleanup completion:(void (^_Nullable)(void))completion -{ - NSSet *diskFilePaths = [self filePathsInAttachmentsFolder]; - long long totalFileSize = [self fileSizeOfFilePaths:diskFilePaths.allObjects]; - NSUInteger fileCount = diskFilePaths.count; - - OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; - YapDatabaseConnection *databaseConnection = primaryStorage.newDatabaseConnection; - - __block int attachmentStreamCount = 0; - NSMutableSet *attachmentFilePaths = [NSMutableSet new]; - NSMutableSet *attachmentIds = [NSMutableSet new]; - [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { - [transaction enumerateKeysAndObjectsInCollection:TSAttachmentStream.collection - usingBlock:^(NSString *key, TSAttachment *attachment, BOOL *stop) { - [attachmentIds addObject:attachment.uniqueId]; - if (![attachment isKindOfClass:[TSAttachmentStream class]]) { - return; - } - TSAttachmentStream *attachmentStream - = (TSAttachmentStream *)attachment; - attachmentStreamCount++; - NSString *_Nullable filePath = [attachmentStream filePath]; - OWSAssert(filePath); - [attachmentFilePaths addObject:filePath]; - - NSString *_Nullable thumbnailPath = [attachmentStream thumbnailPath]; - if (thumbnailPath.length > 0) { - [attachmentFilePaths addObject:thumbnailPath]; - } - }]; - }]; - - DDLogDebug(@"%@ fileCount: %lu", self.logTag, (unsigned long)fileCount); - DDLogDebug(@"%@ totalFileSize: %lld", self.logTag, totalFileSize); - DDLogDebug(@"%@ attachmentStreams: %d", self.logTag, attachmentStreamCount); - DDLogDebug(@"%@ attachmentStreams with file paths: %lu", self.logTag, (unsigned long)attachmentFilePaths.count); - - NSMutableSet *orphanDiskFilePaths = [diskFilePaths mutableCopy]; - [orphanDiskFilePaths minusSet:attachmentFilePaths]; - NSMutableSet *missingAttachmentFilePaths = [attachmentFilePaths mutableCopy]; - [missingAttachmentFilePaths minusSet:diskFilePaths]; - - DDLogDebug(@"%@ orphan disk file paths: %lu", self.logTag, (unsigned long)orphanDiskFilePaths.count); - DDLogDebug(@"%@ missing attachment file paths: %lu", self.logTag, (unsigned long)missingAttachmentFilePaths.count); - - [self printPaths:orphanDiskFilePaths.allObjects label:@"orphan disk file paths"]; - [self printPaths:missingAttachmentFilePaths.allObjects label:@"missing attachment file paths"]; - - __block NSMutableSet *threadIds; - [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { - threadIds = [[NSMutableSet alloc] initWithArray:[transaction allKeysInCollection:TSThread.collection]]; - }]; - - NSMutableSet *orphanInteractionIds = [NSMutableSet new]; - NSMutableSet *messageAttachmentIds = [NSMutableSet new]; - NSMutableSet *quotedReplyThumbnailAttachmentIds = [NSMutableSet new]; - NSMutableSet *contactShareAvatarAttachmentIds = [NSMutableSet new]; - - [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { - [transaction enumerateKeysAndObjectsInCollection:TSMessage.collection - usingBlock:^(NSString *key, TSInteraction *interaction, BOOL *stop) { - if (![threadIds containsObject:interaction.uniqueThreadId]) { - [orphanInteractionIds addObject:interaction.uniqueId]; - } - - if (![interaction isKindOfClass:[TSMessage class]]) { - return; - } - - TSMessage *message = (TSMessage *)interaction; - if (message.attachmentIds.count > 0) { - [messageAttachmentIds addObjectsFromArray:message.attachmentIds]; - } - - TSQuotedMessage *_Nullable quotedMessage = message.quotedMessage; - if (quotedMessage) { - [quotedReplyThumbnailAttachmentIds - addObjectsFromArray:quotedMessage - .thumbnailAttachmentStreamIds]; - } - - OWSContact *_Nullable contactShare = message.contactShare; - if (contactShare && contactShare.avatarAttachmentId) { - [contactShareAvatarAttachmentIds - addObject:contactShare.avatarAttachmentId]; - } - }]; - }]; - - DDLogDebug(@"%@ attachmentIds: %lu", self.logTag, (unsigned long)attachmentIds.count); - DDLogDebug(@"%@ messageAttachmentIds: %lu", self.logTag, (unsigned long)messageAttachmentIds.count); - DDLogDebug(@"%@ quotedReplyThumbnailAttachmentIds: %lu", - self.logTag, - (unsigned long)quotedReplyThumbnailAttachmentIds.count); - DDLogDebug( - @"%@ contactShareAvatarAttachmentIds: %lu", self.logTag, (unsigned long)contactShareAvatarAttachmentIds.count); - - NSMutableSet *orphanAttachmentIds = [attachmentIds mutableCopy]; - [orphanAttachmentIds minusSet:messageAttachmentIds]; - [orphanAttachmentIds minusSet:quotedReplyThumbnailAttachmentIds]; - [orphanAttachmentIds minusSet:contactShareAvatarAttachmentIds]; - NSMutableSet *missingAttachmentIds = [messageAttachmentIds mutableCopy]; - [missingAttachmentIds minusSet:attachmentIds]; - - DDLogDebug(@"%@ orphan attachmentIds: %lu", self.logTag, (unsigned long)orphanAttachmentIds.count); - DDLogDebug(@"%@ missing attachmentIds: %lu", self.logTag, (unsigned long)missingAttachmentIds.count); - DDLogDebug(@"%@ orphan interactions: %lu", self.logTag, (unsigned long)orphanInteractionIds.count); - - // We need to avoid cleaning up new attachments and files that are still in the process of - // being created/written, so we don't clean up anything recent. - - const NSTimeInterval kMinimumOrphanAge = CurrentAppContext().isRunningTests ? 0.f : 15 * kMinuteInterval; - - if (!shouldCleanup) { - return; - } - - [databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { - for (NSString *interactionId in orphanInteractionIds) { - TSInteraction *interaction = [TSInteraction fetchObjectWithUniqueID:interactionId transaction:transaction]; - if (!interaction) { - // This could just be a race condition, but it should be very unlikely. - OWSFail(@"%@ Could not load interaction: %@", self.logTag, interactionId); - continue; - } - DDLogInfo(@"%@ Removing orphan message: %@", self.logTag, interaction.uniqueId); - [interaction removeWithTransaction:transaction]; - } - for (NSString *attachmentId in orphanAttachmentIds) { - TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction]; - if (!attachment) { - // This can happen on launch since we sync contacts/groups, especially if you have a lot of attachments - // to churn through, it's likely it's been deleted since starting this job. - DDLogWarn(@"%@ Could not load attachment: %@", self.logTag, attachmentId); - continue; - } - if (![attachment isKindOfClass:[TSAttachmentStream class]]) { - continue; - } - TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; - // Don't delete attachments which were created in the last N minutes. - if (fabs([attachmentStream.creationTimestamp timeIntervalSinceNow]) < kMinimumOrphanAge) { - DDLogInfo(@"%@ Skipping orphan attachment due to age: %f", - self.logTag, - fabs([attachmentStream.creationTimestamp timeIntervalSinceNow])); - continue; - } - DDLogInfo(@"%@ Removing orphan attachmentStream from DB: %@", self.logTag, attachmentStream.uniqueId); - [attachmentStream removeWithTransaction:transaction]; - } - }]; - - for (NSString *filePath in orphanDiskFilePaths) { - NSError *error; - NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error]; - if (!attributes || error) { - OWSFail(@"%@ Could not get attributes of file at: %@", self.logTag, filePath); - continue; - } - // Don't delete files which were created in the last N minutes. - if (fabs([attributes.fileModificationDate timeIntervalSinceNow]) < kMinimumOrphanAge) { - DDLogInfo(@"%@ Skipping orphan attachment file due to age: %f", - self.logTag, - fabs([attributes.fileModificationDate timeIntervalSinceNow])); - continue; - } - - DDLogInfo(@"%@ Deleting orphan attachment file: %@", self.logTag, filePath); - [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; - if (error) { - OWSFail(@"%@ Could not remove orphan file at: %@", self.logTag, filePath); - } - } - - if (completion) { - dispatch_async(dispatch_get_main_queue(), ^{ - completion(); - }); - } -} - -+ (void)printPaths:(NSArray *)paths label:(NSString *)label -{ - for (NSString *path in [paths sortedArrayUsingSelector:@selector(compare:)]) { - DDLogDebug(@"%@ %@: %@", self.logTag, label, path); - } -} - -+ (NSSet *)filePathsInAttachmentsFolder -{ - NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder]; - DDLogDebug(@"%@ attachmentsFolder: %@", self.logTag, attachmentsFolder); - - return [self filePathsInDirectory:attachmentsFolder]; -} - -+ (NSSet *)filePathsInDirectory:(NSString *)dirPath -{ - NSMutableSet *filePaths = [NSMutableSet new]; - NSError *error; - NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error]; - if (error) { - OWSFail(@"%@ contentsOfDirectoryAtPath error: %@", self.logTag, error); - return [NSSet new]; - } - for (NSString *fileName in fileNames) { - NSString *filePath = [dirPath stringByAppendingPathComponent:fileName]; - BOOL isDirectory; - [[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]; - if (isDirectory) { - [filePaths addObjectsFromArray:[self filePathsInDirectory:filePath].allObjects]; - } else { - [filePaths addObject:filePath]; - } - } - return filePaths; -} - -+ (long long)fileSizeOfFilePath:(NSString *)filePath -{ - NSError *error; - NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error][NSFileSize]; - if (error) { - OWSFail(@"%@ attributesOfItemAtPath: %@ error: %@", self.logTag, filePath, error); - return 0; - } - return fileSize.longLongValue; -} - -+ (long long)fileSizeOfFilePaths:(NSArray *)filePaths -{ - long long result = 0; - for (NSString *filePath in filePaths) { - result += [self fileSizeOfFilePath:filePath]; - } - return result; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/AppContext.h b/SignalServiceKit/src/Util/AppContext.h index 8b2801491c..6ec9baa26b 100755 --- a/SignalServiceKit/src/Util/AppContext.h +++ b/SignalServiceKit/src/Util/AppContext.h @@ -16,6 +16,7 @@ extern NSString *const OWSApplicationWillResignActiveNotification; extern NSString *const OWSApplicationDidBecomeActiveNotification; typedef void (^BackgroundTaskExpirationHandler)(void); +typedef void (^AppActiveBlock)(void); NSString *NSStringForUIApplicationState(UIApplicationState value); @@ -91,6 +92,8 @@ NSString *NSStringForUIApplicationState(UIApplicationState value); // Should be a NOOP if isMainApp is NO. - (void)setNetworkActivityIndicatorVisible:(BOOL)value; +- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block; + @end id CurrentAppContext(void); diff --git a/SignalServiceKit/src/Util/AppReadiness.m b/SignalServiceKit/src/Util/AppReadiness.m index 31491893d1..61a3de3361 100755 --- a/SignalServiceKit/src/Util/AppReadiness.m +++ b/SignalServiceKit/src/Util/AppReadiness.m @@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN @property (atomic) BOOL isAppReady; -@property (nonatomic, nullable) NSMutableArray *appReadyBlocks; +@property (nonatomic) NSMutableArray *appReadyBlocks; @end @@ -39,6 +39,8 @@ NS_ASSUME_NONNULL_BEGIN OWSSingletonAssert(); + self.appReadyBlocks = [NSMutableArray new]; + return self; } @@ -64,9 +66,6 @@ NS_ASSUME_NONNULL_BEGIN return; } - if (!self.appReadyBlocks) { - self.appReadyBlocks = [NSMutableArray new]; - } [self.appReadyBlocks addObject:block]; } @@ -92,10 +91,11 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertIsOnMainThread(); OWSAssert(self.isAppReady); - for (AppReadyBlock block in self.appReadyBlocks) { + NSArray *appReadyBlocks = [self.appReadyBlocks copy]; + [self.appReadyBlocks removeAllObjects]; + for (AppReadyBlock block in appReadyBlocks) { block(); } - self.appReadyBlocks = nil; } @end diff --git a/SignalServiceKit/src/Util/AppVersion.h b/SignalServiceKit/src/Util/AppVersion.h index df84b8b35a..71da4ce720 100755 --- a/SignalServiceKit/src/Util/AppVersion.h +++ b/SignalServiceKit/src/Util/AppVersion.h @@ -4,17 +4,21 @@ @interface AppVersion : NSObject -@property (nonatomic, readonly) NSString *firstAppVersion; -@property (nonatomic, readonly) NSString *lastAppVersion; -@property (nonatomic, readonly) NSString *currentAppVersion; +// The properties are updated immediately after launch. +@property (atomic, readonly) NSString *firstAppVersion; +@property (atomic, readonly) NSString *lastAppVersion; +@property (atomic, readonly) NSString *currentAppVersion; -// Unlike lastAppVersion, this property isn't updated until -// appLaunchDidComplete is called. -@property (nonatomic, readonly) NSString *lastCompletedLaunchAppVersion; -@property (nonatomic, readonly) NSString *lastCompletedLaunchMainAppVersion; -@property (nonatomic, readonly) NSString *lastCompletedLaunchSAEAppVersion; +// There properties aren't updated until appLaunchDidComplete is called. +@property (atomic, readonly) NSString *lastCompletedLaunchAppVersion; +@property (atomic, readonly) NSString *lastCompletedLaunchMainAppVersion; +@property (atomic, readonly) NSString *lastCompletedLaunchSAEAppVersion; -+ (instancetype)instance; +@property (atomic, readonly) NSDate *appLaunchTime; + +- (instancetype)init NS_UNAVAILABLE; + ++ (instancetype)sharedInstance; - (void)mainAppLaunchDidComplete; - (void)saeLaunchDidComplete; diff --git a/SignalServiceKit/src/Util/AppVersion.m b/SignalServiceKit/src/Util/AppVersion.m index a9bcd64a7e..ff99018254 100755 --- a/SignalServiceKit/src/Util/AppVersion.m +++ b/SignalServiceKit/src/Util/AppVersion.m @@ -15,12 +15,13 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE @interface AppVersion () -@property (nonatomic) NSString *firstAppVersion; -@property (nonatomic) NSString *lastAppVersion; -@property (nonatomic) NSString *currentAppVersion; -@property (nonatomic) NSString *lastCompletedLaunchAppVersion; -@property (nonatomic) NSString *lastCompletedLaunchMainAppVersion; -@property (nonatomic) NSString *lastCompletedLaunchSAEAppVersion; +@property (atomic) NSString *firstAppVersion; +@property (atomic) NSString *lastAppVersion; +@property (atomic) NSString *currentAppVersion; + +@property (atomic) NSString *lastCompletedLaunchAppVersion; +@property (atomic) NSString *lastCompletedLaunchMainAppVersion; +@property (atomic) NSString *lastCompletedLaunchSAEAppVersion; @end @@ -28,7 +29,8 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE @implementation AppVersion -+ (instancetype)instance { ++ (instancetype)sharedInstance +{ static AppVersion *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -39,6 +41,10 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE } - (void)configure { + OWSAssertIsOnMainThread(); + + _appLaunchTime = [NSDate new]; + self.currentAppVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; // The version of the app when it was first launched. @@ -78,6 +84,8 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE - (void)appLaunchDidComplete { + OWSAssertIsOnMainThread(); + DDLogInfo(@"%@ appLaunchDidComplete", self.logTag); self.lastCompletedLaunchAppVersion = self.currentAppVersion; @@ -90,6 +98,8 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE - (void)mainAppLaunchDidComplete { + OWSAssertIsOnMainThread(); + self.lastCompletedLaunchMainAppVersion = self.currentAppVersion; [[NSUserDefaults appUserDefaults] setObject:self.currentAppVersion forKey:kNSUserDefaults_LastCompletedLaunchAppVersion_MainApp]; @@ -99,6 +109,8 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE - (void)saeLaunchDidComplete { + OWSAssertIsOnMainThread(); + self.lastCompletedLaunchSAEAppVersion = self.currentAppVersion; [[NSUserDefaults appUserDefaults] setObject:self.currentAppVersion forKey:kNSUserDefaults_LastCompletedLaunchAppVersion_SAE]; diff --git a/SignalShareExtension/ShareViewController.swift b/SignalShareExtension/ShareViewController.swift index 07ba68ad68..9421932144 100644 --- a/SignalShareExtension/ShareViewController.swift +++ b/SignalShareExtension/ShareViewController.swift @@ -44,7 +44,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed Logger.info("\(self.logTag) \(#function)") - _ = AppVersion() + _ = AppVersion.sharedInstance() startupLogging() @@ -265,7 +265,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed // We don't need to use DeviceSleepManager in the SAE. - AppVersion.instance().saeLaunchDidComplete() + AppVersion.sharedInstance().saeLaunchDidComplete() Environment.current().contactsManager.loadSignalAccountsFromCache() Environment.current().contactsManager.startObserving() @@ -277,7 +277,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed OWSProfileManager.shared().ensureLocalProfileCached() - // We don't need to use OWSOrphanedDataCleaner in the SAE. + // We don't need to use OWSOrphanDataCleaner in the SAE. // We don't need to fetch the local profile in the SAE diff --git a/SignalShareExtension/utils/ShareAppExtensionContext.m b/SignalShareExtension/utils/ShareAppExtensionContext.m index 99d2862047..0fcf69175d 100644 --- a/SignalShareExtension/utils/ShareAppExtensionContext.m +++ b/SignalShareExtension/utils/ShareAppExtensionContext.m @@ -205,6 +205,11 @@ NS_ASSUME_NONNULL_BEGIN OWSFail(@"%@ called %s.", self.logTag, __PRETTY_FUNCTION__); } +- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block +{ + OWSProdLogAndFail(@"%@ cannot run main app active blocks in share extension.", self.logTag); +} + @end NS_ASSUME_NONNULL_END