diff --git a/Pods b/Pods index 40ba5afa32..cc810a1d99 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 40ba5afa3249a18770c2340896fa28fdb662d188 +Subproject commit cc810a1d99add579b5d8f099f06b543677bfeade diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index e85b8eae9c..b2e3f64472 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -283,6 +283,7 @@ 34E5DC8220D8050D00C08145 /* RegistrationUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E5DC8120D8050D00C08145 /* RegistrationUtils.m */; }; 34E88D262098C5AE00A608F4 /* ContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E88D252098C5AE00A608F4 /* ContactViewController.swift */; }; 34E8A8D12085238A00B272B1 /* ProtoParsingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E8A8D02085238900B272B1 /* ProtoParsingTest.m */; }; + 34EA69402194933900702471 /* AttachmentDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EA693F2194933900702471 /* AttachmentDownloadView.swift */; }; 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */; }; 34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */; }; 4503F1BE20470A5B00CEE724 /* classic-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */; }; @@ -981,6 +982,7 @@ 34E5DC8120D8050D00C08145 /* RegistrationUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RegistrationUtils.m; sourceTree = ""; }; 34E88D252098C5AE00A608F4 /* ContactViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactViewController.swift; sourceTree = ""; }; 34E8A8D02085238900B272B1 /* ProtoParsingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProtoParsingTest.m; sourceTree = ""; }; + 34EA693F2194933900702471 /* AttachmentDownloadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentDownloadView.swift; sourceTree = ""; }; 34F308A01ECB469700BB7697 /* OWSBezierPathView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBezierPathView.h; sourceTree = ""; }; 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBezierPathView.m; sourceTree = ""; }; 34FD936E1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAnyTouchGestureRecognizer.h; path = views/OWSAnyTouchGestureRecognizer.h; sourceTree = ""; }; @@ -1828,6 +1830,7 @@ 34D1F0951F867BFC0066283D /* Cells */ = { isa = PBXGroup; children = ( + 34EA693F2194933900702471 /* AttachmentDownloadView.swift */, 34D1F0BB1F8D108C0066283D /* AttachmentUploadView.h */, 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */, 3488F9352191CC4000E524CC /* ConversationMediaView.swift */, @@ -3419,6 +3422,7 @@ 458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */, 34B6A905218B4C91007C4606 /* TypingIndicatorInteraction.swift in Sources */, 4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */, + 34EA69402194933900702471 /* AttachmentDownloadView.swift in Sources */, 340FC8AB204DAC8D007AEB0F /* DomainFrontingCountryViewController.m in Sources */, 3496744D2076768700080B5F /* OWSMessageBubbleView.m in Sources */, 34B3F8751E8DF1700035BE1A /* CallViewController.swift in Sources */, diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 3507d9b26d..6934076138 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -7,6 +7,7 @@ // Separate iOS Frameworks from other imports. #import "AppSettingsViewController.h" +#import "AttachmentUploadView.h" #import "ContactCellView.h" #import "ContactTableViewCell.h" #import "ConversationViewCell.h" diff --git a/Signal/src/ViewControllers/ColorPickerViewController.swift b/Signal/src/ViewControllers/ColorPickerViewController.swift index d7853e43ea..c18fdd48f2 100644 --- a/Signal/src/ViewControllers/ColorPickerViewController.swift +++ b/Signal/src/ViewControllers/ColorPickerViewController.swift @@ -429,7 +429,7 @@ private class MockConversationViewItem: NSObject, ConversationViewItem { return 2 } - func firstValidGalleryAttachment() -> TSAttachmentStream? { + func firstValidAlbumAttachment() -> TSAttachmentStream? { owsFailDebug("unexpected invocation") return nil } diff --git a/Signal/src/ViewControllers/ConversationView/Cells/AttachmentDownloadView.swift b/Signal/src/ViewControllers/ConversationView/Cells/AttachmentDownloadView.swift new file mode 100644 index 0000000000..1915d69392 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/AttachmentDownloadView.swift @@ -0,0 +1,84 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc +public class AttachmentDownloadView: UIView { + + // MARK: - Dependencies + + private var attachmentDownloads: OWSAttachmentDownloads { + return SSKEnvironment.shared.attachmentDownloads + } + + // MARK: - + + private let attachmentId: String + private let radius: CGFloat + private let shapeLayer = CAShapeLayer() + + @objc + public required init(attachmentId: String, radius: CGFloat) { + self.attachmentId = attachmentId + self.radius = radius + + super.init(frame: .zero) + + layer.addSublayer(shapeLayer) + } + + @available(*, unavailable, message: "use other init() instead.") + required public init?(coder aDecoder: NSCoder) { + notImplemented() + } + + @objc public override var bounds: CGRect { + didSet { + if oldValue != bounds { + updateLayers() + } + } + } + + @objc public override var frame: CGRect { + didSet { + if oldValue != frame { + updateLayers() + } + } + } + + internal func updateLayers() { + AssertIsOnMainThread() + + self.shapeLayer.frame = self.bounds + + guard let progress = attachmentDownloads.downloadProgress(forAttachmentId: attachmentId) else { + Logger.warn("No progress for attachment.") + shapeLayer.path = nil + return + } + + // Prevent the shape layer from animating changes. + CATransaction.begin() + CATransaction.setDisableActions(true) + + let center = CGPoint(x: self.bounds.width * 0.5, + y: self.bounds.height * 0.5) + let outerRadius: CGFloat = radius * 1.0 + let innerRadius: CGFloat = radius * 0.9 + let startAngle: CGFloat = CGFloat.pi * 1.5 + let endAngle: CGFloat = CGFloat.pi * (1.5 + 2 * CGFloat(progress.floatValue)) + + let bezierPath = UIBezierPath() + bezierPath.addArc(withCenter: center, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true) + bezierPath.addArc(withCenter: center, radius: innerRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false) + + shapeLayer.path = bezierPath.cgPath + shapeLayer.fillColor = UIColor.ows_gray60.cgColor + + CATransaction.commit() + } +} diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift b/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift index 14325da727..aa2ffeb8eb 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift @@ -6,17 +6,32 @@ import Foundation @objc public class ConversationMediaView: UIView { + + // MARK: - Dependencies + + private var attachmentDownloads: OWSAttachmentDownloads { + return SSKEnvironment.shared.attachmentDownloads + } + + // MARK: - + private let mediaCache: NSCache private let attachment: TSAttachment + private let isOutgoing: Bool + private let maxMessageWidth: CGFloat private var loadBlock : (() -> Void)? private var unloadBlock : (() -> Void)? private var didFailToLoad = false @objc public required init(mediaCache: NSCache, - attachment: TSAttachment) { + attachment: TSAttachment, + isOutgoing: Bool, + maxMessageWidth: CGFloat) { self.mediaCache = mediaCache self.attachment = attachment + self.isOutgoing = isOutgoing + self.maxMessageWidth = maxMessageWidth super.init(frame: .zero) @@ -35,8 +50,7 @@ public class ConversationMediaView: UIView { AssertIsOnMainThread() guard let attachmentStream = attachment as? TSAttachmentStream else { - // TODO: Handle this case. - owsFailDebug("Missing attachment stream.") + addDownloadProgressIfNecessary() return } if attachmentStream.isAnimated { @@ -48,13 +62,55 @@ public class ConversationMediaView: UIView { } else { // TODO: Handle this case. owsFailDebug("Attachment has unexpected type.") + configureForMissingOrInvalid() } } - private func addMediaSubview(_ subview: UIView) { - addSubview(subview) - subview.autoPinEdgesToSuperviewEdges() - // TODO: Possibly add upload/download indicator here. + // + typealias ProgressCallback = (Bool) -> Void + + private func addDownloadProgressIfNecessary() { + guard let attachmentPointer = attachment as? TSAttachmentPointer else { + owsFailDebug("Attachment has unexpected type.") + configureForMissingOrInvalid() + return + } + guard let attachmentId = attachmentPointer.uniqueId else { + owsFailDebug("Attachment stream missing unique ID.") + configureForMissingOrInvalid() + return + } + + guard nil != attachmentDownloads.downloadProgress(forAttachmentId: attachmentId) else { + // Not being downloaded. + configureForMissingOrInvalid() + return + } + + backgroundColor = UIColor.ows_gray05 + let progressView = AttachmentDownloadView(attachmentId: attachmentId, radius: maxMessageWidth * 0.1) + self.addSubview(progressView) + progressView.autoPinEdgesToSuperviewEdges() + } + + private func addUploadProgressIfNecessary(_ subview: UIView, + progressCallback: ProgressCallback? = nil) { + guard isOutgoing else { + return + } + guard let attachmentStream = attachment as? TSAttachmentStream else { + return + } + guard !attachmentStream.isUploaded else { + return + } + let uploadView = AttachmentUploadView(attachment: attachmentStream) { (isAttachmentReady) in + if let progressCallback = progressCallback { + progressCallback(isAttachmentReady) + } + } + subview.addSubview(uploadView) + uploadView.autoPinEdgesToSuperviewEdges() } private func configureForAnimatedImage(attachmentStream: TSAttachmentStream) { @@ -71,7 +127,10 @@ public class ConversationMediaView: UIView { animatedImageView.layer.minificationFilter = kCAFilterTrilinear animatedImageView.layer.magnificationFilter = kCAFilterTrilinear animatedImageView.backgroundColor = Theme.offBackgroundColor - addMediaSubview(animatedImageView) + addSubview(animatedImageView) + animatedImageView.autoPinEdgesToSuperviewEdges() + addUploadProgressIfNecessary(animatedImageView) + loadBlock = { [weak self] in guard let strongSelf = self else { return @@ -91,8 +150,8 @@ public class ConversationMediaView: UIView { let animatedImage = YYImage(contentsOfFile: filePath) return animatedImage }, - cacheKey: cacheKey, - canLoadAsync: true) + cacheKey: cacheKey, + canLoadAsync: true) guard let image = cachedValue as? YYImage else { return } @@ -117,7 +176,9 @@ public class ConversationMediaView: UIView { stillImageView.layer.minificationFilter = kCAFilterTrilinear stillImageView.layer.magnificationFilter = kCAFilterTrilinear stillImageView.backgroundColor = Theme.offBackgroundColor - addMediaSubview(stillImageView) + addSubview(stillImageView) + stillImageView.autoPinEdgesToSuperviewEdges() + addUploadProgressIfNecessary(stillImageView) loadBlock = { [weak self] in guard let strongSelf = self else { return @@ -136,8 +197,8 @@ public class ConversationMediaView: UIView { Logger.error("Could not load thumbnail") }) }, - cacheKey: cacheKey, - canLoadAsync: true) + cacheKey: cacheKey, + canLoadAsync: true) guard let image = cachedValue as? UIImage else { return } @@ -162,14 +223,18 @@ public class ConversationMediaView: UIView { stillImageView.layer.minificationFilter = kCAFilterTrilinear stillImageView.layer.magnificationFilter = kCAFilterTrilinear stillImageView.backgroundColor = Theme.offBackgroundColor - addMediaSubview(stillImageView) - // TODO: Hide during upload/download. let videoPlayIcon = UIImage(named: "play_button") let videoPlayButton = UIImageView(image: videoPlayIcon) stillImageView.addSubview(videoPlayButton) videoPlayButton.autoCenterInSuperview() + addSubview(stillImageView) + stillImageView.autoPinEdgesToSuperviewEdges() + addUploadProgressIfNecessary(stillImageView) { (isAttachmentReady) in + videoPlayButton.isHidden = !isAttachmentReady + } + loadBlock = { [weak self] in guard let strongSelf = self else { return @@ -188,8 +253,8 @@ public class ConversationMediaView: UIView { Logger.error("Could not load thumbnail") }) }, - cacheKey: cacheKey, - canLoadAsync: true) + cacheKey: cacheKey, + canLoadAsync: true) guard let image = cachedValue as? UIImage else { return } @@ -200,6 +265,12 @@ public class ConversationMediaView: UIView { } } + private func configureForMissingOrInvalid() { + // TODO: Get final value from design. + backgroundColor = UIColor.ows_gray45 + // TODO: Add error icon. + } + private func tryToLoadMedia(loadMediaBlock: @escaping () -> AnyObject?, cacheKey: String, canLoadAsync: Bool) -> AnyObject? { diff --git a/Signal/src/ViewControllers/ConversationView/Cells/MediaAlbumCellView.swift b/Signal/src/ViewControllers/ConversationView/Cells/MediaAlbumCellView.swift index dca34997d0..a107f1d46f 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/MediaAlbumCellView.swift +++ b/Signal/src/ViewControllers/ConversationView/Cells/MediaAlbumCellView.swift @@ -22,11 +22,14 @@ public class MediaAlbumCellView: UIStackView { @objc public required init(mediaCache: NSCache, items: [ConversationMediaAlbumItem], + isOutgoing: Bool, maxMessageWidth: CGFloat) { self.items = items self.itemViews = MediaAlbumCellView.itemsToDisplay(forItems: items).map { ConversationMediaView(mediaCache: mediaCache, - attachment: $0.attachment) + attachment: $0.attachment, + isOutgoing: isOutgoing, + maxMessageWidth: maxMessageWidth) } super.init(frame: .zero) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index fbbfeddc70..fd4eff6b63 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -241,7 +241,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes case OWSMessageCellType_ContactShare: return NO; case OWSMessageCellType_MediaAlbum: - // Is there a gallery title? + // Is there an album title? return self.hasBodyText; } } @@ -787,6 +787,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes OWSMediaAlbumCellView *albumView = [[OWSMediaAlbumCellView alloc] initWithMediaCache:self.cellMediaCache items:self.viewItem.mediaAlbumItems + isOutgoing:self.isOutgoing maxMessageWidth:self.conversationStyle.maxMessageWidth]; self.loadCellContentBlock = ^{ [albumView loadMedia]; @@ -804,7 +805,10 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes OWSAssertDebug([self.attachmentStream isVisualMedia]); ConversationMediaView *mediaView = - [[ConversationMediaView alloc] initWithMediaCache:self.cellMediaCache attachment:self.attachmentStream]; + [[ConversationMediaView alloc] initWithMediaCache:self.cellMediaCache + attachment:self.attachmentStream + isOutgoing:self.isOutgoing + maxMessageWidth:self.conversationStyle.maxMessageWidth]; self.loadCellContentBlock = ^{ [mediaView loadMedia]; }; @@ -1414,9 +1418,9 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes OWSAssertDebug(self.viewItem.mediaAlbumItems.count > 0); // For now, use first valid attachment. - TSAttachmentStream *_Nullable attachmentStream = self.viewItem.firstValidGalleryAttachment; + TSAttachmentStream *_Nullable attachmentStream = self.viewItem.firstValidAlbumAttachment; if (!attachmentStream) { - OWSLogInfo(@"Ignoring tap on gallery without any valid attachments."); + OWSLogInfo(@"Ignoring tap on album without any valid attachments."); return; } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index b5ec951714..1004ee1bc3 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -144,7 +144,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); // For other view views (like the typing indicator), this is a unique, stable string. - (NSString *)itemId; -- (nullable TSAttachmentStream *)firstValidGalleryAttachment; +- (nullable TSAttachmentStream *)firstValidAlbumAttachment; @end diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index d63d294aa2..a1bcefc85e 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -409,7 +409,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } } -- (nullable TSAttachmentStream *)firstValidGalleryAttachment +- (nullable TSAttachmentStream *)firstValidAlbumAttachment { OWSAssertDebug(self.mediaAlbumItems.count > 0); @@ -575,9 +575,9 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) NSArray *mediaAlbumItems = [self mediaAlbumItemsForAttachments:attachments]; self.mediaAlbumItems = mediaAlbumItems; self.messageCellType = OWSMessageCellType_MediaAlbum; - NSString *_Nullable galleryTitle = [message bodyTextWithTransaction:transaction]; - if (galleryTitle) { - self.displayableBodyText = [self displayableBodyTextForText:galleryTitle interactionId:message.uniqueId]; + NSString *_Nullable albumTitle = [message bodyTextWithTransaction:transaction]; + if (albumTitle) { + self.displayableBodyText = [self displayableBodyTextForText:albumTitle interactionId:message.uniqueId]; } return; } @@ -899,7 +899,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) break; } case OWSMessageCellType_MediaAlbum: { - OWSFailDebug(@"Can't copy media gallery"); + OWSFailDebug(@"Can't copy media album"); break; } } @@ -952,7 +952,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } } if (attachmentStreams.count < 1) { - OWSFailDebug(@"Can't share media gallery; no valid items."); + OWSFailDebug(@"Can't share media album; no valid items."); return; } [AttachmentSharing showShareUIForAttachments:attachmentStreams completion:nil]; @@ -1126,7 +1126,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } case OWSMessageCellType_MediaAlbum: // TODO: I suspect we need separate "can save media", "can share media", etc. methods. - return self.firstValidGalleryAttachment != nil; + return self.firstValidAlbumAttachment != nil; } } diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.h b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.h index e08e60c82b..1701e9296f 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.h +++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.h @@ -24,6 +24,8 @@ extern NSString *const kAttachmentDownloadAttachmentIDKey; */ @interface OWSAttachmentDownloads : NSObject +- (nullable NSNumber *)downloadProgressForAttachmentId:(NSString *)attachmentId; + // 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. diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m index 9014dd2caa..a5d3d3a94a 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m +++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m @@ -45,6 +45,7 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); @property (nonatomic, readonly, nullable) TSMessage *message; @property (nonatomic, readonly) AttachmentDownloadSuccess success; @property (nonatomic, readonly) AttachmentDownloadFailure failure; +@property (atomic) CGFloat progress; @end @@ -77,7 +78,7 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); @interface OWSAttachmentDownloads () // This property should only be accessed while synchronized on this class. -@property (nonatomic, readonly) NSMutableSet *downloadingAttachmentIdSet; +@property (nonatomic, readonly) NSMutableDictionary *downloadingJobMap; // This property should only be accessed while synchronized on this class. @property (nonatomic, readonly) NSMutableArray *attachmentDownloadJobQueue; @@ -109,7 +110,7 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); return self; } - _downloadingAttachmentIdSet = [NSMutableSet new]; + _downloadingJobMap = [NSMutableDictionary new]; _attachmentDownloadJobQueue = [NSMutableArray new]; return self; @@ -117,6 +118,18 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); #pragma mark - +- (nullable NSNumber *)downloadProgressForAttachmentId:(NSString *)attachmentId +{ + + @synchronized(self) { + OWSAttachmentDownloadJob *_Nullable job = self.downloadingJobMap[attachmentId]; + if (!job) { + return nil; + } + return @(job.progress); + } +} + - (void)downloadAttachmentsForMessage:(TSMessage *)message transaction:(YapDatabaseReadTransaction *)transaction success:(void (^)(NSArray *attachmentStreams))success @@ -253,20 +266,20 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); @synchronized(self) { const NSUInteger kMaxSimultaneousDownloads = 4; - if (self.downloadingAttachmentIdSet.count >= kMaxSimultaneousDownloads) { + if (self.downloadingJobMap.count >= kMaxSimultaneousDownloads) { return; } job = self.attachmentDownloadJobQueue.firstObject; if (!job) { return; } - if ([self.downloadingAttachmentIdSet containsObject:job.attachmentPointer.uniqueId]) { + if (self.downloadingJobMap[job.attachmentPointer.uniqueId] != nil) { // 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.downloadingJobMap[job.attachmentPointer.uniqueId] = job; } [self.primaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { @@ -278,7 +291,7 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); } }]; - [self retrieveAttachment:job.attachmentPointer + [self retrieveAttachmentForJob:job success:^(TSAttachmentStream *attachmentStream) { OWSLogVerbose(@"Attachment download succeeded."); @@ -294,7 +307,7 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); job.success(attachmentStream); @synchronized(self) { - [self.downloadingAttachmentIdSet removeObject:job.attachmentPointer.uniqueId]; + [self.downloadingJobMap removeObjectForKey:job.attachmentPointer.uniqueId]; } [self startDownloadIfPossible]; @@ -314,7 +327,7 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); }]; @synchronized(self) { - [self.downloadingAttachmentIdSet removeObject:job.attachmentPointer.uniqueId]; + [self.downloadingJobMap removeObjectForKey:job.attachmentPointer.uniqueId]; } job.failure(error); @@ -326,11 +339,12 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); #pragma mark - -- (void)retrieveAttachment:(TSAttachmentPointer *)attachment - success:(void (^)(TSAttachmentStream *attachmentStream))successHandler - failure:(void (^)(NSError *error))failureHandler +- (void)retrieveAttachmentForJob:(OWSAttachmentDownloadJob *)job + success:(void (^)(TSAttachmentStream *attachmentStream))successHandler + failure:(void (^)(NSError *error))failureHandler { - OWSAssertDebug(attachment); + OWSAssertDebug(job); + TSAttachmentPointer *attachmentPointer = job.attachmentPointer; __block OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; @@ -353,10 +367,10 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); }); }; - if (attachment.serverId < 100) { - OWSLogError(@"Suspicious attachment id: %llu", (unsigned long long)attachment.serverId); + if (attachmentPointer.serverId < 100) { + OWSLogError(@"Suspicious attachment id: %llu", (unsigned long long)attachmentPointer.serverId); } - TSRequest *request = [OWSRequestFactory attachmentRequestWithAttachmentId:attachment.serverId]; + TSRequest *request = [OWSRequestFactory attachmentRequestWithAttachmentId:attachmentPointer.serverId]; [self.networkManager makeRequest:request success:^(NSURLSessionDataTask *task, id responseObject) { @@ -374,22 +388,22 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); dispatch_async([OWSDispatch attachmentsQueue], ^{ [self downloadFromLocation:location - pointer:attachment + job:job success:^(NSString *encryptedDataFilePath) { [self decryptAttachmentPath:encryptedDataFilePath - pointer:attachment + attachmentPointer:attachmentPointer success:markAndHandleSuccess failure:markAndHandleFailure]; } failure:^(NSURLSessionTask *_Nullable task, NSError *error) { - if (attachment.serverId < 100) { + if (attachmentPointer.serverId < 100) { // This looks like the symptom of the "frequent 404 // downloading attachments with low server ids". NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response; NSInteger statusCode = [httpResponse statusCode]; OWSFailDebug(@"%d Failure with suspicious attachment id: %llu, %@", (int)statusCode, - (unsigned long long)attachment.serverId, + (unsigned long long)attachmentPointer.serverId, error); } markAndHandleFailure(error); @@ -401,14 +415,14 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); OWSProdError([OWSAnalyticsEvents errorAttachmentRequestFailed]); } OWSLogError(@"Failed retrieval of attachment with error: %@", error); - if (attachment.serverId < 100) { + if (attachmentPointer.serverId < 100) { // This _shouldn't_ be the symptom of the "frequent 404 // downloading attachments with low server ids". NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response; NSInteger statusCode = [httpResponse statusCode]; OWSFailDebug(@"%d Failure with suspicious attachment id: %llu, %@", (int)statusCode, - (unsigned long long)attachment.serverId, + (unsigned long long)attachmentPointer.serverId, error); } return markAndHandleFailure(error); @@ -416,12 +430,12 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); } - (void)decryptAttachmentPath:(NSString *)encryptedDataFilePath - pointer:(TSAttachmentPointer *)attachment + attachmentPointer:(TSAttachmentPointer *)attachmentPointer success:(void (^)(TSAttachmentStream *attachmentStream))success failure:(void (^)(NSError *error))failure { OWSAssertDebug(encryptedDataFilePath.length > 0); - OWSAssertDebug(attachment); + OWSAssertDebug(attachmentPointer); // Use attachmentDecryptSerialQueue to ensure that we only load into memory // & decrypt a single attachment at a time. @@ -435,7 +449,10 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); return failure(error); } - [self decryptAttachmentData:encryptedData pointer:attachment success:success failure:failure]; + [self decryptAttachmentData:encryptedData + attachmentPointer:attachmentPointer + success:success + failure:failure]; if (![OWSFileSystem deleteFile:encryptedDataFilePath]) { OWSLogError(@"Could not delete temporary file."); @@ -445,15 +462,17 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); } - (void)decryptAttachmentData:(NSData *)cipherText - pointer:(TSAttachmentPointer *)attachment + attachmentPointer:(TSAttachmentPointer *)attachmentPointer success:(void (^)(TSAttachmentStream *attachmentStream))successHandler failure:(void (^)(NSError *error))failureHandler { + OWSAssertDebug(attachmentPointer); + NSError *decryptError; NSData *_Nullable plaintext = [Cryptography decryptAttachment:cipherText - withKey:attachment.encryptionKey - digest:attachment.digest - unpaddedSize:attachment.byteCount + withKey:attachmentPointer.encryptionKey + digest:attachmentPointer.digest + unpaddedSize:attachmentPointer.byteCount error:&decryptError]; if (decryptError) { @@ -469,7 +488,7 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); return; } - TSAttachmentStream *stream = [[TSAttachmentStream alloc] initWithPointer:attachment]; + TSAttachmentStream *stream = [[TSAttachmentStream alloc] initWithPointer:attachmentPointer]; NSError *writeError; [stream writeData:plaintext error:&writeError]; @@ -494,10 +513,13 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); } - (void)downloadFromLocation:(NSString *)location - pointer:(TSAttachmentPointer *)pointer + job:(OWSAttachmentDownloadJob *)job success:(void (^)(NSString *encryptedDataPath))successHandler failure:(void (^)(NSURLSessionTask *_Nullable task, NSError *error))failureHandlerParam { + OWSAssertDebug(job); + TSAttachmentPointer *attachmentPointer = job.attachmentPointer; + AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.requestSerializer = [AFHTTPRequestSerializer serializer]; [manager.requestSerializer setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"]; @@ -560,8 +582,10 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); return; } + job.progress = progress.fractionCompleted; + [self fireProgressNotification:MAX(kAttachmentDownloadProgressTheta, progress.fractionCompleted) - attachmentId:pointer.uniqueId]; + attachmentId:attachmentPointer.uniqueId]; // We only need to check the content length header once. if (hasCheckedContentLength) {