Add download progress indicators.

This commit is contained in:
Matthew Chen 2018-11-08 11:14:30 -05:00
parent a26086b303
commit 654325c6dc
7 changed files with 209 additions and 44 deletions

View File

@ -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 */,

View File

@ -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()
}
}

View File

@ -6,9 +6,19 @@ 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
@ -16,10 +26,12 @@ public class ConversationMediaView: UIView {
@objc
public required init(mediaCache: NSCache<NSString, AnyObject>,
attachment: TSAttachment,
isOutgoing: Bool) {
isOutgoing: Bool,
maxMessageWidth: CGFloat) {
self.mediaCache = mediaCache
self.attachment = attachment
self.isOutgoing = isOutgoing
self.maxMessageWidth = maxMessageWidth
super.init(frame: .zero)
@ -38,6 +50,7 @@ public class ConversationMediaView: UIView {
AssertIsOnMainThread()
guard let attachmentStream = attachment as? TSAttachmentStream else {
addDownloadProgressIfNecessary()
return
}
if attachmentStream.isAnimated {
@ -49,11 +62,39 @@ public class ConversationMediaView: UIView {
} else {
// TODO: Handle this case.
owsFailDebug("Attachment has unexpected type.")
configureForMissingOrInvalid()
}
}
private func addAttachmentUploadViewIfNecessary(_ subview: UIView,
completion: @escaping (Bool) -> Void) {
//
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 let progress = 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: @escaping ProgressCallback) {
guard isOutgoing else {
return
}
@ -63,7 +104,8 @@ public class ConversationMediaView: UIView {
guard !attachmentStream.isUploaded else {
return
}
let uploadView = AttachmentUploadView(attachment: attachmentStream) { (_) in
let uploadView = AttachmentUploadView(attachment: attachmentStream) { (isAttachmentReady) in
progressCallback(isAttachmentReady)
}
subview.addSubview(uploadView)
uploadView.autoPinEdgesToSuperviewEdges()
@ -85,7 +127,7 @@ public class ConversationMediaView: UIView {
animatedImageView.backgroundColor = Theme.offBackgroundColor
addSubview(animatedImageView)
animatedImageView.autoPinEdgesToSuperviewEdges()
addAttachmentUploadViewIfNecessary(animatedImageView) { (_) in
addUploadProgressIfNecessary(animatedImageView) { (_) in
}
loadBlock = { [weak self] in
guard let strongSelf = self else {
@ -134,7 +176,7 @@ public class ConversationMediaView: UIView {
stillImageView.backgroundColor = Theme.offBackgroundColor
addSubview(stillImageView)
stillImageView.autoPinEdgesToSuperviewEdges()
addAttachmentUploadViewIfNecessary(stillImageView) { (_) in
addUploadProgressIfNecessary(stillImageView) { (_) in
}
loadBlock = { [weak self] in
guard let strongSelf = self else {
@ -188,7 +230,7 @@ public class ConversationMediaView: UIView {
addSubview(stillImageView)
stillImageView.autoPinEdgesToSuperviewEdges()
addAttachmentUploadViewIfNecessary(stillImageView) { (isAttachmentReady) in
addUploadProgressIfNecessary(stillImageView) { (isAttachmentReady) in
videoPlayButton.isHidden = !isAttachmentReady
}
@ -210,8 +252,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
}
@ -222,6 +264,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? {

View File

@ -28,7 +28,8 @@ public class MediaAlbumCellView: UIStackView {
self.itemViews = MediaAlbumCellView.itemsToDisplay(forItems: items).map {
ConversationMediaView(mediaCache: mediaCache,
attachment: $0.attachment,
isOutgoing: isOutgoing)
isOutgoing: isOutgoing,
maxMessageWidth: maxMessageWidth)
}
super.init(frame: .zero)

View File

@ -804,9 +804,11 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
OWSAssertDebug(self.attachmentStream);
OWSAssertDebug([self.attachmentStream isVisualMedia]);
ConversationMediaView *mediaView = [[ConversationMediaView alloc] initWithMediaCache:self.cellMediaCache
attachment:self.attachmentStream
isOutgoing:self.isOutgoing];
ConversationMediaView *mediaView =
[[ConversationMediaView alloc] initWithMediaCache:self.cellMediaCache
attachment:self.attachmentStream
isOutgoing:self.isOutgoing
maxMessageWidth:self.conversationStyle.maxMessageWidth];
self.loadCellContentBlock = ^{
[mediaView loadMedia];
};

View File

@ -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.

View File

@ -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) {