You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m

425 lines
16 KiB
Matlab

//
// 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
9 years ago
#import "ContactsManagerProtocol.h"
#import "NSDate+OWS.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
9 years ago
#import "OWSDisappearingConfigurationUpdateInfoMessage.h"
#import "OWSDisappearingMessagesConfiguration.h"
#import "OWSDisappearingMessagesFinder.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
9 years ago
#import "TSIncomingMessage.h"
#import "TSMessage.h"
#import "TSStorageManager.h"
NS_ASSUME_NONNULL_BEGIN
// Can we move to Signal-iOS?
@interface OWSDisappearingMessagesJob ()
@property (nonatomic, readonly) YapDatabaseConnection *databaseConnection;
@property (nonatomic, readonly) OWSDisappearingMessagesFinder *disappearingMessagesFinder;
// These three properties should only be accessed on the main thread.
@property (nonatomic) BOOL hasStarted;
@property (nonatomic, nullable) NSTimer *timer;
@property (nonatomic, nullable) NSDate *timerScheduleDate;
@end
#pragma mark -
@implementation OWSDisappearingMessagesJob
+ (instancetype)sharedJob
{
static OWSDisappearingMessagesJob *sharedJob = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedJob = [[self alloc] initWithStorageManager:[TSStorageManager sharedManager]];
});
return sharedJob;
}
- (instancetype)initWithStorageManager:(TSStorageManager *)storageManager
{
self = [super init];
if (!self) {
return self;
}
_databaseConnection = storageManager.newDatabaseConnection;
_disappearingMessagesFinder = [OWSDisappearingMessagesFinder new];
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;
}
// This method should only be called on the serialQueue.
- (void)run
{
uint64_t now = [NSDate ows_millisecondTimeStamp];
OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
__block uint expirationCount = 0;
[self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self.disappearingMessagesFinder enumerateExpiredMessagesWithBlock:^(TSMessage *message) {
// sanity check
if (message.expiresAt > now) {
DDLogError(
@"%@ Refusing to remove message which doesn't expire until: %lld", self.logTag, message.expiresAt);
return;
}
DDLogDebug(@"%@ Removing message which expired at: %lld", self.logTag, message.expiresAt);
[message removeWithTransaction:transaction];
expirationCount++;
}
transaction:transaction];
}];
DDLogDebug(@"%@ Removed %u expired messages", self.logTag, expirationCount);
backgroundTask = nil;
}
// This method should only be called on the serialQueue.
- (void)runLoop
{
DDLogVerbose(@"%@ Run", self.logTag);
[self run];
uint64_t now = [NSDate ows_millisecondTimeStamp];
__block NSNumber *nextExpirationTimestampNumber;
[self.databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
nextExpirationTimestampNumber =
[self.disappearingMessagesFinder nextExpirationTimestampWithTransaction:transaction];
}];
if (!nextExpirationTimestampNumber) {
// In theory we could kill the loop here. It should resume when the next expiring message is saved,
// But this is a safeguard for any race conditions that exist while running the job as a new message is saved.
DDLogDebug(@"%@ No more expiring messages.", self.logTag);
[self runLater];
return;
}
uint64_t nextExpirationAt = [nextExpirationTimestampNumber unsignedLongLongValue];
[self runByDate:[NSDate ows_dateWithMillisecondsSince1970:MAX(nextExpirationAt, now)]];
}
+ (void)setExpirationForMessage:(TSMessage *)message
{
dispatch_async(self.serialQueue, ^{
[[self sharedJob] setExpirationForMessage:message];
});
}
- (void)setExpirationForMessage:(TSMessage *)message
{
if (!message.isExpiringMessage) {
return;
}
OWSDisappearingMessagesConfiguration *disappearingConfig =
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:message.uniqueThreadId];
if (!disappearingConfig.isEnabled) {
return;
}
[self setExpirationForMessage:message expirationStartedAt:[NSDate ows_millisecondTimeStamp]];
}
+ (void)setExpirationForMessage:(TSMessage *)message expirationStartedAt:(uint64_t)expirationStartedAt
{
dispatch_async(self.serialQueue, ^{
[[self sharedJob] setExpirationForMessage:message expirationStartedAt:expirationStartedAt];
});
}
// This method should only be called on the serialQueue.
- (void)setExpirationForMessage:(TSMessage *)message expirationStartedAt:(uint64_t)expirationStartedAt
{
[self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self setExpirationForMessage:message expirationStartedAt:expirationStartedAt transaction:transaction];
}];
}
- (void)setExpirationForMessage:(TSMessage *)message
expirationStartedAt:(uint64_t)expirationStartedAt
transaction:(YapDatabaseReadWriteTransaction *_Nonnull)transaction
{
OWSAssert(transaction);
if (!message.isExpiringMessage) {
return;
}
int startedSecondsAgo = [NSDate new].timeIntervalSince1970 - expirationStartedAt / 1000.0;
DDLogDebug(@"%@ Starting expiration for message read %d seconds ago", self.logTag, startedSecondsAgo);
// Don't clobber if multiple actions simultaneously triggered expiration.
if (message.expireStartedAt == 0 || message.expireStartedAt > expirationStartedAt) {
[message updateWithExpireStartedAt:expirationStartedAt transaction:transaction];
}
// Necessary that the async expiration run happens *after* the message is saved with expiration configuration.
[self runByDate:[NSDate ows_dateWithMillisecondsSince1970:message.expiresAt]];
}
+ (void)setExpirationsForThread:(TSThread *)thread
{
dispatch_async(self.serialQueue, ^{
[[self sharedJob] setExpirationsForThread:thread];
});
}
// This method should only be called on the serialQueue.
- (void)setExpirationsForThread:(TSThread *)thread
{
OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
uint64_t now = [NSDate ows_millisecondTimeStamp];
[self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self.disappearingMessagesFinder
enumerateUnstartedExpiringMessagesInThread:thread
block:^(TSMessage *_Nonnull message) {
DDLogWarn(
@"%@ Starting expiring message which should have already "
@"been started.",
self.logTag);
// specify "now" in case D.M. have since been disabled, but we have
// existing unstarted expiring messages that still need to expire.
[self setExpirationForMessage:message
expirationStartedAt:now
transaction:transaction];
}
transaction:transaction];
}];
backgroundTask = nil;
}
+ (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message
contactsManager:(id<ContactsManagerProtocol>)contactsManager
{
[[self sharedJob] becomeConsistentWithConfigurationForMessage:message contactsManager:contactsManager];
}
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
9 years ago
- (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message
contactsManager:(id<ContactsManagerProtocol>)contactsManager
{
OWSAssert(message);
OWSAssert(contactsManager);
7 years ago
__block OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{
// 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 =
[OWSDisappearingMessagesConfiguration fetchOrCreateDefaultWithThreadId:message.uniqueThreadId];
BOOL changed = NO;
if (message.expiresInSeconds == 0) {
if (disappearingMessagesConfiguration.isEnabled) {
changed = YES;
DDLogWarn(@"%@ Received remote message which had no expiration set, disabling our expiration to become "
@"consistent.",
self.logTag);
disappearingMessagesConfiguration.enabled = NO;
[disappearingMessagesConfiguration save];
}
} else if (message.expiresInSeconds != disappearingMessagesConfiguration.durationSeconds) {
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
9 years ago
changed = YES;
DDLogInfo(@"%@ Received remote message with different expiration set, updating our expiration to become "
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
9 years ago
@"consistent.",
self.logTag);
disappearingMessagesConfiguration.enabled = YES;
disappearingMessagesConfiguration.durationSeconds = message.expiresInSeconds;
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
9 years ago
[disappearingMessagesConfiguration save];
}
if (!changed) {
return;
}
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
9 years ago
if ([message isKindOfClass:[TSIncomingMessage class]]) {
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)message;
NSString *contactName = [contactsManager displayNameForPhoneIdentifier:incomingMessage.messageAuthorId];
// We want the info message to appear _before_ the message.
[[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestamp - 1
thread:message.thread
configuration:disappearingMessagesConfiguration
createdByRemoteName:contactName] save];
} else {
// We want the info message to appear _before_ the message.
[[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestamp - 1
thread:message.thread
configuration:disappearingMessagesConfiguration]
save];
}
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
9 years ago
}
- (void)startIfNecessary
{
dispatch_async(dispatch_get_main_queue(), ^{
if (self.hasStarted) {
return;
}
self.hasStarted = YES;
[self runNow];
});
}
- (void)runNow
{
[self runByDate:[NSDate new] ignoreMinDelay:YES];
}
- (NSTimeInterval)maxDelaySeconds
{
// Don't run less often than once per N minutes.
return 5 * kMinuteInterval;
}
// Waits the maximum amount of time to run again.
- (void)runLater
{
[self runByDate:[NSDate dateWithTimeIntervalSinceNow:self.maxDelaySeconds] ignoreMinDelay:YES];
}
- (void)runByDate:(NSDate *)date
{
[self runByDate:date ignoreMinDelay:NO];
}
- (void)runByDate:(NSDate *)date ignoreMinDelay:(BOOL)ignoreMinDelay
{
OWSAssert(date);
dispatch_async(dispatch_get_main_queue(), ^{
if (!CurrentAppContext().isMainAppAndActive) {
// Don't schedule run when inactive or not in main app.
return;
}
NSDateFormatter *dateFormatter = [NSDateFormatter new];
dateFormatter.dateStyle = NSDateFormatterNoStyle;
dateFormatter.timeStyle = kCFDateFormatterMediumStyle;
dateFormatter.locale = [NSLocale systemLocale];
// Don't run more often than once per second.
const NSTimeInterval kMinDelaySeconds = ignoreMinDelay ? 0.f : 1.f;
NSTimeInterval delaySeconds
= MAX(kMinDelaySeconds, MIN(self.maxDelaySeconds, [date timeIntervalSinceDate:[NSDate new]]));
NSDate *timerScheduleDate = [NSDate dateWithTimeIntervalSinceNow:delaySeconds];
if (self.timerScheduleDate && [timerScheduleDate timeIntervalSinceDate:self.timerScheduleDate] > 0) {
DDLogVerbose(@"%@ Request to run at %@ (%d sec.) ignored due to scheduled run at %@ (%d sec.)",
self.logTag,
[dateFormatter stringFromDate:date],
(int)round(MAX(0, [date timeIntervalSinceDate:[NSDate new]])),
[dateFormatter stringFromDate:self.timerScheduleDate],
(int)round(MAX(0, [self.timerScheduleDate timeIntervalSinceDate:[NSDate new]])));
return;
}
// Update Schedule
DDLogVerbose(@"%@ Scheduled run at %@ (%d sec.)",
self.logTag,
[dateFormatter stringFromDate:timerScheduleDate],
(int)round(MAX(0, [timerScheduleDate timeIntervalSinceDate:[NSDate new]])));
[self resetTimer];
self.timerScheduleDate = timerScheduleDate;
self.timer = [NSTimer weakScheduledTimerWithTimeInterval:delaySeconds
target:self
selector:@selector(timerDidFire)
userInfo:nil
repeats:NO];
});
}
- (void)timerDidFire
{
8 years ago
OWSAssertIsOnMainThread();
if (!CurrentAppContext().isMainAppAndActive) {
// Don't schedule run when inactive or not in main app.
8 years ago
OWSFail(@"%@ Disappearing messages job timer fired while main app inactive.", self.logTag);
return;
}
[self resetTimer];
dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{
[self runLoop];
});
}
- (void)resetTimer
{
8 years ago
OWSAssertIsOnMainThread();
[self.timer invalidate];
self.timer = nil;
self.timerScheduleDate = nil;
}
#pragma mark - Notifications
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
8 years ago
OWSAssertIsOnMainThread();
[AppReadiness runNowOrWhenAppIsReady:^{
[self runNow];
}];
}
- (void)applicationWillResignActive:(NSNotification *)notification
{
8 years ago
OWSAssertIsOnMainThread();
[self resetTimer];
}
@end
NS_ASSUME_NONNULL_END