session-ios/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m

422 lines
15 KiB
Mathematica
Raw Normal View History

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSDisappearingMessagesJob.h"
#import "AppContext.h"
#import "AppReadiness.h"
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
#import "ContactsManagerProtocol.h"
#import "NSTimer+OWS.h"
#import "OWSBackgroundTask.h"
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
#import "OWSDisappearingConfigurationUpdateInfoMessage.h"
#import "OWSDisappearingMessagesConfiguration.h"
#import "OWSDisappearingMessagesFinder.h"
#import "OWSPrimaryStorage.h"
2018-10-10 23:36:41 +02:00
#import "SSKEnvironment.h"
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
#import "TSIncomingMessage.h"
#import "TSMessage.h"
#import "TSThread.h"
2018-09-21 21:41:10 +02:00
#import <SignalCoreKit/NSDate+OWS.h>
NS_ASSUME_NONNULL_BEGIN
// Can we move to Signal-iOS?
@interface OWSDisappearingMessagesJob ()
2017-06-15 21:36:21 +02:00
@property (nonatomic, readonly) YapDatabaseConnection *databaseConnection;
@property (nonatomic, readonly) OWSDisappearingMessagesFinder *disappearingMessagesFinder;
+ (dispatch_queue_t)serialQueue;
// These three properties should only be accessed on the main thread.
@property (nonatomic) BOOL hasStarted;
@property (nonatomic, nullable) NSTimer *nextDisappearanceTimer;
@property (nonatomic, nullable) NSDate *nextDisappearanceDate;
@property (nonatomic, nullable) NSTimer *fallbackTimer;
@end
void AssertIsOnDisappearingMessagesQueue()
{
#ifdef DEBUG
if (@available(iOS 10.0, *)) {
dispatch_assert_queue(OWSDisappearingMessagesJob.serialQueue);
}
#endif
}
#pragma mark -
@implementation OWSDisappearingMessagesJob
+ (instancetype)sharedJob
{
2018-10-10 23:36:41 +02:00
OWSAssertDebug(SSKEnvironment.shared.disappearingMessagesJob);
return SSKEnvironment.shared.disappearingMessagesJob;
}
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
{
self = [super init];
if (!self) {
return self;
}
_databaseConnection = primaryStorage.newDatabaseConnection;
_disappearingMessagesFinder = [OWSDisappearingMessagesFinder new];
// suspenders in case a deletion schedule is missed.
NSTimeInterval kFallBackTimerInterval = 5 * kMinuteInterval;
[AppReadiness runNowOrWhenAppDidBecomeReady:^{
if (CurrentAppContext().isMainApp) {
self.fallbackTimer = [NSTimer weakScheduledTimerWithTimeInterval:kFallBackTimerInterval
target:self
selector:@selector(fallbackTimerDidFire)
userInfo:nil
repeats:YES];
}
}];
OWSSingletonAssert();
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:OWSApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillResignActive:)
name:OWSApplicationWillResignActiveNotification
object:nil];
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
+ (dispatch_queue_t)serialQueue
{
static dispatch_queue_t queue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
queue = dispatch_queue_create("org.whispersystems.disappearing.messages", DISPATCH_QUEUE_SERIAL);
});
return queue;
}
#pragma mark - Dependencies
- (id<ContactsManagerProtocol>)contactsManager
{
return SSKEnvironment.shared.contactsManager;
}
#pragma mark -
- (NSUInteger)deleteExpiredMessages
{
AssertIsOnDisappearingMessagesQueue();
uint64_t now = [NSDate ows_millisecondTimeStamp];
OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
__block NSUInteger expirationCount = 0;
2017-06-15 21:36:21 +02:00
[self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self.disappearingMessagesFinder enumerateExpiredMessagesWithBlock:^(TSMessage *message) {
// sanity check
if (message.expiresAt > now) {
OWSFailDebug(@"Refusing to remove message which doesn't expire until: %lld", message.expiresAt);
return;
}
OWSLogInfo(@"Removing message which expired at: %lld", message.expiresAt);
[message removeWithTransaction:transaction];
expirationCount++;
}
transaction:transaction];
}];
OWSLogDebug(@"Removed %lu expired messages", (unsigned long)expirationCount);
OWSAssertDebug(backgroundTask);
backgroundTask = nil;
return expirationCount;
}
// deletes any expired messages and schedules the next run.
- (NSUInteger)runLoop
{
OWSLogVerbose(@"in runLoop");
AssertIsOnDisappearingMessagesQueue();
NSUInteger deletedCount = [self deleteExpiredMessages];
__block NSNumber *nextExpirationTimestampNumber;
2017-06-15 21:36:21 +02:00
[self.databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
nextExpirationTimestampNumber =
[self.disappearingMessagesFinder nextExpirationTimestampWithTransaction:transaction];
}];
if (!nextExpirationTimestampNumber) {
OWSLogDebug(@"No more expiring messages.");
return deletedCount;
}
uint64_t nextExpirationAt = nextExpirationTimestampNumber.unsignedLongLongValue;
NSDate *nextEpirationDate = [NSDate ows_dateWithMillisecondsSince1970:nextExpirationAt];
[self scheduleRunByDate:nextEpirationDate];
return deletedCount;
}
- (void)startAnyExpirationForMessage:(TSMessage *)message
expirationStartedAt:(uint64_t)expirationStartedAt
transaction:(YapDatabaseReadWriteTransaction *_Nonnull)transaction
{
OWSAssertDebug(transaction);
if (!message.isExpiringMessage) {
return;
}
NSTimeInterval startedSecondsAgo = ([NSDate ows_millisecondTimeStamp] - expirationStartedAt) / 1000.0;
OWSLogDebug(@"Starting expiration for message read %f seconds ago", startedSecondsAgo);
// Don't clobber if multiple actions simultaneously triggered expiration.
if (message.expireStartedAt == 0 || message.expireStartedAt > expirationStartedAt) {
[message updateWithExpireStartedAt:expirationStartedAt transaction:transaction];
}
[transaction addCompletionQueue:nil
completionBlock:^{
// Necessary that the async expiration run happens *after* the message is saved with it's new
// expiration configuration.
[self scheduleRunByDate:[NSDate ows_dateWithMillisecondsSince1970:message.expiresAt]];
}];
}
#pragma mark - Apply Remote Configuration
- (void)becomeConsistentWithDisappearingDuration:(uint32_t)duration
thread:(TSThread *)thread
createdByRemoteRecipientId:(nullable NSString *)remoteRecipientId
createdInExistingGroup:(BOOL)createdInExistingGroup
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssertDebug(thread);
OWSAssertDebug(transaction);
2018-08-02 21:18:40 +02:00
OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
NSString *_Nullable remoteContactName = nil;
if (remoteRecipientId) {
remoteContactName = [self.contactsManager displayNameForPhoneIdentifier:remoteRecipientId
transaction:transaction];
}
// 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 =
[thread disappearingMessagesConfigurationWithTransaction:transaction];
if (duration == 0) {
disappearingMessagesConfiguration.enabled = NO;
} else {
disappearingMessagesConfiguration.enabled = YES;
disappearingMessagesConfiguration.durationSeconds = duration;
}
if (!disappearingMessagesConfiguration.dictionaryValueDidChange) {
return;
}
OWSLogInfo(@"becoming consistent with disappearing message configuration: %@",
disappearingMessagesConfiguration.dictionaryValue);
[disappearingMessagesConfiguration saveWithTransaction:transaction];
// MJK TODO - should be safe to remove this senderTimestamp
OWSDisappearingConfigurationUpdateInfoMessage *infoMessage =
[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
thread:thread
configuration:disappearingMessagesConfiguration
createdByRemoteName:remoteContactName
createdInExistingGroup:createdInExistingGroup];
[infoMessage saveWithTransaction:transaction];
OWSAssertDebug(backgroundTask);
backgroundTask = nil;
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
}
#pragma mark -
- (void)startIfNecessary
{
dispatch_async(dispatch_get_main_queue(), ^{
if (self.hasStarted) {
return;
}
self.hasStarted = YES;
dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{
2018-05-03 01:15:46 +02:00
// Theoretically this shouldn't be necessary, but there was a race condition when receiving a backlog
// of messages across timer changes which could cause a disappearing message's timer to never be started.
[self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self cleanupMessagesWhichFailedToStartExpiringWithTransaction:transaction];
}];
[self runLoop];
});
});
}
- (NSDateFormatter *)dateFormatter
{
static NSDateFormatter *dateFormatter;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dateFormatter = [NSDateFormatter new];
dateFormatter.dateStyle = NSDateFormatterNoStyle;
dateFormatter.timeStyle = kCFDateFormatterMediumStyle;
dateFormatter.locale = [NSLocale systemLocale];
});
return dateFormatter;
}
- (void)scheduleRunByDate:(NSDate *)date
{
OWSAssertDebug(date);
dispatch_async(dispatch_get_main_queue(), ^{
if (!CurrentAppContext().isMainAppAndActive) {
// Don't schedule run when inactive or not in main app.
return;
}
// Don't run more often than once per second.
const NSTimeInterval kMinDelaySeconds = 1.0;
NSTimeInterval delaySeconds = MAX(kMinDelaySeconds, date.timeIntervalSinceNow);
NSDate *newTimerScheduleDate = [NSDate dateWithTimeIntervalSinceNow:delaySeconds];
if (self.nextDisappearanceDate && [self.nextDisappearanceDate isBeforeDate:newTimerScheduleDate]) {
OWSLogVerbose(@"Request to run at %@ (%d sec.) ignored due to earlier scheduled run at %@ (%d sec.)",
[self.dateFormatter stringFromDate:date],
(int)round(MAX(0, [date timeIntervalSinceDate:[NSDate new]])),
[self.dateFormatter stringFromDate:self.nextDisappearanceDate],
(int)round(MAX(0, [self.nextDisappearanceDate timeIntervalSinceDate:[NSDate new]])));
return;
}
// Update Schedule
OWSLogVerbose(@"Scheduled run at %@ (%d sec.)",
[self.dateFormatter stringFromDate:newTimerScheduleDate],
(int)round(MAX(0, [newTimerScheduleDate timeIntervalSinceDate:[NSDate new]])));
[self resetNextDisappearanceTimer];
self.nextDisappearanceDate = newTimerScheduleDate;
self.nextDisappearanceTimer = [NSTimer weakScheduledTimerWithTimeInterval:delaySeconds
target:self
selector:@selector(disappearanceTimerDidFire)
userInfo:nil
repeats:NO];
});
}
- (void)disappearanceTimerDidFire
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSLogDebug(@"");
if (!CurrentAppContext().isMainAppAndActive) {
// Don't schedule run when inactive or not in main app.
OWSFailDebug(@"Disappearing messages job timer fired while main app inactive.");
return;
}
[self resetNextDisappearanceTimer];
dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{
[self runLoop];
});
}
- (void)fallbackTimerDidFire
{
OWSAssertIsOnMainThread();
OWSLogDebug(@"");
BOOL recentlyScheduledDisappearanceTimer = NO;
if (fabs(self.nextDisappearanceDate.timeIntervalSinceNow) < 1.0) {
recentlyScheduledDisappearanceTimer = YES;
}
2018-05-18 19:23:10 +02:00
if (!CurrentAppContext().isMainAppAndActive) {
OWSLogInfo(@"Ignoring fallbacktimer for app which is not main and active.");
2018-05-18 19:23:10 +02:00
return;
}
dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{
NSUInteger deletedCount = [self runLoop];
// Normally deletions should happen via the disappearanceTimer, to make sure that they're prompt.
// So, if we're deleting something via this fallback timer, something may have gone wrong. The
// exception is if we're in close proximity to the disappearanceTimer, in which case a race condition
// is inevitable.
if (!recentlyScheduledDisappearanceTimer && deletedCount > 0) {
OWSFailDebug(@"unexpectedly deleted disappearing messages via fallback timer.");
}
});
}
- (void)resetNextDisappearanceTimer
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
[self.nextDisappearanceTimer invalidate];
self.nextDisappearanceTimer = nil;
self.nextDisappearanceDate = nil;
}
2018-05-03 01:15:46 +02:00
#pragma mark - Cleanup
- (void)cleanupMessagesWhichFailedToStartExpiringWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[self.disappearingMessagesFinder
enumerateMessagesWhichFailedToStartExpiringWithBlock:^(TSMessage *_Nonnull message) {
OWSFailDebug(@"starting old timer for message timestamp: %lu", (unsigned long)message.timestamp);
// We don't know when it was actually read, so assume it was read as soon as it was received.
uint64_t readTimeBestGuess = message.receivedAtTimestamp;
2018-06-13 19:20:58 +02:00
[self startAnyExpirationForMessage:message expirationStartedAt:readTimeBestGuess transaction:transaction];
}
transaction:transaction];
2018-05-03 01:15:46 +02:00
}
#pragma mark - Notifications
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
[AppReadiness runNowOrWhenAppDidBecomeReady:^{
dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{
[self runLoop];
});
}];
}
- (void)applicationWillResignActive:(NSNotification *)notification
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
[self resetNextDisappearanceTimer];
}
@end
NS_ASSUME_NONNULL_END