Send multiple attachments from the share extension.

pull/1/head
Matthew Chen 7 years ago
parent 2bad0c20b6
commit aeadea67e2

@ -1,5 +1,5 @@
// //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
#import "SelectThreadViewController.h" #import "SelectThreadViewController.h"
@ -7,11 +7,12 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@class SignalAttachment; @class SignalAttachment;
@protocol ShareViewDelegate; @protocol ShareViewDelegate;
@interface SharingThreadPickerViewController : SelectThreadViewController @interface SharingThreadPickerViewController : SelectThreadViewController
@property (nonatomic) SignalAttachment *attachment; @property (nonatomic) NSArray<SignalAttachment *> *attachments;
- (instancetype)initWithShareViewDelegate:(id<ShareViewDelegate>)shareViewDelegate; - (instancetype)initWithShareViewDelegate:(id<ShareViewDelegate>)shareViewDelegate;

@ -127,13 +127,18 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion);
- (nullable NSString *)convertAttachmentToMessageTextIfPossible - (nullable NSString *)convertAttachmentToMessageTextIfPossible
{ {
if (!self.attachment.isConvertibleToTextMessage) { if (self.attachments.count > 1) {
return nil; return nil;
} }
if (self.attachment.dataLength >= kOversizeTextMessageSizeThreshold) { OWSAssertDebug(self.attachments.count == 1);
SignalAttachment *attachment = self.attachments.firstObject;
if (!attachment.isConvertibleToTextMessage) {
return nil; return nil;
} }
NSData *data = self.attachment.data; if (attachment.dataLength >= kOversizeTextMessageSizeThreshold) {
return nil;
}
NSData *data = attachment.data;
OWSAssertDebug(data.length < kOversizeTextMessageSizeThreshold); OWSAssertDebug(data.length < kOversizeTextMessageSizeThreshold);
NSString *_Nullable messageText = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSString *_Nullable messageText = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
OWSLogVerbose(@"messageTextForAttachment: %@", messageText); OWSLogVerbose(@"messageTextForAttachment: %@", messageText);
@ -142,42 +147,67 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion);
- (void)threadWasSelected:(TSThread *)thread - (void)threadWasSelected:(TSThread *)thread
{ {
OWSAssertDebug(self.attachment); OWSAssertDebug(self.attachments.count > 0);
OWSAssertDebug(thread); OWSAssertDebug(thread);
self.thread = thread; self.thread = thread;
if (self.attachment.isConvertibleToContactShare) { if ([self tryToShareAsMessageText]) {
[self showContactShareApproval]; return;
}
if ([self tryToShareAsContactShare]) {
return; return;
} }
OWSNavigationController *approvalModal =
[AttachmentApprovalViewController wrappedInNavControllerWithAttachments:self.attachments approvalDelegate:self];
[self presentViewController:approvalModal animated:YES completion:nil];
}
- (BOOL)tryToShareAsMessageText
{
OWSAssertDebug(self.attachments.count > 0);
NSString *_Nullable messageText = [self convertAttachmentToMessageTextIfPossible]; NSString *_Nullable messageText = [self convertAttachmentToMessageTextIfPossible];
if (!messageText) {
return NO;
}
if (messageText) { MessageApprovalViewController *approvalVC =
MessageApprovalViewController *approvalVC = [[MessageApprovalViewController alloc] initWithMessageText:messageText
[[MessageApprovalViewController alloc] initWithMessageText:messageText thread:self.thread
thread:thread contactsManager:self.contactsManager
contactsManager:self.contactsManager delegate:self];
delegate:self];
[self.navigationController pushViewController:approvalVC animated:YES]; [self.navigationController pushViewController:approvalVC animated:YES];
} else { return YES;
// TODO ALBUMS - send album via SAE }
OWSNavigationController *approvalModal =
[AttachmentApprovalViewController wrappedInNavControllerWithAttachments:@[ self.attachment ] - (BOOL)tryToShareAsContactShare
approvalDelegate:self]; {
[self presentViewController:approvalModal animated:YES completion:nil]; OWSAssertDebug(self.attachments.count > 0);
if (self.attachments.count > 1) {
return nil;
} }
OWSAssertDebug(self.attachments.count == 1);
SignalAttachment *attachment = self.attachments.firstObject;
if (!attachment.isConvertibleToContactShare) {
return NO;
}
[self showContactShareApproval:attachment];
return YES;
} }
- (void)showContactShareApproval - (void)showContactShareApproval:(SignalAttachment *)attachment
{ {
OWSAssertDebug(self.attachment); OWSAssertDebug(attachment);
OWSAssertDebug(self.thread); OWSAssertDebug(self.thread);
OWSAssertDebug(self.attachment.isConvertibleToContactShare); OWSAssertDebug(attachment.isConvertibleToContactShare);
NSData *data = self.attachment.data; NSData *data = attachment.data;
CNContact *_Nullable cnContact = [Contact cnContactWithVCardData:data]; CNContact *_Nullable cnContact = [Contact cnContactWithVCardData:data];
Contact *_Nullable contact = [[Contact alloc] initWithSystemContact:cnContact]; Contact *_Nullable contact = [[Contact alloc] initWithSystemContact:cnContact];
@ -242,13 +272,13 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion);
// the sending operation. Alternatively, we could use a durable send, but do more to make sure the // the sending operation. Alternatively, we could use a durable send, but do more to make sure the
// SAE runs as long as it needs. // SAE runs as long as it needs.
// TODO ALBUMS - send album via SAE // TODO ALBUMS - send album via SAE
outgoingMessage = [ThreadUtil sendMessageNonDurablyWithAttachment:attachments.firstObject outgoingMessage = [ThreadUtil sendMessageNonDurablyWithAttachments:attachments
inThread:self.thread inThread:self.thread
quotedReplyModel:nil quotedReplyModel:nil
messageSender:self.messageSender messageSender:self.messageSender
completion:^(NSError *_Nullable error) { completion:^(NSError *_Nullable error) {
sendCompletion(error, outgoingMessage); sendCompletion(error, outgoingMessage);
}]; }];
// This is necessary to show progress. // This is necessary to show progress.
self.outgoingMessage = outgoingMessage; self.outgoingMessage = outgoingMessage;

@ -72,11 +72,11 @@ NS_ASSUME_NONNULL_BEGIN
failure:(void (^)(NSError *error))failureHandler; failure:(void (^)(NSError *error))failureHandler;
// Used by SAE, otherwise we should use the durable `enqueue` counterpart // Used by SAE, otherwise we should use the durable `enqueue` counterpart
+ (TSOutgoingMessage *)sendMessageNonDurablyWithAttachment:(SignalAttachment *)attachment + (TSOutgoingMessage *)sendMessageNonDurablyWithAttachments:(NSArray<SignalAttachment *> *)attachments
inThread:(TSThread *)thread inThread:(TSThread *)thread
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
messageSender:(OWSMessageSender *)messageSender messageSender:(OWSMessageSender *)messageSender
completion:(void (^_Nullable)(NSError *_Nullable error))completion; completion:(void (^_Nullable)(NSError *_Nullable error))completion;
// Used by SAE, otherwise we should use the durable `enqueue` counterpart // Used by SAE, otherwise we should use the durable `enqueue` counterpart
+ (TSOutgoingMessage *)sendMessageNonDurablyWithContactShare:(OWSContact *)contactShare + (TSOutgoingMessage *)sendMessageNonDurablyWithContactShare:(OWSContact *)contactShare

@ -213,15 +213,14 @@ NS_ASSUME_NONNULL_BEGIN
return message; return message;
} }
+ (TSOutgoingMessage *)sendMessageNonDurablyWithAttachment:(SignalAttachment *)attachment + (TSOutgoingMessage *)sendMessageNonDurablyWithAttachments:(NSArray<SignalAttachment *> *)attachments
inThread:(TSThread *)thread inThread:(TSThread *)thread
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
messageSender:(OWSMessageSender *)messageSender messageSender:(OWSMessageSender *)messageSender
completion:(void (^_Nullable)(NSError *_Nullable error))completion completion:(void (^_Nullable)(NSError *_Nullable error))completion
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
OWSAssertDebug(attachment); OWSAssertDebug(attachments.count > 0);
OWSAssertDebug([attachment mimeType].length > 0);
OWSAssertDebug(thread); OWSAssertDebug(thread);
OWSAssertDebug(messageSender); OWSAssertDebug(messageSender);
@ -229,22 +228,28 @@ NS_ASSUME_NONNULL_BEGIN
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:thread.uniqueId]; [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:thread.uniqueId];
uint32_t expiresInSeconds = (configuration.isEnabled ? configuration.durationSeconds : 0); uint32_t expiresInSeconds = (configuration.isEnabled ? configuration.durationSeconds : 0);
BOOL isVoiceMessage = (attachments.count == 1 && attachments.firstObject.isVoiceMessage);
NSString *_Nullable messageBody = (attachments.count == 1 ? attachments.firstObject.captionText : nil);
TSOutgoingMessage *message = TSOutgoingMessage *message =
[[TSOutgoingMessage alloc] initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp] [[TSOutgoingMessage alloc] initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp]
inThread:thread inThread:thread
messageBody:attachment.captionText messageBody:messageBody
attachmentIds:[NSMutableArray new] attachmentIds:[NSMutableArray new]
expiresInSeconds:expiresInSeconds expiresInSeconds:expiresInSeconds
expireStartedAt:0 expireStartedAt:0
isVoiceMessage:[attachment isVoiceMessage] isVoiceMessage:isVoiceMessage
groupMetaMessage:TSGroupMetaMessageUnspecified groupMetaMessage:TSGroupMetaMessageUnspecified
quotedMessage:[quotedReplyModel buildQuotedMessageForSending] quotedMessage:[quotedReplyModel buildQuotedMessageForSending]
contactShare:nil]; contactShare:nil];
[messageSender sendAttachment:attachment.dataSource NSMutableArray<OWSOutgoingAttachmentInfo *> *attachmentInfos = [NSMutableArray new];
contentType:attachment.mimeType for (SignalAttachment *attachment in attachments) {
sourceFilename:attachment.filenameOrDefault OWSAssertDebug([attachment mimeType].length > 0);
albumMessageId:message.uniqueId
[attachmentInfos addObject:[attachment buildOutgoingAttachmentInfoWithMessage:message]];
}
[messageSender sendAttachments:attachmentInfos
inMessage:message inMessage:message
success:^{ success:^{
OWSLogDebug(@"Successfully sent message attachment."); OWSLogDebug(@"Successfully sent message attachment.");

@ -334,7 +334,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
} else { } else {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else { return } guard let strongSelf = self else { return }
strongSelf.buildAttachmentAndPresentConversationPicker() strongSelf.buildAttachmentsAndPresentConversationPicker()
} }
} }
@ -534,10 +534,10 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
} }
} }
private func buildAttachmentAndPresentConversationPicker() { private func buildAttachmentsAndPresentConversationPicker() {
AssertIsOnMainThread() AssertIsOnMainThread()
self.buildAttachment().map { [weak self] attachment in self.buildAttachments().map { [weak self] attachments in
AssertIsOnMainThread() AssertIsOnMainThread()
guard let strongSelf = self else { return } guard let strongSelf = self else { return }
@ -546,9 +546,9 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
let conversationPicker = SharingThreadPickerViewController(shareViewDelegate: strongSelf) let conversationPicker = SharingThreadPickerViewController(shareViewDelegate: strongSelf)
Logger.debug("presentConversationPicker: \(conversationPicker)") Logger.debug("presentConversationPicker: \(conversationPicker)")
conversationPicker.attachment = attachment conversationPicker.attachments = attachments
strongSelf.showPrimaryViewController(conversationPicker) strongSelf.showPrimaryViewController(conversationPicker)
Logger.info("showing picker with attachment: \(attachment)") Logger.info("showing picker with attachments: \(attachments)")
}.catch { [weak self] error in }.catch { [weak self] error in
AssertIsOnMainThread() AssertIsOnMainThread()
guard let strongSelf = self else { return } guard let strongSelf = self else { return }
@ -585,6 +585,11 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
return firstUtiType == utiType return firstUtiType == utiType
} }
private class func isVisualMediaItem(itemProvider: NSItemProvider) -> Bool {
return (itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) ||
itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMovie as String))
}
private class func isUrlItem(itemProvider: NSItemProvider) -> Bool { private class func isUrlItem(itemProvider: NSItemProvider) -> Bool {
return itemMatchesSpecificUtiType(itemProvider: itemProvider, return itemMatchesSpecificUtiType(itemProvider: itemProvider,
utiType: kUTTypeURL as String) utiType: kUTTypeURL as String)
@ -611,26 +616,6 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
return matchingUtiType return matchingUtiType
} }
private class func preferredItemProvider(inputItem: NSExtensionItem) -> NSItemProvider? {
guard let attachments = inputItem.attachments else {
return nil
}
// Prefer a URL provider if available
// TODO: Support multi-image messages.
if let preferredAttachment = attachments.first(where: { (attachment: Any) -> Bool in
guard let itemProvider = attachment as? NSItemProvider else {
return false
}
return isUrlItem(itemProvider: itemProvider)
}) {
return preferredAttachment as? NSItemProvider
}
// else return whatever is available
return inputItem.attachments?.first as? NSItemProvider
}
private class func createDataSource(utiType: String, url: URL, customFileName: String?) -> DataSource? { private class func createDataSource(utiType: String, url: URL, customFileName: String?) -> DataSource? {
if utiType == (kUTTypeURL as String) { if utiType == (kUTTypeURL as String) {
// Share URLs as oversize text messages whose text content is the URL. // Share URLs as oversize text messages whose text content is the URL.
@ -662,10 +647,23 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
} }
} }
private func buildAttachment() -> Promise<SignalAttachment> { private class func preferredItemProviders(inputItem: NSExtensionItem) -> [NSItemProvider]? {
guard let inputItem: NSExtensionItem = self.extensionContext?.inputItems.first as? NSExtensionItem else { guard let attachments = inputItem.attachments else {
let error = ShareViewControllerError.assertionError(description: "no input item") return nil
return Promise(error: error) }
var visualMediaItemProviders = [NSItemProvider]()
for attachment in attachments {
guard let itemProvider = attachment as? NSItemProvider else {
owsFailDebug("Unexpected attachment type: \(String(describing: attachment))")
continue
}
if isVisualMediaItem(itemProvider: itemProvider) {
visualMediaItemProviders.append(itemProvider)
}
}
if visualMediaItemProviders.count > 0 {
return visualMediaItemProviders
} }
// A single inputItem can have multiple attachments, e.g. sharing from Firefox gives // A single inputItem can have multiple attachments, e.g. sharing from Firefox gives
@ -675,10 +673,75 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
// FIXME: For now, we prefer the URL provider and discard the text provider, since it's more useful to share the URL than the caption // FIXME: For now, we prefer the URL provider and discard the text provider, since it's more useful to share the URL than the caption
// but we *should* include both. This will be a bigger change though since our share extension is currently heavily predicated // but we *should* include both. This will be a bigger change though since our share extension is currently heavily predicated
// on one itemProvider per share. // on one itemProvider per share.
guard let itemProvider: NSItemProvider = type(of: self).preferredItemProvider(inputItem: inputItem) else {
let error = ShareViewControllerError.assertionError(description: "No item provider in input item attachments") // Prefer a URL provider if available
if let preferredAttachment = attachments.first(where: { (attachment: Any) -> Bool in
guard let itemProvider = attachment as? NSItemProvider else {
return false
}
return isUrlItem(itemProvider: itemProvider)
}) {
if let itemProvider = preferredAttachment as? NSItemProvider {
return [itemProvider]
} else {
owsFailDebug("Unexpected attachment type: \(String(describing: preferredAttachment))")
}
}
// else return whatever is available
if let itemProvider = inputItem.attachments?.first as? NSItemProvider {
return [itemProvider]
} else {
owsFailDebug("Missing attachment.")
}
return []
}
private func selectItemProviders() -> Promise<[NSItemProvider]> {
guard let inputItems = self.extensionContext?.inputItems else {
let error = ShareViewControllerError.assertionError(description: "no input item")
return Promise(error: error) return Promise(error: error)
} }
for inputItemRaw in inputItems {
guard let inputItem = inputItemRaw as? NSExtensionItem else {
Logger.error("invalid inputItem \(inputItemRaw)")
continue
}
if let itemProviders = ShareViewController.preferredItemProviders(inputItem: inputItem) {
return Promise.value(itemProviders)
}
}
let error = ShareViewControllerError.assertionError(description: "no input item")
return Promise(error: error)
}
private
struct LoadedItem {
let itemProvider: NSItemProvider
let itemUrl: URL
let utiType: String
var customFileName: String?
var isConvertibleToTextMessage = false
var isConvertibleToContactShare = false
init(itemProvider: NSItemProvider,
itemUrl: URL,
utiType: String,
customFileName: String? = nil,
isConvertibleToTextMessage: Bool = false,
isConvertibleToContactShare: Bool = false) {
self.itemProvider = itemProvider
self.itemUrl = itemUrl
self.utiType = utiType
self.customFileName = customFileName
self.isConvertibleToTextMessage = isConvertibleToTextMessage
self.isConvertibleToContactShare = isConvertibleToContactShare
}
}
private func loadItemProvider(itemProvider: NSItemProvider) -> Promise<LoadedItem> {
Logger.info("attachment: \(itemProvider)") Logger.info("attachment: \(itemProvider)")
// We need to be very careful about which UTI type we use. // We need to be very careful about which UTI type we use.
@ -696,17 +759,12 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
} }
Logger.debug("matched utiType: \(srcUtiType)") Logger.debug("matched utiType: \(srcUtiType)")
let (promise, resolver) = Promise<(itemUrl: URL, utiType: String)>.pending() let (promise, resolver) = Promise<LoadedItem>.pending()
var customFileName: String?
var isConvertibleToTextMessage = false
var isConvertibleToContactShare = false
let loadCompletion: NSItemProvider.CompletionHandler = { [weak self] let loadCompletion: NSItemProvider.CompletionHandler = { [weak self]
(value, error) in (value, error) in
guard let _ = self else { return } guard let _ = self else { return }
guard error == nil else { guard error == nil else {
resolver.reject(error!) resolver.reject(error!)
return return
@ -721,11 +779,13 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
Logger.info("value type: \(type(of: value))") Logger.info("value type: \(type(of: value))")
if let data = value as? Data { if let data = value as? Data {
let customFileName = "Contact.vcf"
var isConvertibleToContactShare = false
// Although we don't support contacts _yet_, when we do we'll want to make // Although we don't support contacts _yet_, when we do we'll want to make
// sure they are shared with a reasonable filename. // sure they are shared with a reasonable filename.
if ShareViewController.itemMatchesSpecificUtiType(itemProvider: itemProvider, if ShareViewController.itemMatchesSpecificUtiType(itemProvider: itemProvider,
utiType: kUTTypeVCard as String) { utiType: kUTTypeVCard as String) {
customFileName = "Contact.vcf"
if Contact(vCardData: data) != nil { if Contact(vCardData: data) != nil {
isConvertibleToContactShare = true isConvertibleToContactShare = true
@ -744,7 +804,11 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
return return
} }
let fileUrl = URL(fileURLWithPath: tempFilePath) let fileUrl = URL(fileURLWithPath: tempFilePath)
resolver.fulfill((itemUrl: fileUrl, utiType: srcUtiType)) resolver.fulfill(LoadedItem(itemProvider: itemProvider,
itemUrl: fileUrl,
utiType: srcUtiType,
customFileName: customFileName,
isConvertibleToContactShare: isConvertibleToContactShare))
} else if let string = value as? String { } else if let string = value as? String {
Logger.debug("string provider: \(string)") Logger.debug("string provider: \(string)")
guard let data = string.filterStringForDisplay().data(using: String.Encoding.utf8) else { guard let data = string.filterStringForDisplay().data(using: String.Encoding.utf8) else {
@ -760,21 +824,33 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
let fileUrl = URL(fileURLWithPath: tempFilePath) let fileUrl = URL(fileURLWithPath: tempFilePath)
isConvertibleToTextMessage = !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String) let isConvertibleToTextMessage = !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String)
if UTTypeConformsTo(srcUtiType as CFString, kUTTypeText) { if UTTypeConformsTo(srcUtiType as CFString, kUTTypeText) {
resolver.fulfill((itemUrl: fileUrl, utiType: srcUtiType)) resolver.fulfill(LoadedItem(itemProvider: itemProvider,
itemUrl: fileUrl,
utiType: srcUtiType,
isConvertibleToTextMessage: isConvertibleToTextMessage))
} else { } else {
resolver.fulfill((itemUrl: fileUrl, utiType: kUTTypeText as String)) resolver.fulfill(LoadedItem(itemProvider: itemProvider,
itemUrl: fileUrl,
utiType: kUTTypeText as String,
isConvertibleToTextMessage: isConvertibleToTextMessage))
} }
} else if let url = value as? URL { } else if let url = value as? URL {
// If the share itself is a URL (e.g. a link from Safari), try to send this as a text message. // If the share itself is a URL (e.g. a link from Safari), try to send this as a text message.
isConvertibleToTextMessage = (itemProvider.registeredTypeIdentifiers.contains(kUTTypeURL as String) && let isConvertibleToTextMessage = (itemProvider.registeredTypeIdentifiers.contains(kUTTypeURL as String) &&
!itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String)) !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String))
if isConvertibleToTextMessage { if isConvertibleToTextMessage {
resolver.fulfill((itemUrl: url, utiType: kUTTypeURL as String)) resolver.fulfill(LoadedItem(itemProvider: itemProvider,
itemUrl: url,
utiType: kUTTypeURL as String,
isConvertibleToTextMessage: isConvertibleToTextMessage))
} else { } else {
resolver.fulfill((itemUrl: url, utiType: srcUtiType)) resolver.fulfill(LoadedItem(itemProvider: itemProvider,
itemUrl: url,
utiType: srcUtiType,
isConvertibleToTextMessage: isConvertibleToTextMessage))
} }
} else if let image = value as? UIImage { } else if let image = value as? UIImage {
if let data = UIImagePNGRepresentation(image) { if let data = UIImagePNGRepresentation(image) {
@ -782,7 +858,8 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
do { do {
let url = NSURL.fileURL(withPath: tempFilePath) let url = NSURL.fileURL(withPath: tempFilePath)
try data.write(to: url) try data.write(to: url)
resolver.fulfill((url, srcUtiType)) resolver.fulfill(LoadedItem(itemProvider: itemProvider, itemUrl: url,
utiType: srcUtiType))
} catch { } catch {
resolver.reject(ShareViewControllerError.assertionError(description: "couldn't write UIImage: \(String(describing: error))")) resolver.reject(ShareViewControllerError.assertionError(description: "couldn't write UIImage: \(String(describing: error))"))
} }
@ -799,76 +876,108 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
itemProvider.loadItem(forTypeIdentifier: srcUtiType, options: nil, completionHandler: loadCompletion) itemProvider.loadItem(forTypeIdentifier: srcUtiType, options: nil, completionHandler: loadCompletion)
return promise.then { [weak self] (itemUrl: URL, utiType: String) -> Promise<SignalAttachment> in return promise
guard let strongSelf = self else { }
let error = ShareViewControllerError.obsoleteShare
return Promise(error: error)
}
let url: URL = try {
if strongSelf.isVideoNeedingRelocation(itemProvider: itemProvider, itemUrl: itemUrl) {
return try SignalAttachment.copyToVideoTempDir(url: itemUrl)
} else {
return itemUrl
}
}()
Logger.debug("building DataSource with url: \(url), utiType: \(utiType)") private func buildAttachment(forLoadedItem loadedItem: LoadedItem) -> Promise<SignalAttachment> {
let itemProvider = loadedItem.itemProvider
let itemUrl = loadedItem.itemUrl
let utiType = loadedItem.utiType
guard let dataSource = ShareViewController.createDataSource(utiType: utiType, url: url, customFileName: customFileName) else { var url = itemUrl
throw ShareViewControllerError.assertionError(description: "Unable to read attachment data") do {
if isVideoNeedingRelocation(itemProvider: itemProvider, itemUrl: itemUrl) {
url = try SignalAttachment.copyToVideoTempDir(url: itemUrl)
} }
} catch {
let error = ShareViewControllerError.assertionError(description: "Could not copy video")
return Promise(error: error)
}
// start with base utiType, but it might be something generic like "image" Logger.debug("building DataSource with url: \(url), utiType: \(utiType)")
var specificUTIType = utiType
if utiType == (kUTTypeURL as String) { guard let dataSource = ShareViewController.createDataSource(utiType: utiType, url: url, customFileName: loadedItem.customFileName) else {
// Use kUTTypeURL for URLs. let error = ShareViewControllerError.assertionError(description: "Unable to read attachment data")
} else if UTTypeConformsTo(utiType as CFString, kUTTypeText) { return Promise(error: error)
// Use kUTTypeText for text. }
} else if url.pathExtension.count > 0 {
// Determine a more specific utiType based on file extension // start with base utiType, but it might be something generic like "image"
if let typeExtension = MIMETypeUtil.utiType(forFileExtension: url.pathExtension) { var specificUTIType = utiType
Logger.debug("utiType based on extension: \(typeExtension)") if utiType == (kUTTypeURL as String) {
specificUTIType = typeExtension // Use kUTTypeURL for URLs.
} } else if UTTypeConformsTo(utiType as CFString, kUTTypeText) {
// Use kUTTypeText for text.
} else if url.pathExtension.count > 0 {
// Determine a more specific utiType based on file extension
if let typeExtension = MIMETypeUtil.utiType(forFileExtension: url.pathExtension) {
Logger.debug("utiType based on extension: \(typeExtension)")
specificUTIType = typeExtension
} }
}
guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, dataUTI: specificUTIType) else { guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, dataUTI: specificUTIType) else {
// This can happen, e.g. when sharing a quicktime-video from iCloud drive. // This can happen, e.g. when sharing a quicktime-video from iCloud drive.
let (promise, exportSession) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType) let (promise, exportSession) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType)
// TODO: How can we move waiting for this export to the end of the share flow rather than having to do it up front? // TODO: How can we move waiting for this export to the end of the share flow rather than having to do it up front?
// Ideally we'd be able to start it here, and not block the UI on conversion unless there's still work to be done // Ideally we'd be able to start it here, and not block the UI on conversion unless there's still work to be done
// when the user hits "send". // when the user hits "send".
if let exportSession = exportSession { if let exportSession = exportSession {
let progressPoller = ProgressPoller(timeInterval: 0.1, ratioCompleteBlock: { return exportSession.progress }) let progressPoller = ProgressPoller(timeInterval: 0.1, ratioCompleteBlock: { return exportSession.progress })
strongSelf.progressPoller = progressPoller self.progressPoller = progressPoller
progressPoller.startPolling() progressPoller.startPolling()
guard let loadViewController = strongSelf.loadViewController else { guard let loadViewController = self.loadViewController else {
owsFailDebug("load view controller was unexpectedly nil") owsFailDebug("load view controller was unexpectedly nil")
return promise return promise
} }
DispatchQueue.main.async { DispatchQueue.main.async {
loadViewController.progress = progressPoller.progress loadViewController.progress = progressPoller.progress
}
} }
}
return promise
}
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium)
if loadedItem.isConvertibleToContactShare {
Logger.info("isConvertibleToContactShare")
attachment.isConvertibleToContactShare = true
} else if loadedItem.isConvertibleToTextMessage {
Logger.info("isConvertibleToTextMessage")
attachment.isConvertibleToTextMessage = true
}
return Promise.value(attachment)
}
return promise private func buildAttachments() -> Promise<[SignalAttachment]> {
let promise = selectItemProviders().then { [weak self] (itemProviders) -> Promise<[SignalAttachment]> in
guard let strongSelf = self else {
let error = ShareViewControllerError.assertionError(description: "expired")
return Promise(error: error)
} }
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium) var loadPromises = [Promise<SignalAttachment>]()
if isConvertibleToContactShare { for itemProvider in itemProviders {
Logger.info("isConvertibleToContactShare") let loadPromise = strongSelf.loadItemProvider(itemProvider: itemProvider)
attachment.isConvertibleToContactShare = isConvertibleToContactShare .then({ (loadedItem) -> Promise<SignalAttachment> in
} else if isConvertibleToTextMessage { return strongSelf.buildAttachment(forLoadedItem: loadedItem)
Logger.info("isConvertibleToTextMessage") })
attachment.isConvertibleToTextMessage = isConvertibleToTextMessage
loadPromises.append(loadPromise)
} }
return Promise.value(attachment) return when(fulfilled: loadPromises)
} }.then { (signalAttachments) -> Promise<[SignalAttachment]> in
guard signalAttachments.count > 0 else {
let error = ShareViewControllerError.assertionError(description: "no valid attachments")
return Promise(error: error)
}
return Promise.value(signalAttachments)
}
promise.retainUntilComplete()
return promise
} }
// Some host apps (e.g. iOS Photos.app) sometimes auto-converts some video formats (e.g. com.apple.quicktime-movie) // Some host apps (e.g. iOS Photos.app) sometimes auto-converts some video formats (e.g. com.apple.quicktime-movie)

Loading…
Cancel
Save