diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 734b08064..3507d9b26 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -86,7 +86,6 @@ #import #import #import -#import #import #import #import diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift b/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift index 68829ef48..14325da72 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift @@ -234,7 +234,6 @@ public class ConversationMediaView: UIView { AssertIsOnMainThread() guard let loadBlock = loadBlock else { - owsFailDebug("Missing loadBlock") return } loadBlock() @@ -245,7 +244,6 @@ public class ConversationMediaView: UIView { AssertIsOnMainThread() guard let unloadBlock = unloadBlock else { - owsFailDebug("Missing unloadBlock") return } unloadBlock() diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h index 33fa1b9f6..6da61337a 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h @@ -39,8 +39,7 @@ extern const UIDataDetectorTypes kOWSAllowedDataDetectorTypes; - (void)didTapTruncatedTextMessage:(id)conversationItem; -- (void)didTapFailedIncomingAttachment:(id)viewItem - attachmentPointer:(TSAttachmentPointer *)attachmentPointer; +- (void)didTapFailedIncomingAttachment:(id)viewItem; - (void)didTapConversationItem:(id)viewItem quotedReply:(OWSQuotedReplyModel *)quotedReply; - (void)didTapConversationItem:(id)viewItem diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index a3dc4cc47..fbbfeddc7 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -1402,7 +1402,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes OWSAssertDebug(attachmentPointer); if (attachmentPointer.state == TSAttachmentPointerStateFailed) { - [self.delegate didTapFailedIncomingAttachment:self.viewItem attachmentPointer:attachmentPointer]; + [self.delegate didTapFailedIncomingAttachment:self.viewItem]; } break; } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 6ec594bff..daa8f2ce5 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -67,7 +67,7 @@ #import #import #import -#import +#import #import #import #import @@ -297,6 +297,11 @@ typedef enum : NSUInteger { return SSKEnvironment.shared.typingIndicators; } +- (OWSAttachmentDownloads *)attachmentDownloads +{ + return SSKEnvironment.shared.attachmentDownloads; +} + #pragma mark - - (void)addNotificationListeners @@ -1644,13 +1649,11 @@ typedef enum : NSUInteger { #pragma mark Bubble User Actions - (void)handleFailedDownloadTapForMessage:(TSMessage *)message - attachmentPointer:(TSAttachmentPointer *)attachmentPointer { - OWSAttachmentsProcessor *processor = - [[OWSAttachmentsProcessor alloc] initWithAttachmentPointers:@[ attachmentPointer ]]; + OWSAssert(message); - [self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { - [processor fetchAttachmentsForMessage:message + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + [self.attachmentDownloads downloadAttachmentsForMessage:message transaction:transaction success:^(NSArray *attachmentStreams) { OWSLogInfo(@"Successfully redownloaded attachment in thread: %@", message.thread); @@ -2159,15 +2162,13 @@ typedef enum : NSUInteger { } - (void)didTapFailedIncomingAttachment:(id)viewItem - attachmentPointer:(TSAttachmentPointer *)attachmentPointer { OWSAssertIsOnMainThread(); OWSAssertDebug(viewItem); - OWSAssertDebug(attachmentPointer); // Restart failed downloads TSMessage *message = (TSMessage *)viewItem.interaction; - [self handleFailedDownloadTapForMessage:message attachmentPointer:attachmentPointer]; + [self handleFailedDownloadTapForMessage:message]; } - (void)didTapFailedOutgoingMessage:(TSOutgoingMessage *)message @@ -2192,17 +2193,13 @@ typedef enum : NSUInteger { return; } - OWSAttachmentsProcessor *processor = - [[OWSAttachmentsProcessor alloc] initWithAttachmentPointers:@[ attachmentPointer ]]; - - [self.editingDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [processor fetchAttachmentsForMessage:nil - transaction:transaction + [self.uiDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction *transaction) { + [self.attachmentDownloads downloadAttachmentPointer:attachmentPointer success:^(NSArray *attachmentStreams) { OWSAssertDebug(attachmentStreams.count == 1); TSAttachmentStream *attachmentStream = attachmentStreams.firstObject; [self.editingDatabaseConnection - asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *postSuccessTransaction) { + readWriteWithBlock:^(YapDatabaseReadWriteTransaction *postSuccessTransaction) { [message setQuotedMessageThumbnailAttachmentStream:attachmentStream]; [message saveWithTransaction:postSuccessTransaction]; }]; @@ -2210,8 +2207,8 @@ typedef enum : NSUInteger { failure:^(NSError *error) { OWSLogWarn(@"Failed to redownload thumbnail with error: %@", error); [self.editingDatabaseConnection - asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *postSuccessTransaction) { - [message touchWithTransaction:transaction]; + readWriteWithBlock:^(YapDatabaseReadWriteTransaction *postSuccessTransaction) { + [message touchWithTransaction:postSuccessTransaction]; }]; }]; }]; diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index bd15cf48a..5073ffd5d 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -694,7 +694,7 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele navigationController.pushViewController(viewController, animated: true) } - func didTapFailedIncomingAttachment(_ viewItem: ConversationViewItem, attachmentPointer: TSAttachmentPointer) { + func didTapFailedIncomingAttachment(_ viewItem: ConversationViewItem) { // no - op } diff --git a/SignalMessaging/environment/AppSetup.m b/SignalMessaging/environment/AppSetup.m index 02abb1d79..023f5bc6b 100644 --- a/SignalMessaging/environment/AppSetup.m +++ b/SignalMessaging/environment/AppSetup.m @@ -11,6 +11,7 @@ #import #import #import +#import #import #import #import @@ -86,6 +87,7 @@ NS_ASSUME_NONNULL_BEGIN OWSSyncManager *syncManager = [[OWSSyncManager alloc] initDefault]; id reachabilityManager = [SSKReachabilityManagerImpl new]; id typingIndicators = [[OWSTypingIndicatorsImpl alloc] init]; + OWSAttachmentDownloads *attachmentDownloads = [[OWSAttachmentDownloads alloc] init]; OWSAudioSession *audioSession = [OWSAudioSession new]; OWSSounds *sounds = [[OWSSounds alloc] initWithPrimaryStorage:primaryStorage]; @@ -123,7 +125,8 @@ NS_ASSUME_NONNULL_BEGIN outgoingReceiptManager:outgoingReceiptManager reachabilityManager:reachabilityManager syncManager:syncManager - typingIndicators:typingIndicators]]; + typingIndicators:typingIndicators + attachmentDownloads:attachmentDownloads]]; appSpecificSingletonBlock(); diff --git a/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.h b/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.h index 83892e887..7fbcec362 100644 --- a/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.h +++ b/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.h @@ -5,24 +5,14 @@ NS_ASSUME_NONNULL_BEGIN @class OWSIncomingSentMessageTranscript; -@class OWSPrimaryStorage; -@class OWSReadReceiptManager; @class TSAttachmentStream; -@class TSNetworkManager; @class YapDatabaseReadWriteTransaction; -@protocol ContactsManagerProtocol; - // This job is used to process "outgoing message" notifications from linked devices. @interface OWSRecordTranscriptJob : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript; -- (instancetype)initWithIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript - networkManager:(TSNetworkManager *)networkManager - primaryStorage:(OWSPrimaryStorage *)primaryStorage - readReceiptManager:(OWSReadReceiptManager *)readReceiptManager - contactsManager:(id)contactsManager NS_DESIGNATED_INITIALIZER; - (void)runWithAttachmentHandler:(void (^)(NSArray *attachmentStreams))attachmentHandler diff --git a/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m b/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m index a4a04cd59..16b22acac 100644 --- a/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m +++ b/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m @@ -3,7 +3,7 @@ // #import "OWSRecordTranscriptJob.h" -#import "OWSAttachmentsProcessor.h" +#import "OWSAttachmentDownloads.h" #import "OWSDisappearingMessagesJob.h" #import "OWSIncomingSentMessageTranscript.h" #import "OWSPrimaryStorage+SessionStore.h" @@ -19,31 +19,15 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSRecordTranscriptJob () -@property (nonatomic, readonly) TSNetworkManager *networkManager; -@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; -@property (nonatomic, readonly) OWSReadReceiptManager *readReceiptManager; -@property (nonatomic, readonly) id contactsManager; - @property (nonatomic, readonly) OWSIncomingSentMessageTranscript *incomingSentMessageTranscript; @end +#pragma mark - + @implementation OWSRecordTranscriptJob - (instancetype)initWithIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript -{ - return [self initWithIncomingSentMessageTranscript:incomingSentMessageTranscript - networkManager:TSNetworkManager.sharedManager - primaryStorage:OWSPrimaryStorage.sharedManager - readReceiptManager:OWSReadReceiptManager.sharedManager - contactsManager:SSKEnvironment.shared.contactsManager]; -} - -- (instancetype)initWithIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript - networkManager:(TSNetworkManager *)networkManager - primaryStorage:(OWSPrimaryStorage *)primaryStorage - readReceiptManager:(OWSReadReceiptManager *)readReceiptManager - contactsManager:(id)contactsManager { self = [super init]; if (!self) { @@ -51,14 +35,47 @@ NS_ASSUME_NONNULL_BEGIN } _incomingSentMessageTranscript = incomingSentMessageTranscript; - _networkManager = networkManager; - _primaryStorage = primaryStorage; - _readReceiptManager = readReceiptManager; - _contactsManager = contactsManager; return self; } +#pragma mark - Dependencies + +- (OWSPrimaryStorage *)primaryStorage +{ + OWSAssertDebug(SSKEnvironment.shared.primaryStorage); + + return SSKEnvironment.shared.primaryStorage; +} + +- (TSNetworkManager *)networkManager +{ + OWSAssertDebug(SSKEnvironment.shared.networkManager); + + return SSKEnvironment.shared.networkManager; +} + +- (OWSReadReceiptManager *)readReceiptManager +{ + OWSAssert(SSKEnvironment.shared.readReceiptManager); + + return SSKEnvironment.shared.readReceiptManager; +} + +- (id)contactsManager +{ + OWSAssertDebug(SSKEnvironment.shared.contactsManager); + + return SSKEnvironment.shared.contactsManager; +} + +- (OWSAttachmentDownloads *)attachmentDownloads +{ + return SSKEnvironment.shared.attachmentDownloads; +} + +#pragma mark - + - (void)runWithAttachmentHandler:(void (^)(NSArray *attachmentStreams))attachmentHandler transaction:(YapDatabaseReadWriteTransaction *)transaction { @@ -107,22 +124,19 @@ NS_ASSUME_NONNULL_BEGIN transaction:transaction]; if ([attachmentPointer isKindOfClass:[TSAttachmentPointer class]]) { - OWSAttachmentsProcessor *attachmentProcessor = - [[OWSAttachmentsProcessor alloc] initWithAttachmentPointers:@[ attachmentPointer ]]; + OWSLogDebug(@"downloading attachments for transcript: %lu", (unsigned long)transcript.timestamp); - OWSLogDebug(@"downloading thumbnail for transcript: %lu", (unsigned long)transcript.timestamp); - [attachmentProcessor fetchAttachmentsForMessage:outgoingMessage - transaction:transaction + [self.attachmentDownloads downloadAttachmentPointer:attachmentPointer success:^(NSArray *attachmentStreams) { OWSAssertDebug(attachmentStreams.count == 1); TSAttachmentStream *attachmentStream = attachmentStreams.firstObject; [self.primaryStorage.newDatabaseConnection - asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [outgoingMessage setQuotedMessageThumbnailAttachmentStream:attachmentStream]; [outgoingMessage saveWithTransaction:transaction]; }]; } - failure:^(NSError *_Nonnull error) { + failure:^(NSError *error) { OWSLogWarn(@"failed to fetch thumbnail for transcript: %lu with error: %@", (unsigned long)transcript.timestamp, error); @@ -161,15 +175,14 @@ NS_ASSUME_NONNULL_BEGIN [self.readReceiptManager applyEarlyReadReceiptsForOutgoingMessageFromLinkedDevice:outgoingMessage transaction:transaction]; - OWSAttachmentsProcessor *attachmentsProcessor = - [[OWSAttachmentsProcessor alloc] initWithAttachmentPointers:attachmentPointers]; - [attachmentsProcessor - fetchAttachmentsForMessage:outgoingMessage - transaction:transaction - success:attachmentHandler - failure:^(NSError *_Nonnull error) { - OWSLogError(@"failed to fetch transcripts attachments for message: %@", outgoingMessage); - }]; + [self.attachmentDownloads + downloadAttachmentsForMessage:outgoingMessage + transaction:transaction + success:attachmentHandler + failure:^(NSError *error) { + OWSLogError( + @"failed to fetch transcripts attachments for message: %@", outgoingMessage); + }]; } @end diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.h b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.h new file mode 100644 index 000000000..e08e60c82 --- /dev/null +++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.h @@ -0,0 +1,46 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kAttachmentDownloadProgressNotification; +extern NSString *const kAttachmentDownloadProgressKey; +extern NSString *const kAttachmentDownloadAttachmentIDKey; + +@class SSKProtoAttachmentPointer; +@class TSAttachment; +@class TSAttachmentPointer; +@class TSAttachmentStream; +@class TSMessage; +@class YapDatabaseReadTransaction; +@class YapDatabaseReadWriteTransaction; + +#pragma mark - + +/** + * Given incoming attachment protos, determines which we support. + * It can download those that we support and notifies threads when it receives unsupported attachments. + */ +@interface OWSAttachmentDownloads : NSObject + +// This will try to download all un-downloaded attachments for a given message. +// Any attachments for the message which are already downloaded are skipped BUT +// they are included in the success callback. +// +// success/failure are always called on a worker queue. +- (void)downloadAttachmentsForMessage:(TSMessage *)message + transaction:(YapDatabaseReadTransaction *)transaction + success:(void (^)(NSArray *attachmentStreams))success + failure:(void (^)(NSError *error))failure; + +// This will try to download a single attachment. +// +// success/failure are always called on a worker queue. +- (void)downloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer + success:(void (^)(NSArray *attachmentStreams))success + failure:(void (^)(NSError *error))failure; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentsProcessor.m b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m similarity index 59% rename from SignalServiceKit/src/Messages/Attachments/OWSAttachmentsProcessor.m rename to SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m index 15aca313f..9014dd2ca 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentsProcessor.m +++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m @@ -2,7 +2,7 @@ // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // -#import "OWSAttachmentsProcessor.h" +#import "OWSAttachmentDownloads.h" #import "AppContext.h" #import "MIMETypeUtil.h" #import "NSNotificationCenter+OWS.h" @@ -36,103 +36,307 @@ NSString *const kAttachmentDownloadAttachmentIDKey = @"kAttachmentDownloadAttach // indicator shows up as quickly as possible. static const CGFloat kAttachmentDownloadProgressTheta = 0.001f; -@interface OWSAttachmentsProcessor () +typedef void (^AttachmentDownloadSuccess)(TSAttachmentStream *attachmentStream); +typedef void (^AttachmentDownloadFailure)(NSError *error); -@property (nonatomic, readonly) TSNetworkManager *networkManager; +@interface OWSAttachmentDownloadJob : NSObject + +@property (nonatomic, readonly) TSAttachmentPointer *attachmentPointer; +@property (nonatomic, readonly, nullable) TSMessage *message; +@property (nonatomic, readonly) AttachmentDownloadSuccess success; +@property (nonatomic, readonly) AttachmentDownloadFailure failure; @end -@implementation OWSAttachmentsProcessor +#pragma mark - -- (instancetype)initWithAttachmentPointers:(NSArray *)attachmentPointers +@implementation OWSAttachmentDownloadJob + +- (instancetype)initWithAttachmentPointer:(TSAttachmentPointer *)attachmentPointer + message:(nullable TSMessage *)message + success:(AttachmentDownloadSuccess)success + failure:(AttachmentDownloadFailure)failure { self = [super init]; if (!self) { return self; } - _attachmentPointers = attachmentPointers; + _attachmentPointer = attachmentPointer; + _message = message; + _success = success; + _failure = failure; return self; } +@end + +#pragma mark - + +@interface OWSAttachmentDownloads () + +// This property should only be accessed while synchronized on this class. +@property (nonatomic, readonly) NSMutableSet *downloadingAttachmentIdSet; +// This property should only be accessed while synchronized on this class. +@property (nonatomic, readonly) NSMutableArray *attachmentDownloadJobQueue; + +@end + +#pragma mark - + +@implementation OWSAttachmentDownloads + #pragma mark - Dependencies +- (OWSPrimaryStorage *)primaryStorage +{ + return SSKEnvironment.shared.primaryStorage; +} + - (TSNetworkManager *)networkManager { return SSKEnvironment.shared.networkManager; } -#pragma mark -- (void)fetchAttachmentsForMessage:(nullable TSMessage *)message - transaction:(YapDatabaseReadWriteTransaction *)transaction - success:(void (^)(NSArray *attachmentStreams))successHandler - failure:(void (^)(NSError *error))failureHandler +#pragma mark - + +- (instancetype)init { - OWSAssertDebug(transaction); - OWSAssertDebug(self.attachmentPointers.count > 0); - - NSMutableArray *promises = [NSMutableArray array]; - NSMutableArray *attachmentStreams = [NSMutableArray array]; - - for (TSAttachmentPointer *attachmentPointer in self.attachmentPointers) { - AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { - [self retrieveAttachment:attachmentPointer - message:message - transaction:transaction - success:^(TSAttachmentStream *attachmentStream) { - OWSLogVerbose(@"Attachment download succeeded."); - @synchronized(attachmentStreams) { - [attachmentStreams addObject:attachmentStream]; - } - resolve(@(1)); - } - failure:^(NSError *error) { - OWSLogError(@"Attachment download failed with error: %@", error); - resolve(error); - }]; - }]; - [promises addObject:promise]; + self = [super init]; + if (!self) { + return self; } - // We use PMKJoin(), not PMKWhen(), because we don't want the - // completion promise to execute until _all_ promises - // have either succeeded or failed. PMKWhen() executes as - // soon as any of its input promises fail. - AnyPromise *completionPromise - = PMKJoin(promises) - .then(^(id value) { - NSArray *attachmentStreamsCopy; - @synchronized(attachmentStreams) { - attachmentStreamsCopy = [attachmentStreams copy]; - } - OWSLogInfo(@"Attachment downloads succeeded: %lu.", (unsigned long)attachmentStreamsCopy.count); - successHandler(attachmentStreamsCopy); - }) - .catch(^(NSError *error) { - failureHandler(error); - }); - [completionPromise retainUntilComplete]; + _downloadingAttachmentIdSet = [NSMutableSet new]; + _attachmentDownloadJobQueue = [NSMutableArray new]; + + return self; } +#pragma mark - + +- (void)downloadAttachmentsForMessage:(TSMessage *)message + transaction:(YapDatabaseReadTransaction *)transaction + success:(void (^)(NSArray *attachmentStreams))success + failure:(void (^)(NSError *error))failure +{ + OWSAssertDebug(transaction); + OWSAssertDebug(message); + + NSMutableArray *attachmentStreams = [NSMutableArray array]; + NSMutableArray *attachmentPointers = [NSMutableArray new]; + + for (TSAttachment *attachment in [message attachmentsWithTransaction:transaction]) { + if ([attachment isKindOfClass:[TSAttachmentStream class]]) { + TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; + [attachmentStreams addObject:attachmentStream]; + } else if ([attachment isKindOfClass:[TSAttachmentPointer class]]) { + TSAttachmentPointer *attachmentPointer = (TSAttachmentPointer *)attachment; + [attachmentPointers addObject:attachmentPointer]; + } else { + OWSFailDebug(@"Unexpected attachment type: %@", attachment.class); + } + } + + [self enqueueJobsForAttachmentStreams:attachmentStreams + attachmentPointers:attachmentPointers + message:message + success:success + failure:failure]; +} + +- (void)downloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer + success:(void (^)(NSArray *attachmentStreams))success + failure:(void (^)(NSError *error))failure +{ + OWSAssertDebug(attachmentPointer); + + [self enqueueJobsForAttachmentStreams:@[] + attachmentPointers:@[ + attachmentPointer, + ] + message:nil + success:success + failure:failure]; +} + +- (void)enqueueJobsForAttachmentStreams:(NSArray *)attachmentStreamsParam + attachmentPointers:(NSArray *)attachmentPointers + message:(nullable TSMessage *)message + success:(void (^)(NSArray *attachmentStreams))successHandler + failure:(void (^)(NSError *error))failureHandler +{ + OWSAssertDebug(attachmentStreamsParam); + OWSAssertDebug(attachmentPointers.count > 0); + + // To avoid deadlocks, synchronize on self outside of the transaction. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (attachmentPointers.count < 1) { + OWSAssertDebug(attachmentStreamsParam.count > 0); + successHandler(attachmentStreamsParam); + return; + } + + NSMutableArray *attachmentStreams = [attachmentStreamsParam mutableCopy]; + NSMutableArray *promises = [NSMutableArray array]; + for (TSAttachmentPointer *attachmentPointer in attachmentPointers) { + AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + [self enqueueJobForAttachmentPointer:attachmentPointer + message:message + success:^(TSAttachmentStream *attachmentStream) { + @synchronized(attachmentStreams) { + [attachmentStreams addObject:attachmentStream]; + } + + resolve(@(1)); + } + failure:^(NSError *error) { + resolve(error); + }]; + }]; + [promises addObject:promise]; + } + + // We use PMKJoin(), not PMKWhen(), because we don't want the + // completion promise to execute until _all_ promises + // have either succeeded or failed. PMKWhen() executes as + // soon as any of its input promises fail. + AnyPromise *completionPromise + = PMKJoin(promises) + .then(^(id value) { + NSArray *attachmentStreamsCopy; + @synchronized(attachmentStreams) { + attachmentStreamsCopy = [attachmentStreams copy]; + } + OWSLogInfo(@"Attachment downloads succeeded: %lu.", (unsigned long)attachmentStreamsCopy.count); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + successHandler(attachmentStreamsCopy); + }); + }) + .catch(^(NSError *error) { + OWSLogError(@"Attachment downloads failed."); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + failureHandler(error); + }); + }); + [completionPromise retainUntilComplete]; + }); +} + +- (void)enqueueJobForAttachmentPointer:(TSAttachmentPointer *)attachmentPointer + message:(nullable TSMessage *)message + success:(void (^)(TSAttachmentStream *attachmentStream))success + failure:(void (^)(NSError *error))failure +{ + OWSAssertDebug(attachmentPointer); + + OWSAttachmentDownloadJob *job = [[OWSAttachmentDownloadJob alloc] initWithAttachmentPointer:attachmentPointer + message:message + success:success + failure:failure]; + + @synchronized(self) { + [self.attachmentDownloadJobQueue addObject:job]; + } + + [self startDownloadIfPossible]; +} + +- (void)startDownloadIfPossible +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + OWSAttachmentDownloadJob *_Nullable job; + + @synchronized(self) { + const NSUInteger kMaxSimultaneousDownloads = 4; + if (self.downloadingAttachmentIdSet.count >= kMaxSimultaneousDownloads) { + return; + } + job = self.attachmentDownloadJobQueue.firstObject; + if (!job) { + return; + } + if ([self.downloadingAttachmentIdSet containsObject:job.attachmentPointer.uniqueId]) { + // Ensure we only have one download in flight at a time for a given attachment. + OWSLogWarn(@"Ignoring duplicate download."); + return; + } + [self.attachmentDownloadJobQueue removeObjectAtIndex:0]; + [self.downloadingAttachmentIdSet addObject:job.attachmentPointer.uniqueId]; + } + + [self.primaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + job.attachmentPointer.state = TSAttachmentPointerStateDownloading; + [job.attachmentPointer saveWithTransaction:transaction]; + + if (job.message) { + [job.message touchWithTransaction:transaction]; + } + }]; + + [self retrieveAttachment:job.attachmentPointer + success:^(TSAttachmentStream *attachmentStream) { + OWSLogVerbose(@"Attachment download succeeded."); + + [self.primaryStorage.dbReadWriteConnection + readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [attachmentStream saveWithTransaction:transaction]; + + if (job.message) { + [job.message touchWithTransaction:transaction]; + } + }]; + + job.success(attachmentStream); + + @synchronized(self) { + [self.downloadingAttachmentIdSet removeObject:job.attachmentPointer.uniqueId]; + } + + [self startDownloadIfPossible]; + } + failure:^(NSError *error) { + OWSLogError(@"Attachment download failed with error: %@", error); + + [self.primaryStorage.dbReadWriteConnection + readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + job.attachmentPointer.mostRecentFailureLocalizedText = error.localizedDescription; + job.attachmentPointer.state = TSAttachmentPointerStateFailed; + [job.attachmentPointer saveWithTransaction:transaction]; + + if (job.message) { + [job.message touchWithTransaction:transaction]; + } + }]; + + @synchronized(self) { + [self.downloadingAttachmentIdSet removeObject:job.attachmentPointer.uniqueId]; + } + + job.failure(error); + + [self startDownloadIfPossible]; + }]; + }); +} + +#pragma mark - + - (void)retrieveAttachment:(TSAttachmentPointer *)attachment - message:(nullable TSMessage *)message - transaction:(YapDatabaseReadWriteTransaction *)transaction success:(void (^)(TSAttachmentStream *attachmentStream))successHandler failure:(void (^)(NSError *error))failureHandler { - OWSAssertDebug(transaction); + OWSAssertDebug(attachment); __block OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; - [self setAttachment:attachment isDownloadingInMessage:message transaction:transaction]; - void (^markAndHandleFailure)(NSError *) = ^(NSError *error) { - // Ensure enclosing transaction is complete. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self setAttachment:attachment didFailInMessage:message error:error]; failureHandler(error); OWSAssertDebug(backgroundTask); @@ -141,12 +345,8 @@ static const CGFloat kAttachmentDownloadProgressTheta = 0.001f; }; void (^markAndHandleSuccess)(TSAttachmentStream *attachmentStream) = ^(TSAttachmentStream *attachmentStream) { - // Ensure enclosing transaction is complete. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ successHandler(attachmentStream); - if (message) { - [message touch]; - } OWSAssertDebug(backgroundTask); backgroundTask = nil; @@ -263,7 +463,8 @@ static const CGFloat kAttachmentDownloadProgressTheta = 0.001f; } if (!plaintext) { - NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); + NSError *error = OWSErrorWithCodeDescription( + OWSErrorCodeFailedToDecryptMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); failureHandler(error); return; } @@ -278,7 +479,6 @@ static const CGFloat kAttachmentDownloadProgressTheta = 0.001f; return; } - [stream save]; successHandler(stream); } @@ -299,7 +499,7 @@ static const CGFloat kAttachmentDownloadProgressTheta = 0.001f; failure:(void (^)(NSURLSessionTask *_Nullable task, NSError *error))failureHandlerParam { AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; - manager.requestSerializer = [AFHTTPRequestSerializer serializer]; + manager.requestSerializer = [AFHTTPRequestSerializer serializer]; [manager.requestSerializer setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); @@ -451,31 +651,6 @@ static const CGFloat kAttachmentDownloadProgressTheta = 0.001f; }]; } -- (void)setAttachment:(TSAttachmentPointer *)pointer - isDownloadingInMessage:(nullable TSMessage *)message - transaction:(YapDatabaseReadWriteTransaction *)transaction -{ - OWSAssertDebug(transaction); - - pointer.state = TSAttachmentPointerStateDownloading; - [pointer saveWithTransaction:transaction]; - if (message) { - [message touchWithTransaction:transaction]; - } -} - -- (void)setAttachment:(TSAttachmentPointer *)pointer - didFailInMessage:(nullable TSMessage *)message - error:(NSError *)error -{ - pointer.mostRecentFailureLocalizedText = error.localizedDescription; - pointer.state = TSAttachmentPointerStateFailed; - [pointer save]; - if (message) { - [message touch]; - } -} - @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentPointer.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentPointer.m index 363e31f26..2af896187 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentPointer.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentPointer.m @@ -107,6 +107,9 @@ NS_ASSUME_NONNULL_BEGIN (NSArray *)attachmentProtos albumMessage:(TSMessage *)albumMessage { + OWSAssertDebug(attachmentProtos); + OWSAssertDebug(albumMessage); + NSMutableArray *attachmentPointers = [NSMutableArray new]; for (SSKProtoAttachmentPointer *attachmentProto in attachmentProtos) { TSAttachmentPointer *_Nullable attachmentPointer = diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m index b10f35b9f..e51184ecd 100644 --- a/SignalServiceKit/src/Messages/OWSMessageManager.m +++ b/SignalServiceKit/src/Messages/OWSMessageManager.m @@ -9,7 +9,7 @@ #import "MimeTypeUtil.h" #import "NSNotificationCenter+OWS.h" #import "NotificationsProtocol.h" -#import "OWSAttachmentsProcessor.h" +#import "OWSAttachmentDownloads.h" #import "OWSBlockingManager.h" #import "OWSCallMessageHandler.h" #import "OWSContact.h" @@ -164,6 +164,11 @@ NS_ASSUME_NONNULL_BEGIN return SSKEnvironment.shared.typingIndicators; } +- (OWSAttachmentDownloads *)attachmentDownloads +{ + return SSKEnvironment.shared.attachmentDownloads; +} + #pragma mark - - (void)startObserving @@ -729,13 +734,14 @@ NS_ASSUME_NONNULL_BEGIN return; } - TSAttachmentPointer *avatarPointer = + TSAttachmentPointer *_Nullable avatarPointer = [TSAttachmentPointer attachmentPointerFromProto:dataMessage.group.avatar albumMessage:nil]; - OWSAttachmentsProcessor *attachmentsProcessor = - [[OWSAttachmentsProcessor alloc] initWithAttachmentPointers:@[ avatarPointer ]]; - [attachmentsProcessor fetchAttachmentsForMessage:nil - transaction:transaction + if (!avatarPointer) { + OWSLogWarn(@"received unsupported group avatar envelope"); + return; + } + [self.attachmentDownloads downloadAttachmentPointer:avatarPointer success:^(NSArray *attachmentStreams) { OWSAssertDebug(attachmentStreams.count == 1); TSAttachmentStream *attachmentStream = attachmentStreams.firstObject; @@ -771,35 +777,26 @@ NS_ASSUME_NONNULL_BEGIN return; } - TSIncomingMessage *_Nullable createdMessage = [self handleReceivedEnvelope:envelope - withDataMessage:dataMessage - transaction:transaction]; - if (!createdMessage) { + TSIncomingMessage *_Nullable message = + [self handleReceivedEnvelope:envelope withDataMessage:dataMessage transaction:transaction]; + + if (!message) { return; } - NSArray *attachmentPointers = - [TSAttachmentPointer attachmentPointersFromProtos:dataMessage.attachments albumMessage:createdMessage]; - for (TSAttachmentPointer *pointer in attachmentPointers) { - [pointer saveWithTransaction:transaction]; - [createdMessage.attachmentIds addObject:pointer.uniqueId]; - } - [createdMessage saveWithTransaction:transaction]; + [message saveWithTransaction:transaction]; - OWSLogDebug(@"incoming attachment message: %@", createdMessage.debugDescription); + OWSLogDebug(@"incoming attachment message: %@", message.debugDescription); - OWSAttachmentsProcessor *attachmentsProcessor = - [[OWSAttachmentsProcessor alloc] initWithAttachmentPointers:attachmentPointers]; - - [attachmentsProcessor fetchAttachmentsForMessage:createdMessage + [self.attachmentDownloads downloadAttachmentsForMessage:message transaction:transaction success:^(NSArray *attachmentStreams) { OWSLogDebug(@"successfully fetched attachments: %lu for message: %@", (unsigned long)attachmentStreams.count, - createdMessage); + message); } failure:^(NSError *error) { - OWSLogError(@"failed to fetch attachments for message: %@ with error: %@", createdMessage, error); + OWSLogError(@"failed to fetch attachments for message: %@ with error: %@", message, error); }]; } @@ -1264,6 +1261,22 @@ NS_ASSUME_NONNULL_BEGIN serverTimestamp:serverTimestamp wasReceivedByUD:wasReceivedByUD]; + NSArray *attachmentPointers = + [TSAttachmentPointer attachmentPointersFromProtos:dataMessage.attachments + albumMessage:incomingMessage]; + for (TSAttachmentPointer *pointer in attachmentPointers) { + [pointer saveWithTransaction:transaction]; + [incomingMessage.attachmentIds addObject:pointer.uniqueId]; + } + + if (body.length == 0 && attachmentPointers.count < 1 && !contact) { + OWSLogWarn(@"ignoring empty incoming message from: %@ for group: %@ with timestamp: %lu", + envelopeAddress(envelope), + groupId, + (unsigned long)timestamp); + return nil; + } + [self finalizeIncomingMessage:incomingMessage thread:oldGroupThread envelope:envelope @@ -1298,6 +1311,20 @@ NS_ASSUME_NONNULL_BEGIN serverTimestamp:serverTimestamp wasReceivedByUD:wasReceivedByUD]; + NSArray *attachmentPointers = + [TSAttachmentPointer attachmentPointersFromProtos:dataMessage.attachments albumMessage:incomingMessage]; + for (TSAttachmentPointer *pointer in attachmentPointers) { + [pointer saveWithTransaction:transaction]; + [incomingMessage.attachmentIds addObject:pointer.uniqueId]; + } + + if (body.length == 0 && attachmentPointers.count < 1 && !contact) { + OWSLogWarn(@"ignoring empty incoming message from: %@ with timestamp: %lu", + envelopeAddress(envelope), + (unsigned long)timestamp); + return nil; + } + [self finalizeIncomingMessage:incomingMessage thread:thread envelope:envelope @@ -1344,16 +1371,12 @@ NS_ASSUME_NONNULL_BEGIN transaction:transaction]; if ([attachmentPointer isKindOfClass:[TSAttachmentPointer class]]) { - OWSAttachmentsProcessor *attachmentProcessor = - [[OWSAttachmentsProcessor alloc] initWithAttachmentPointers:@[ attachmentPointer ]]; - OWSLogDebug(@"downloading thumbnail for message: %lu", (unsigned long)incomingMessage.timestamp); - [attachmentProcessor fetchAttachmentsForMessage:incomingMessage - transaction:transaction + [self.attachmentDownloads downloadAttachmentPointer:attachmentPointer success:^(NSArray *attachmentStreams) { OWSAssertDebug(attachmentStreams.count == 1); TSAttachmentStream *attachmentStream = attachmentStreams.firstObject; - [self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [incomingMessage setQuotedMessageThumbnailAttachmentStream:attachmentStream]; [incomingMessage saveWithTransaction:transaction]; }]; @@ -1374,14 +1397,11 @@ NS_ASSUME_NONNULL_BEGIN if (![attachmentPointer isKindOfClass:[TSAttachmentPointer class]]) { OWSFailDebug(@"avatar attachmentPointer was unexpectedly nil"); } else { - OWSAttachmentsProcessor *attachmentProcessor = - [[OWSAttachmentsProcessor alloc] initWithAttachmentPointers:@[ attachmentPointer ]]; - OWSLogDebug(@"downloading contact avatar for message: %lu", (unsigned long)incomingMessage.timestamp); - [attachmentProcessor fetchAttachmentsForMessage:incomingMessage - transaction:transaction + + [self.attachmentDownloads downloadAttachmentPointer:attachmentPointer success:^(NSArray *attachmentStreams) { - [self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [incomingMessage touchWithTransaction:transaction]; }]; } diff --git a/SignalServiceKit/src/SSKEnvironment.h b/SignalServiceKit/src/SSKEnvironment.h index b516750cb..8dc5179d2 100644 --- a/SignalServiceKit/src/SSKEnvironment.h +++ b/SignalServiceKit/src/SSKEnvironment.h @@ -9,6 +9,7 @@ NS_ASSUME_NONNULL_BEGIN @class ContactDiscoveryService; @class ContactsUpdater; @class OWS2FAManager; +@class OWSAttachmentDownloads; @class OWSBatchMessageProcessor; @class OWSBlockingManager; @class OWSDisappearingMessagesJob; @@ -60,7 +61,8 @@ NS_ASSUME_NONNULL_BEGIN outgoingReceiptManager:(OWSOutgoingReceiptManager *)outgoingReceiptManager reachabilityManager:(id)reachabilityManager syncManager:(id)syncManager - typingIndicators:(id)typingIndicators NS_DESIGNATED_INITIALIZER; + typingIndicators:(id)typingIndicators + attachmentDownloads:(OWSAttachmentDownloads *)attachmentDownloads NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; @@ -97,6 +99,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) id syncManager; @property (nonatomic, readonly) id reachabilityManager; @property (nonatomic, readonly) id typingIndicators; +@property (nonatomic, readonly) OWSAttachmentDownloads *attachmentDownloads; // This property is configured after Environment is created. @property (atomic, nullable) id callMessageHandler; diff --git a/SignalServiceKit/src/SSKEnvironment.m b/SignalServiceKit/src/SSKEnvironment.m index 37de71b65..2b2000590 100644 --- a/SignalServiceKit/src/SSKEnvironment.m +++ b/SignalServiceKit/src/SSKEnvironment.m @@ -35,6 +35,7 @@ static SSKEnvironment *sharedSSKEnvironment; @property (nonatomic) id syncManager; @property (nonatomic) id reachabilityManager; @property (nonatomic) id typingIndicators; +@property (nonatomic) OWSAttachmentDownloads *attachmentDownloads; @end @@ -73,6 +74,7 @@ static SSKEnvironment *sharedSSKEnvironment; reachabilityManager:(id)reachabilityManager syncManager:(id)syncManager typingIndicators:(id)typingIndicators + attachmentDownloads:(OWSAttachmentDownloads *)attachmentDownloads { self = [super init]; if (!self) { @@ -103,6 +105,7 @@ static SSKEnvironment *sharedSSKEnvironment; OWSAssertDebug(syncManager); OWSAssertDebug(reachabilityManager); OWSAssertDebug(typingIndicators); + OWSAssertDebug(attachmentDownloads); _contactsManager = contactsManager; _messageSender = messageSender; @@ -128,6 +131,7 @@ static SSKEnvironment *sharedSSKEnvironment; _syncManager = syncManager; _reachabilityManager = reachabilityManager; _typingIndicators = typingIndicators; + _attachmentDownloads = attachmentDownloads; return self; } diff --git a/SignalServiceKit/src/TestUtils/MockSSKEnvironment.m b/SignalServiceKit/src/TestUtils/MockSSKEnvironment.m index 957610a72..65f5a5ce3 100644 --- a/SignalServiceKit/src/TestUtils/MockSSKEnvironment.m +++ b/SignalServiceKit/src/TestUtils/MockSSKEnvironment.m @@ -5,6 +5,7 @@ #import "MockSSKEnvironment.h" #import "ContactDiscoveryService.h" #import "OWS2FAManager.h" +#import "OWSAttachmentDownloads.h" #import "OWSBatchMessageProcessor.h" #import "OWSBlockingManager.h" #import "OWSDisappearingMessagesJob.h" @@ -76,6 +77,7 @@ NS_ASSUME_NONNULL_BEGIN id reachabilityManager = [SSKReachabilityManagerImpl new]; id syncManager = [[OWSMockSyncManager alloc] init]; id typingIndicators = [[OWSTypingIndicatorsImpl alloc] init]; + OWSAttachmentDownloads *attachmentDownloads = [[OWSAttachmentDownloads alloc] init]; self = [super initWithContactsManager:contactsManager messageSender:messageSender @@ -100,7 +102,8 @@ NS_ASSUME_NONNULL_BEGIN outgoingReceiptManager:outgoingReceiptManager reachabilityManager:reachabilityManager syncManager:syncManager - typingIndicators:typingIndicators]; + typingIndicators:typingIndicators + attachmentDownloads:attachmentDownloads]; if (!self) { return nil;