Merge branch 'charlesmchen/albumProgress'
This commit is contained in:
commit
1b49fa294f
2
Pods
2
Pods
@ -1 +1 @@
|
||||
Subproject commit 40ba5afa3249a18770c2340896fa28fdb662d188
|
||||
Subproject commit cc810a1d99add579b5d8f099f06b543677bfeade
|
||||
@ -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 = "<group>"; };
|
||||
34E88D252098C5AE00A608F4 /* ContactViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactViewController.swift; sourceTree = "<group>"; };
|
||||
34E8A8D02085238900B272B1 /* ProtoParsingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProtoParsingTest.m; sourceTree = "<group>"; };
|
||||
34EA693F2194933900702471 /* AttachmentDownloadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentDownloadView.swift; sourceTree = "<group>"; };
|
||||
34F308A01ECB469700BB7697 /* OWSBezierPathView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBezierPathView.h; sourceTree = "<group>"; };
|
||||
34F308A11ECB469700BB7697 /* OWSBezierPathView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBezierPathView.m; sourceTree = "<group>"; };
|
||||
34FD936E1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAnyTouchGestureRecognizer.h; path = views/OWSAnyTouchGestureRecognizer.h; sourceTree = "<group>"; };
|
||||
@ -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 */,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -429,7 +429,7 @@ private class MockConversationViewItem: NSObject, ConversationViewItem {
|
||||
return 2
|
||||
}
|
||||
|
||||
func firstValidGalleryAttachment() -> TSAttachmentStream? {
|
||||
func firstValidAlbumAttachment() -> TSAttachmentStream? {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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<NSString, AnyObject>
|
||||
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<NSString, AnyObject>,
|
||||
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? {
|
||||
|
||||
@ -22,11 +22,14 @@ public class MediaAlbumCellView: UIStackView {
|
||||
@objc
|
||||
public required init(mediaCache: NSCache<NSString, AnyObject>,
|
||||
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)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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<ConversationMediaAlbumItem *> *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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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<NSString *> *downloadingAttachmentIdSet;
|
||||
@property (nonatomic, readonly) NSMutableDictionary<NSString *, OWSAttachmentDownloadJob *> *downloadingJobMap;
|
||||
// This property should only be accessed while synchronized on this class.
|
||||
@property (nonatomic, readonly) NSMutableArray<OWSAttachmentDownloadJob *> *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<TSAttachmentStream *> *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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user