session-ios/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m

894 lines
30 KiB
Mathematica
Raw Normal View History

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
2015-12-07 03:31:43 +01:00
#import "TSAttachmentStream.h"
#import "MIMETypeUtil.h"
2017-08-30 15:58:02 +02:00
#import "NSData+Image.h"
2017-11-16 16:12:47 +01:00
#import "OWSFileSystem.h"
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
#import "TSAttachmentPointer.h"
#import <AVFoundation/AVFoundation.h>
2018-09-21 21:41:10 +02:00
#import <SignalCoreKit/Threading.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
#import <YapDatabase/YapDatabase.h>
2015-12-07 03:31:43 +01:00
NS_ASSUME_NONNULL_BEGIN
2018-09-05 17:44:51 +02:00
const NSUInteger kThumbnailDimensionPointsSmall = 200;
const NSUInteger kThumbnailDimensionPointsMedium = 450;
2018-09-05 19:47:37 +02:00
// This size is large enough to render full screen.
const NSUInteger ThumbnailDimensionPointsLarge()
{
CGSize screenSizePoints = UIScreen.mainScreen.bounds.size;
const CGFloat kMinZoomFactor = 2.f;
2018-09-05 21:15:10 +02:00
return MAX(screenSizePoints.width, screenSizePoints.height) * kMinZoomFactor;
2018-09-05 19:47:37 +02:00
}
2018-09-04 21:21:48 +02:00
typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail);
2018-08-31 02:59:26 +02:00
@interface TSAttachmentStream ()
// We only want to generate the file path for this attachment once, so that
// changes in the file path generation logic don't break existing attachments.
@property (nullable, nonatomic) NSString *localRelativeFilePath;
2018-02-02 16:56:16 +01:00
// These properties should only be accessed while synchronized on self.
@property (nullable, nonatomic) NSNumber *cachedImageWidth;
@property (nullable, nonatomic) NSNumber *cachedImageHeight;
2018-02-02 16:56:16 +01:00
// This property should only be accessed on the main thread.
@property (nullable, nonatomic) NSNumber *cachedAudioDurationSeconds;
@property (atomic, nullable) NSNumber *isValidImageCached;
@property (atomic, nullable) NSNumber *isValidVideoCached;
@end
#pragma mark -
2015-12-07 03:31:43 +01:00
@implementation TSAttachmentStream
- (instancetype)initWithContentType:(NSString *)contentType
byteCount:(UInt32)byteCount
sourceFilename:(nullable NSString *)sourceFilename
caption:(nullable NSString *)caption
albumMessageId:(nullable NSString *)albumMessageId
{
self = [super initWithContentType:contentType
byteCount:byteCount
sourceFilename:sourceFilename
caption:caption
albumMessageId:albumMessageId];
if (!self) {
return self;
}
2015-12-07 03:31:43 +01:00
self.isDownloaded = YES;
// TSAttachmentStream doesn't have any "incoming vs. outgoing"
// state, but this constructor is used only for new outgoing
// attachments which haven't been uploaded yet.
_isUploaded = NO;
_creationTimestamp = [NSDate new];
2017-06-21 15:58:19 +02:00
[self ensureFilePath];
2017-05-17 18:44:05 +02:00
2015-12-07 03:31:43 +01:00
return self;
}
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
- (instancetype)initWithPointer:(TSAttachmentPointer *)pointer
{
// Once saved, this AttachmentStream will replace the AttachmentPointer in the attachments collection.
self = [super initWithPointer:pointer];
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
if (!self) {
return self;
}
_contentType = pointer.contentType;
self.isDownloaded = YES;
// TSAttachmentStream doesn't have any "incoming vs. outgoing"
// state, but this constructor is used only for new incoming
// attachments which don't need to be uploaded.
_isUploaded = YES;
self.attachmentType = pointer.attachmentType;
_creationTimestamp = [NSDate new];
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
2017-06-21 15:58:19 +02:00
[self ensureFilePath];
2017-05-17 18:44:05 +02:00
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (!self) {
return self;
}
// OWS105AttachmentFilePaths will ensure the file path is saved if necessary.
2017-06-21 15:58:19 +02:00
[self ensureFilePath];
2017-05-17 18:44:05 +02:00
// OWS105AttachmentFilePaths will ensure the creation timestamp is saved if necessary.
if (!_creationTimestamp) {
_creationTimestamp = [NSDate new];
}
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
return self;
}
- (void)upgradeFromAttachmentSchemaVersion:(NSUInteger)attachmentSchemaVersion
{
[super upgradeFromAttachmentSchemaVersion:attachmentSchemaVersion];
if (attachmentSchemaVersion < 3) {
// We want to treat any legacy TSAttachmentStream as though
// they have already been uploaded. If it needs to be reuploaded,
// the OWSUploadingService will update this progress when the
// upload begins.
self.isUploaded = YES;
}
2017-10-10 22:13:54 +02:00
if (attachmentSchemaVersion < 4) {
// Legacy image sizes don't correctly reflect image orientation.
2018-02-02 16:56:16 +01:00
@synchronized(self)
{
self.cachedImageWidth = nil;
self.cachedImageHeight = nil;
}
2017-10-10 22:13:54 +02:00
}
}
2017-06-21 15:58:19 +02:00
- (void)ensureFilePath
2017-05-17 18:44:05 +02:00
{
if (self.localRelativeFilePath) {
return;
}
NSString *attachmentsFolder = [[self class] attachmentsFolder];
2017-05-19 23:33:17 +02:00
NSString *filePath = [MIMETypeUtil filePathForAttachment:self.uniqueId
ofMIMEType:self.contentType
sourceFilename:self.sourceFilename
inFolder:attachmentsFolder];
if (!filePath) {
OWSFailDebug(@"Could not generate path for attachment.");
2017-05-17 18:44:05 +02:00
return;
}
2017-05-19 23:33:17 +02:00
if (![filePath hasPrefix:attachmentsFolder]) {
OWSFailDebug(@"Attachment paths should all be in the attachments folder.");
2017-05-17 18:44:05 +02:00
return;
}
2017-05-19 23:33:17 +02:00
NSString *localRelativeFilePath = [filePath substringFromIndex:attachmentsFolder.length];
2017-05-17 18:44:05 +02:00
if (localRelativeFilePath.length < 1) {
OWSFailDebug(@"Empty local relative attachment paths.");
2017-05-17 18:44:05 +02:00
return;
}
self.localRelativeFilePath = localRelativeFilePath;
OWSAssertDebug(self.originalFilePath);
}
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
#pragma mark - File Management
- (nullable NSData *)readDataFromFileWithError:(NSError **)error
{
2017-05-17 18:44:05 +02:00
*error = nil;
2018-09-04 16:25:42 +02:00
NSString *_Nullable filePath = self.originalFilePath;
2017-05-19 23:33:17 +02:00
if (!filePath) {
OWSFailDebug(@"Missing path for attachment.");
2017-05-17 18:44:05 +02:00
return nil;
}
2017-05-19 23:33:17 +02:00
return [NSData dataWithContentsOfFile:filePath options:0 error:error];
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
}
2015-12-07 03:31:43 +01:00
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
- (BOOL)writeData:(NSData *)data error:(NSError **)error
{
OWSAssertDebug(data);
2017-05-17 18:44:05 +02:00
*error = nil;
2018-09-04 16:25:42 +02:00
NSString *_Nullable filePath = self.originalFilePath;
2017-05-19 23:33:17 +02:00
if (!filePath) {
OWSFailDebug(@"Missing path for attachment.");
2017-05-17 18:44:05 +02:00
return NO;
}
2018-12-07 01:12:16 +01:00
OWSLogDebug(@"Writing attachment to file: %@", filePath);
2017-05-19 23:33:17 +02:00
return [data writeToFile:filePath options:0 error:error];
2015-12-07 03:31:43 +01:00
}
2017-09-18 21:02:34 +02:00
- (BOOL)writeDataSource:(DataSource *)dataSource
{
OWSAssertDebug(dataSource);
2018-09-04 16:25:42 +02:00
NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) {
OWSFailDebug(@"Missing path for attachment.");
return NO;
}
2018-12-07 01:12:16 +01:00
OWSLogDebug(@"Writing attachment to file: %@", filePath);
return [dataSource writeToPath:filePath];
}
2017-11-30 16:02:04 +01:00
+ (NSString *)legacyAttachmentsDirPath
2017-11-28 19:46:26 +01:00
{
return [[OWSFileSystem appDocumentDirectoryPath] stringByAppendingPathComponent:@"Attachments"];
}
2017-11-30 16:02:04 +01:00
+ (NSString *)sharedDataAttachmentsDirPath
2017-11-28 19:46:26 +01:00
{
return [[OWSFileSystem appSharedDataDirectoryPath] stringByAppendingPathComponent:@"Attachments"];
}
+ (nullable NSError *)migrateToSharedData
2017-11-28 19:46:26 +01:00
{
OWSLogInfo(@"");
return [OWSFileSystem moveAppFilePath:self.legacyAttachmentsDirPath
sharedDataFilePath:self.sharedDataAttachmentsDirPath];
2017-11-28 19:46:26 +01:00
}
+ (NSString *)attachmentsFolder
{
static NSString *attachmentsFolder = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
2017-11-30 16:02:04 +01:00
attachmentsFolder = TSAttachmentStream.sharedDataAttachmentsDirPath;
[OWSFileSystem ensureDirectoryExists:attachmentsFolder];
});
return attachmentsFolder;
2015-12-07 03:31:43 +01:00
}
2018-09-04 16:25:42 +02:00
- (nullable NSString *)originalFilePath
{
if (!self.localRelativeFilePath) {
OWSFailDebug(@"Attachment missing local file path.");
return nil;
}
2017-05-17 18:44:05 +02:00
return [[[self class] attachmentsFolder] stringByAppendingPathComponent:self.localRelativeFilePath];
2015-12-07 03:31:43 +01:00
}
- (nullable NSString *)legacyThumbnailPath
2018-03-19 02:57:05 +01:00
{
2018-09-04 16:25:42 +02:00
NSString *filePath = self.originalFilePath;
2018-03-19 02:57:05 +01:00
if (!filePath) {
OWSFailDebug(@"Attachment missing local file path.");
2018-03-19 02:57:05 +01:00
return nil;
}
if (!self.isImage && !self.isVideo && !self.isAnimated) {
return nil;
}
NSString *filename = filePath.lastPathComponent.stringByDeletingPathExtension;
NSString *containingDir = filePath.stringByDeletingLastPathComponent;
NSString *newFilename = [filename stringByAppendingString:@"-signal-ios-thumbnail"];
2018-03-19 02:57:05 +01:00
return [[containingDir stringByAppendingPathComponent:newFilename] stringByAppendingPathExtension:@"jpg"];
}
2018-09-05 16:50:36 +02:00
- (NSString *)thumbnailsDirPath
{
2018-09-05 16:50:36 +02:00
if (!self.localRelativeFilePath) {
OWSFailDebug(@"Attachment missing local file path.");
2018-09-04 21:21:48 +02:00
return nil;
}
2018-09-06 06:01:35 +02:00
// Thumbnails are written to the caches directory, so that iOS can
// remove them if necessary.
2018-09-05 16:50:36 +02:00
NSString *dirName = [NSString stringWithFormat:@"%@-thumbnails", self.uniqueId];
2018-09-06 06:01:35 +02:00
return [OWSFileSystem.cachesDirectoryPath stringByAppendingPathComponent:dirName];
2018-09-05 16:50:36 +02:00
}
- (NSString *)pathForThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
2018-09-05 16:50:36 +02:00
{
NSString *filename = [NSString stringWithFormat:@"thumbnail-%lu.jpg", (unsigned long)thumbnailDimensionPoints];
return [self.thumbnailsDirPath stringByAppendingPathComponent:filename];
2018-09-04 21:21:48 +02:00
}
2018-09-04 16:25:42 +02:00
- (nullable NSURL *)originalMediaURL
{
2018-09-04 16:25:42 +02:00
NSString *_Nullable filePath = self.originalFilePath;
2017-05-19 23:33:17 +02:00
if (!filePath) {
OWSFailDebug(@"Missing path for attachment.");
return nil;
}
2017-05-19 23:33:17 +02:00
return [NSURL fileURLWithPath:filePath];
2015-12-07 03:31:43 +01:00
}
- (void)removeFileWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
{
NSError *error;
2018-09-05 16:50:36 +02:00
NSString *thumbnailsDirPath = self.thumbnailsDirPath;
if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailsDirPath]) {
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:thumbnailsDirPath error:&error];
if (error || !success) {
OWSLogError(@"remove thumbnails dir failed with: %@", error);
2018-09-04 21:21:48 +02:00
}
}
NSString *_Nullable legacyThumbnailPath = self.legacyThumbnailPath;
if (legacyThumbnailPath) {
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:legacyThumbnailPath error:&error];
2018-09-04 21:21:48 +02:00
if (error || !success) {
OWSLogError(@"remove legacy thumbnail failed with: %@", error);
}
}
2018-09-04 16:25:42 +02:00
NSString *_Nullable filePath = self.originalFilePath;
2017-05-19 23:33:17 +02:00
if (!filePath) {
OWSFailDebug(@"Missing path for attachment.");
2017-05-17 18:44:05 +02:00
return;
}
2018-09-04 21:21:48 +02:00
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
if (error || !success) {
OWSLogError(@"remove file failed with: %@", error);
Explain send failures for text and media messages Motivation ---------- We were often swallowing errors or yielding generic errors when it would be better to provide specific errors. We also didn't create an attachment when attachments failed to send, making it impossible to show the user what was happening with an in-progress or failed attachment. Primary Changes --------------- - Funnel all message sending through MessageSender, and remove message sending from MessagesManager. - Record most recent sending error so we can expose it in the UI - Can resend attachments. - Update message status for attachments, just like text messages - Extracted UploadingService from MessagesManager - Saving attachment stream before uploading gives uniform API for send vs. resend - update status for downloading transcript attachments - TSAttachments have a local id, separate from the server allocated id This allows us to save the attachment before the allocation request. Which is is good because: 1. can show feedback to user faster. 2. allows us to show an error when allocation fails. Code Cleanup ------------ - Replaced a lot of global singleton access with injected dependencies to make for easier testing. - Never save group meta messages. Rather than checking before (hopefully) every save, do it in the save method. - Don't use callbacks for sync code. - Handle errors on writing attachment data - Fix old long broken tests that weren't even running. =( - Removed dead code - Use constants vs define - Port flaky travis fixes from Signal-iOS // FREEBIE
2016-10-14 23:00:29 +02:00
}
}
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[super removeWithTransaction:transaction];
[self removeFileWithTransaction:transaction];
}
- (BOOL)isValidVisualMedia
2018-11-05 15:53:48 +01:00
{
if (self.isImage && self.isValidImage) {
return YES;
}
2015-12-07 03:31:43 +01:00
2018-11-05 15:53:48 +01:00
if (self.isVideo && self.isValidVideo) {
return YES;
}
2015-12-07 03:31:43 +01:00
2018-11-05 15:53:48 +01:00
if (self.isAnimated && self.isValidImage) {
return YES;
}
2015-12-07 03:31:43 +01:00
2018-11-05 15:53:48 +01:00
return NO;
2015-12-07 03:31:43 +01:00
}
2018-06-19 20:34:44 +02:00
#pragma mark - Image Validation
2018-08-31 02:59:26 +02:00
- (BOOL)isValidImage
2018-06-19 20:34:44 +02:00
{
OWSAssertDebug(self.isImage || self.isAnimated);
2018-06-19 20:34:44 +02:00
@synchronized(self) {
if (!self.isValidImageCached) {
self.isValidImageCached =
@([NSData ows_isValidImageAtPath:self.originalFilePath mimeType:self.contentType]);
}
return self.isValidImageCached.boolValue;
}
2018-06-19 20:34:44 +02:00
}
2018-08-31 02:59:26 +02:00
- (BOOL)isValidVideo
2018-06-19 20:34:44 +02:00
{
OWSAssertDebug(self.isVideo);
2018-06-19 20:34:44 +02:00
@synchronized(self) {
if (!self.isValidVideoCached) {
self.isValidVideoCached = @([OWSMediaUtils isValidVideoWithPath:self.originalFilePath]);
}
return self.isValidVideoCached.boolValue;
}
2018-06-19 20:34:44 +02:00
}
#pragma mark -
2018-09-04 16:29:30 +02:00
- (nullable UIImage *)originalImage
{
if ([self isVideo]) {
return [self videoStillImage];
} else if ([self isImage] || [self isAnimated]) {
2018-09-04 16:25:42 +02:00
NSURL *_Nullable mediaUrl = self.originalMediaURL;
if (!mediaUrl) {
return nil;
}
2018-08-31 02:59:26 +02:00
if (![self isValidImage]) {
2017-08-30 15:58:02 +02:00
return nil;
}
2018-09-04 16:25:42 +02:00
return [[UIImage alloc] initWithContentsOfFile:self.originalFilePath];
} else {
return nil;
2015-12-07 03:31:43 +01:00
}
}
2018-05-10 03:10:23 +02:00
- (nullable NSData *)validStillImageData
{
if ([self isVideo]) {
OWSFailDebug(@"isVideo was unexpectedly true");
2018-05-10 03:10:23 +02:00
return nil;
}
if ([self isAnimated]) {
OWSFailDebug(@"isAnimated was unexpectedly true");
2018-05-10 03:10:23 +02:00
return nil;
}
2018-09-04 16:25:42 +02:00
if (![NSData ows_isValidImageAtPath:self.originalFilePath mimeType:self.contentType]) {
OWSFailDebug(@"skipping invalid image");
2018-05-10 03:10:23 +02:00
return nil;
}
2018-09-04 16:25:42 +02:00
return [NSData dataWithContentsOfFile:self.originalFilePath];
2018-05-10 03:10:23 +02:00
}
2018-04-03 18:35:43 +02:00
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType
{
return ([MIMETypeUtil isVideo:contentType] || [MIMETypeUtil isImage:contentType] ||
[MIMETypeUtil isAnimated:contentType]);
}
- (nullable UIImage *)videoStillImage
2018-03-19 02:57:05 +01:00
{
NSError *error;
2018-09-06 06:01:35 +02:00
UIImage *_Nullable image = [OWSMediaUtils thumbnailForVideoAtPath:self.originalFilePath
maxDimension:ThumbnailDimensionPointsLarge()
error:&error];
if (error || !image) {
OWSLogError(@"Could not create video still: %@.", error);
2018-08-31 02:59:26 +02:00
return nil;
}
return image;
2015-12-07 03:31:43 +01:00
}
+ (void)deleteAttachments
{
2015-12-07 03:31:43 +01:00
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *fileURL = [NSURL fileURLWithPath:self.attachmentsFolder];
NSArray<NSURL *> *contents =
[fileManager contentsOfDirectoryAtURL:fileURL includingPropertiesForKeys:nil options:0 error:&error];
2015-12-07 03:31:43 +01:00
if (error) {
2018-08-27 16:29:51 +02:00
OWSFailDebug(@"failed to get contents of attachments folder: %@ with error: %@", self.attachmentsFolder, error);
return;
2015-12-07 03:31:43 +01:00
}
for (NSURL *url in contents) {
2017-12-19 03:07:54 +01:00
[fileManager removeItemAtURL:url error:&error];
if (error) {
2018-08-27 16:29:51 +02:00
OWSFailDebug(@"failed to remove item at path: %@ with error: %@", url, error);
}
}
2015-12-07 03:31:43 +01:00
}
- (CGSize)calculateImageSize
{
if ([self isVideo]) {
2018-08-31 02:59:26 +02:00
if (![self isValidVideo]) {
return CGSizeZero;
}
return [self videoStillImage].size;
} else if ([self isImage] || [self isAnimated]) {
2018-09-04 16:25:42 +02:00
NSURL *_Nullable mediaUrl = self.originalMediaURL;
if (!mediaUrl) {
return CGSizeZero;
}
2018-06-19 20:34:44 +02:00
if (![self isValidImage]) {
2017-08-30 15:58:02 +02:00
return CGSizeZero;
}
// With CGImageSource we avoid loading the whole image into memory.
CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)mediaUrl, NULL);
if (!source) {
OWSFailDebug(@"Could not load image: %@", mediaUrl);
return CGSizeZero;
}
NSDictionary *options = @{
(NSString *)kCGImageSourceShouldCache : @(NO),
};
NSDictionary *properties
= (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, (CFDictionaryRef)options);
CGSize imageSize = CGSizeZero;
if (properties) {
2017-10-10 22:13:54 +02:00
NSNumber *orientation = properties[(NSString *)kCGImagePropertyOrientation];
NSNumber *width = properties[(NSString *)kCGImagePropertyPixelWidth];
NSNumber *height = properties[(NSString *)kCGImagePropertyPixelHeight];
2017-10-10 22:13:54 +02:00
if (width && height) {
imageSize = CGSizeMake(width.floatValue, height.floatValue);
2017-10-10 22:13:54 +02:00
if (orientation) {
imageSize =
[self applyImageOrientation:(UIImageOrientation)orientation.intValue toImageSize:imageSize];
}
} else {
OWSFailDebug(@"Could not determine size of image: %@", mediaUrl);
}
}
CFRelease(source);
return imageSize;
} else {
return CGSizeZero;
}
}
2017-10-10 22:13:54 +02:00
- (CGSize)applyImageOrientation:(UIImageOrientation)orientation toImageSize:(CGSize)imageSize
{
switch (orientation) {
case UIImageOrientationUp: // EXIF = 1
case UIImageOrientationUpMirrored: // EXIF = 2
case UIImageOrientationDown: // EXIF = 3
case UIImageOrientationDownMirrored: // EXIF = 4
return imageSize;
case UIImageOrientationLeftMirrored: // EXIF = 5
case UIImageOrientationLeft: // EXIF = 6
case UIImageOrientationRightMirrored: // EXIF = 7
case UIImageOrientationRight: // EXIF = 8
return CGSizeMake(imageSize.height, imageSize.width);
default:
return imageSize;
}
}
2018-02-02 16:56:16 +01:00
- (BOOL)shouldHaveImageSize
{
return ([self isVideo] || [self isImage] || [self isAnimated]);
}
- (CGSize)imageSize
{
OWSAssertDebug(self.shouldHaveImageSize);
2018-02-02 16:56:16 +01:00
@synchronized(self)
{
if (self.cachedImageWidth && self.cachedImageHeight) {
return CGSizeMake(self.cachedImageWidth.floatValue, self.cachedImageHeight.floatValue);
}
2018-02-02 16:56:16 +01:00
CGSize imageSize = [self calculateImageSize];
if (imageSize.width <= 0 || imageSize.height <= 0) {
return CGSizeZero;
2017-11-20 20:50:43 +01:00
}
2018-02-02 16:56:16 +01:00
self.cachedImageWidth = @(imageSize.width);
self.cachedImageHeight = @(imageSize.height);
[self.dbReadWriteConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
2018-02-02 16:56:16 +01:00
NSString *collection = [[self class] collection];
TSAttachmentStream *latestInstance = [transaction objectForKey:self.uniqueId inCollection:collection];
if (latestInstance) {
latestInstance.cachedImageWidth = @(imageSize.width);
latestInstance.cachedImageHeight = @(imageSize.height);
[latestInstance saveWithTransaction:transaction];
} else {
// This message has not yet been saved or has been deleted; do nothing.
// This isn't an error per se, but these race conditions should be
// _very_ rare.
//
// An exception is incoming group avatar updates which we don't ever save.
OWSLogWarn(@"Attachment not yet saved.");
2018-02-02 16:56:16 +01:00
}
}];
return imageSize;
}
}
- (CGFloat)calculateAudioDurationSeconds
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSAssertDebug([self isAudio]);
NSError *error;
2018-09-04 16:25:42 +02:00
AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:self.originalMediaURL error:&error];
if (error && [error.domain isEqualToString:NSOSStatusErrorDomain]
&& (error.code == kAudioFileInvalidFileError || error.code == kAudioFileStreamError_InvalidFile)) {
// Ignore "invalid audio file" errors.
return 0.f;
}
if (!error) {
return (CGFloat)[audioPlayer duration];
} else {
OWSLogError(@"Could not find audio duration: %@", self.originalMediaURL);
return 0;
}
}
- (CGFloat)audioDurationSeconds
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
if (self.cachedAudioDurationSeconds) {
return self.cachedAudioDurationSeconds.floatValue;
}
CGFloat audioDurationSeconds = [self calculateAudioDurationSeconds];
self.cachedAudioDurationSeconds = @(audioDurationSeconds);
2017-11-20 20:50:43 +01:00
[self.dbReadWriteConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSString *collection = [[self class] collection];
TSAttachmentStream *latestInstance = [transaction objectForKey:self.uniqueId inCollection:collection];
if (latestInstance) {
latestInstance.cachedAudioDurationSeconds = @(audioDurationSeconds);
[latestInstance saveWithTransaction:transaction];
} else {
// This message has not yet been saved or has been deleted; do nothing.
// This isn't an error per se, but these race conditions should be
// _very_ rare.
OWSFailDebug(@"Attachment not yet saved.");
2017-11-20 20:50:43 +01:00
}
}];
return audioDurationSeconds;
}
2018-09-04 21:21:48 +02:00
#pragma mark - Thumbnails
- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint
success:(OWSThumbnailSuccess)success
failure:(OWSThumbnailFailure)failure
2018-09-04 21:21:48 +02:00
{
CGFloat maxDimensionHint = MAX(sizeHint.width, sizeHint.height);
NSUInteger thumbnailDimensionPoints;
if (maxDimensionHint <= kThumbnailDimensionPointsSmall) {
thumbnailDimensionPoints = kThumbnailDimensionPointsSmall;
2018-09-05 19:47:37 +02:00
} else if (maxDimensionHint <= kThumbnailDimensionPointsMedium) {
2018-09-05 16:51:24 +02:00
thumbnailDimensionPoints = kThumbnailDimensionPointsMedium;
2018-09-05 19:47:37 +02:00
} else {
thumbnailDimensionPoints = ThumbnailDimensionPointsLarge();
2018-09-04 21:21:48 +02:00
}
return [self thumbnailImageWithThumbnailDimensionPoints:thumbnailDimensionPoints success:success failure:failure];
2018-09-04 21:21:48 +02:00
}
- (nullable UIImage *)thumbnailImageSmallWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure
2018-09-04 21:21:48 +02:00
{
return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall
success:success
failure:failure];
2018-09-04 21:21:48 +02:00
}
- (nullable UIImage *)thumbnailImageMediumWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure
2018-09-04 21:21:48 +02:00
{
return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsMedium
success:success
failure:failure];
2018-09-04 21:21:48 +02:00
}
2018-09-05 19:47:37 +02:00
- (nullable UIImage *)thumbnailImageLargeWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure
{
return [self thumbnailImageWithThumbnailDimensionPoints:ThumbnailDimensionPointsLarge()
success:success
failure:failure];
}
2018-09-04 21:21:48 +02:00
- (nullable UIImage *)thumbnailImageWithThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
success:(OWSThumbnailSuccess)success
failure:(OWSThumbnailFailure)failure
{
OWSLoadedThumbnail *_Nullable loadedThumbnail;
loadedThumbnail = [self loadedThumbnailWithThumbnailDimensionPoints:thumbnailDimensionPoints
success:^(OWSLoadedThumbnail *thumbnail) {
DispatchMainThreadSafe(^{
success(thumbnail.image);
});
}
failure:^{
DispatchMainThreadSafe(^{
failure();
});
}];
return loadedThumbnail.image;
}
- (nullable OWSLoadedThumbnail *)loadedThumbnailWithThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
success:(OWSLoadedThumbnailSuccess)success
failure:(OWSThumbnailFailure)failure
2018-09-04 21:21:48 +02:00
{
CGSize originalSize = self.imageSize;
if (originalSize.width < 1 || originalSize.height < 1) {
2018-09-25 23:26:07 +02:00
// Any time we return nil from this method we have to call the failure handler
// or else the caller waits for an async thumbnail
failure();
2018-09-04 21:21:48 +02:00
return nil;
}
if (originalSize.width <= thumbnailDimensionPoints || originalSize.height <= thumbnailDimensionPoints) {
// There's no point in generating a thumbnail if the original is smaller than the
// thumbnail size.
return [[OWSLoadedThumbnail alloc] initWithImage:self.originalImage filePath:self.originalFilePath];
2018-09-04 21:21:48 +02:00
}
NSString *thumbnailPath = [self pathForThumbnailDimensionPoints:thumbnailDimensionPoints];
if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailPath]) {
2018-09-04 21:21:48 +02:00
UIImage *_Nullable image = [UIImage imageWithContentsOfFile:thumbnailPath];
if (!image) {
OWSFailDebug(@"couldn't load image.");
2018-09-25 23:26:07 +02:00
// Any time we return nil from this method we have to call the failure handler
// or else the caller waits for an async thumbnail
failure();
return nil;
}
return [[OWSLoadedThumbnail alloc] initWithImage:image filePath:thumbnailPath];
2018-09-04 21:21:48 +02:00
}
[OWSThumbnailService.shared ensureThumbnailForAttachment:self
thumbnailDimensionPoints:thumbnailDimensionPoints
success:success
2018-09-06 06:01:35 +02:00
failure:^(NSError *error) {
OWSLogError(@"Failed to create thumbnail: %@", error);
2018-09-06 06:01:35 +02:00
failure();
}];
2018-09-04 21:21:48 +02:00
return nil;
}
- (nullable OWSLoadedThumbnail *)loadedThumbnailSmallSync
{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
2018-09-05 21:41:33 +02:00
__block OWSLoadedThumbnail *_Nullable asyncLoadedThumbnail = nil;
OWSLoadedThumbnail *_Nullable syncLoadedThumbnail = nil;
syncLoadedThumbnail = [self loadedThumbnailWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall
success:^(OWSLoadedThumbnail *thumbnail) {
@synchronized(self) {
asyncLoadedThumbnail = thumbnail;
}
dispatch_semaphore_signal(semaphore);
}
failure:^{
dispatch_semaphore_signal(semaphore);
}];
2018-09-05 17:43:02 +02:00
2018-09-05 21:41:33 +02:00
if (syncLoadedThumbnail) {
return syncLoadedThumbnail;
}
2018-09-05 17:43:02 +02:00
// Wait up to N seconds.
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)));
@synchronized(self) {
2018-09-05 21:41:33 +02:00
return asyncLoadedThumbnail;
}
}
- (nullable UIImage *)thumbnailImageSmallSync
{
OWSLoadedThumbnail *_Nullable loadedThumbnail = [self loadedThumbnailSmallSync];
if (!loadedThumbnail) {
OWSLogInfo(@"Couldn't load small thumbnail sync.");
return nil;
}
return loadedThumbnail.image;
}
- (nullable NSData *)thumbnailDataSmallSync
{
OWSLoadedThumbnail *_Nullable loadedThumbnail = [self loadedThumbnailSmallSync];
if (!loadedThumbnail) {
OWSLogInfo(@"Couldn't load small thumbnail sync.");
return nil;
}
NSError *error;
NSData *_Nullable data = [loadedThumbnail dataAndReturnError:&error];
if (error || !data) {
OWSFailDebug(@"Couldn't load thumbnail data: %@", error);
return nil;
}
return data;
}
- (NSArray<NSString *> *)allThumbnailPaths
{
NSMutableArray<NSString *> *result = [NSMutableArray new];
NSString *thumbnailsDirPath = self.thumbnailsDirPath;
2018-09-05 17:54:39 +02:00
if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailsDirPath]) {
NSError *error;
NSArray<NSString *> *_Nullable fileNames =
[[NSFileManager defaultManager] contentsOfDirectoryAtPath:thumbnailsDirPath error:&error];
if (error || !fileNames) {
OWSFailDebug(@"contentsOfDirectoryAtPath failed with error: %@", error);
2018-09-05 17:54:39 +02:00
} else {
for (NSString *fileName in fileNames) {
NSString *filePath = [thumbnailsDirPath stringByAppendingPathComponent:fileName];
[result addObject:filePath];
}
}
}
NSString *_Nullable legacyThumbnailPath = self.legacyThumbnailPath;
if (legacyThumbnailPath && [[NSFileManager defaultManager] fileExistsAtPath:legacyThumbnailPath]) {
[result addObject:legacyThumbnailPath];
}
return result;
}
2018-03-20 22:22:19 +01:00
#pragma mark - Update With... Methods
- (nullable TSAttachmentStream *)cloneAsThumbnail
{
NSData *_Nullable thumbnailData = self.thumbnailDataSmallSync;
// Only some media types have thumbnails
if (!thumbnailData) {
return nil;
}
// Copy the thumbnail to a new attachment.
NSString *thumbnailName = [NSString stringWithFormat:@"quoted-thumbnail-%@", self.sourceFilename];
TSAttachmentStream *thumbnailAttachment =
[[TSAttachmentStream alloc] initWithContentType:OWSMimeTypeImageJpeg
byteCount:(uint32_t)thumbnailData.length
sourceFilename:thumbnailName
caption:nil
albumMessageId:nil];
NSError *error;
BOOL success = [thumbnailAttachment writeData:thumbnailData error:&error];
if (!success || error) {
OWSLogError(@"Couldn't copy attachment data for message sent to self: %@.", error);
return nil;
}
return thumbnailAttachment;
}
// MARK: Protobuf serialization
+ (nullable SSKProtoAttachmentPointer *)buildProtoForAttachmentId:(nullable NSString *)attachmentId
{
OWSAssertDebug(attachmentId.length > 0);
2018-05-07 18:32:31 +02:00
// TODO we should past in a transaction, rather than sneakily generate one in `fetch...` to make sure we're
// getting a consistent view in the message sending process. A brief glance shows it touches quite a bit of code,
// but should be straight forward.
TSAttachment *attachment = [TSAttachmentStream fetchObjectWithUniqueID:attachmentId];
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
OWSLogError(@"Unexpected type for attachment builder: %@", attachment);
return nil;
}
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
return [attachmentStream buildProto];
}
- (nullable SSKProtoAttachmentPointer *)buildProto
{
SSKProtoAttachmentPointerBuilder *builder = [SSKProtoAttachmentPointer builderWithId:self.serverId];
OWSAssertDebug(self.contentType.length > 0);
builder.contentType = self.contentType;
OWSLogVerbose(@"Sending attachment with filename: '%@'", self.sourceFilename);
if (self.sourceFilename.length > 0) {
builder.fileName = self.sourceFilename;
}
if (self.caption.length > 0) {
builder.caption = self.caption;
}
builder.size = self.byteCount;
builder.key = self.encryptionKey;
builder.digest = self.digest;
builder.flags = self.isVoiceMessage ? SSKProtoAttachmentPointerFlagsVoiceMessage : 0;
if (self.shouldHaveImageSize) {
CGSize imageSize = self.imageSize;
if (imageSize.width < NSIntegerMax && imageSize.height < NSIntegerMax) {
NSInteger imageWidth = (NSInteger)round(imageSize.width);
NSInteger imageHeight = (NSInteger)round(imageSize.height);
if (imageWidth > 0 && imageHeight > 0) {
builder.width = (UInt32)imageWidth;
builder.height = (UInt32)imageHeight;
}
}
}
NSError *error;
SSKProtoAttachmentPointer *_Nullable attachmentProto = [builder buildAndReturnError:&error];
if (error || !attachmentProto) {
OWSFailDebug(@"could not build protobuf: %@", error);
return nil;
}
return attachmentProto;
}
2015-12-07 03:31:43 +01:00
@end
NS_ASSUME_NONNULL_END