Merge branch 'charlesmchen/multiSendSAE'

This commit is contained in:
Matthew Chen 2018-11-13 13:59:28 -05:00
commit 3ccf0a3c98
15 changed files with 468 additions and 189 deletions

2
Pods

@ -1 +1 @@
Subproject commit 7c5cd466ada8524ddea3b3d9b0fe2f734b882a04
Subproject commit 24b0c88968ddd6f05dd12ced06e453fe4bfb206c

View file

@ -20,6 +20,7 @@
#import <SignalServiceKit/TSInvalidIdentityKeyReceivingErrorMessage.h>
#import <SignalServiceKit/TSThread.h>
#import <SignalServiceKit/UIImage+OWS.h>
#import "DebugUIMessagesAssetLoader.h"
NS_ASSUME_NONNULL_BEGIN
@ -113,14 +114,25 @@ NS_ASSUME_NONNULL_BEGIN
[OWS2FAManager.sharedManager setDefaultRepetitionInterval];
}]];
#ifdef DEBUG
[items addObject:[OWSTableItem subPageItemWithText:@"Share UIImage"
actionBlock:^(UIViewController *viewController) {
UIImage *image =
[UIImage imageWithColor:UIColor.redColor size:CGSizeMake(1.f, 1.f)];
[UIImage imageWithColor:UIColor.redColor size:CGSizeMake(1.f, 1.f)];
[AttachmentSharing showShareUIForUIImage:image];
}]];
[items addObject:[OWSTableItem subPageItemWithText:@"Share 2 Images"
actionBlock:^(UIViewController *viewController) {
[DebugUIMisc shareImages:2];
}]];
[items addObject:[OWSTableItem subPageItemWithText:@"Share 2 Videos"
actionBlock:^(UIViewController *viewController) {
[DebugUIMisc shareVideos:2];
}]];
[items addObject:[OWSTableItem subPageItemWithText:@"Share 2 PDFs"
actionBlock:^(UIViewController *viewController) {
[DebugUIMisc sharePDFs:2];
}]];
#endif
[items
@ -265,6 +277,66 @@ NS_ASSUME_NONNULL_BEGIN
[ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil];
}
#ifdef DEBUG
+ (void)shareAssets:(NSUInteger)count
fromAssetLoaders:(NSArray<DebugUIMessagesAssetLoader *> *)assetLoaders
{
[DebugUIMessagesAssetLoader prepareAssetLoaders:assetLoaders
success:^{
[self shareAssets:count
fromPreparedAssetLoaders:assetLoaders];
}
failure:^{
OWSLogError(@"Could not prepare asset loaders.");
}];
}
+ (void)shareAssets:(NSUInteger)count
fromPreparedAssetLoaders:(NSArray<DebugUIMessagesAssetLoader *> *)assetLoaders
{
__block NSMutableArray<NSURL *> *urls = [NSMutableArray new];
for (NSUInteger i = 0;i < count;i++) {
DebugUIMessagesAssetLoader *assetLoader = assetLoaders[arc4random_uniform((uint32_t) assetLoaders.count)];
NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:assetLoader.filePath.pathExtension];
NSError *error;
[[NSFileManager defaultManager] copyItemAtPath:assetLoader.filePath toPath:filePath error:&error];
OWSAssertDebug(!error);
[urls addObject:[NSURL fileURLWithPath:filePath]];
}
OWSLogVerbose(@"urls: %@", urls);
[AttachmentSharing showShareUIForURLs:urls completion:^{
urls = nil;
}];
}
+ (void)shareImages:(NSUInteger)count
{
[self shareAssets:count
fromAssetLoaders:@[
[DebugUIMessagesAssetLoader jpegInstance],
[DebugUIMessagesAssetLoader tinyPngInstance],
]];
}
+ (void)shareVideos:(NSUInteger)count
{
[self shareAssets:count
fromAssetLoaders:@[
[DebugUIMessagesAssetLoader mp4Instance],
]];
}
+ (void)sharePDFs:(NSUInteger)count
{
[self shareAssets:count
fromAssetLoaders:@[
[DebugUIMessagesAssetLoader tinyPdfInstance],
]];
}
#endif
@end
NS_ASSUME_NONNULL_END

View file

@ -150,7 +150,9 @@ NS_ASSUME_NONNULL_BEGIN
- (UIStatusBarStyle)preferredStatusBarStyle
{
if (OWSWindowManager.sharedManager.hasCall) {
if (!CurrentAppContext().isMainApp) {
return super.preferredStatusBarStyle;
} else if (OWSWindowManager.sharedManager.hasCall) {
// Status bar is overlaying the green "call banner"
return UIStatusBarStyleLightContent;
} else {
@ -165,7 +167,9 @@ NS_ASSUME_NONNULL_BEGIN
[UIView setAnimationsEnabled:NO];
if (@available(iOS 11.0, *)) {
if (OWSWindowManager.sharedManager.hasCall) {
if (!CurrentAppContext().isMainApp) {
self.additionalSafeAreaInsets = UIEdgeInsetsZero;
} else if (OWSWindowManager.sharedManager.hasCall) {
self.additionalSafeAreaInsets = UIEdgeInsetsMake(20, 0, 0, 0);
} else {
self.additionalSafeAreaInsets = UIEdgeInsetsZero;

View file

@ -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"
@ -7,11 +7,12 @@
NS_ASSUME_NONNULL_BEGIN
@class SignalAttachment;
@protocol ShareViewDelegate;
@interface SharingThreadPickerViewController : SelectThreadViewController
@property (nonatomic) SignalAttachment *attachment;
@property (nonatomic) NSArray<SignalAttachment *> *attachments;
- (instancetype)initWithShareViewDelegate:(id<ShareViewDelegate>)shareViewDelegate;

View file

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

View file

@ -144,9 +144,11 @@ public class OWSNavigationBar: UINavigationBar {
}
public override func layoutSubviews() {
guard OWSWindowManager.shared().hasCall() else {
super.layoutSubviews()
return
if CurrentAppContext().isMainApp {
guard OWSWindowManager.shared().hasCall() else {
super.layoutSubviews()
return
}
}
guard #available(iOS 11, *) else {

View file

@ -19,6 +19,8 @@ typedef void (^AttachmentSharingCompletion)(void);
+ (void)showShareUIForURL:(NSURL *)url completion:(nullable AttachmentSharingCompletion)completion;
+ (void)showShareUIForURLs:(NSArray<NSURL *> *)urls completion:(nullable AttachmentSharingCompletion)completion;
+ (void)showShareUIForText:(NSString *)text;
+ (void)showShareUIForText:(NSString *)text completion:(nullable AttachmentSharingCompletion)completion;

View file

@ -40,10 +40,18 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)showShareUIForURL:(NSURL *)url completion:(nullable AttachmentSharingCompletion)completion
{
OWSAssertDebug(url);
[AttachmentSharing showShareUIForActivityItems:@[
url,
]
url,
]
completion:completion];
}
+ (void)showShareUIForURLs:(NSArray<NSURL *> *)urls completion:(nullable AttachmentSharingCompletion)completion
{
OWSAssertDebug(urls.count > 0);
[AttachmentSharing showShareUIForActivityItems:urls
completion:completion];
}

View file

@ -19,11 +19,19 @@ public class ProfileFetcherJob: NSObject {
@objc
public class func run(thread: TSThread) {
guard CurrentAppContext().isMainApp else {
return
}
ProfileFetcherJob().run(recipientIds: thread.recipientIdentifiers)
}
@objc
public class func run(recipientId: String, ignoreThrottling: Bool) {
guard CurrentAppContext().isMainApp else {
return
}
ProfileFetcherJob(ignoreThrottling: ignoreThrottling).run(recipientIds: [recipientId])
}
@ -67,6 +75,13 @@ public class ProfileFetcherJob: NSObject {
public func run(recipientIds: [String]) {
AssertIsOnMainThread()
guard CurrentAppContext().isMainApp else {
// Only refresh profiles in the MainApp to decrease the chance of missed SN notifications
// in the AppExtension for our users who choose not to verify contacts.
owsFailDebug("Should only fetch profiles in the main app")
return
}
backgroundTask = OWSBackgroundTask(label: "\(#function)", completionBlock: { [weak self] status in
AssertIsOnMainThread()
@ -79,13 +94,6 @@ public class ProfileFetcherJob: NSObject {
Logger.error("background task time ran out before profile fetch completed.")
})
if (!CurrentAppContext().isMainApp) {
// Only refresh profiles in the MainApp to decrease the chance of missed SN notifications
// in the AppExtension for our users who choose not to verify contacts.
owsFailDebug("Should only fetch profiles in the main app")
return
}
DispatchQueue.main.async {
for recipientId in recipientIds {
self.updateProfile(recipientId: recipientId)

View file

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

View file

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

View file

@ -86,7 +86,6 @@ NS_ASSUME_NONNULL_BEGIN
_incomingMessageFinder = [[OWSIncomingMessageFinder alloc] initWithPrimaryStorage:primaryStorage];
OWSSingletonAssert();
OWSAssertDebug(CurrentAppContext().isMainApp);
return self;
}

View file

@ -76,10 +76,16 @@ NS_SWIFT_NAME(MessageSender)
- (void)sendAttachment:(DataSource *)dataSource
contentType:(NSString *)contentType
sourceFilename:(nullable NSString *)sourceFilename
albumMessageId:(nullable NSString *)albumMessageId
inMessage:(TSOutgoingMessage *)outgoingMessage
success:(void (^)(void))successHandler
failure:(void (^)(NSError *error))failureHandler;
- (void)sendAttachments:(NSArray<OWSOutgoingAttachmentInfo *> *)attachmentInfos
inMessage:(TSOutgoingMessage *)message
success:(void (^)(void))successHandler
failure:(void (^)(NSError *error))failureHandler;
/**
* Same as `sendAttachment:`, but deletes the local copy of the attachment after sending.
* Used for sending sync request data, not for user visible attachments.

View file

@ -444,6 +444,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[self sendAttachment:dataSource
contentType:contentType
sourceFilename:nil
albumMessageId:nil
inMessage:message
success:successWithDeleteHandler
failure:failureWithDeleteHandler];
@ -452,26 +453,41 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
- (void)sendAttachment:(DataSource *)dataSource
contentType:(NSString *)contentType
sourceFilename:(nullable NSString *)sourceFilename
albumMessageId:(nullable NSString *)albumMessageId
inMessage:(TSOutgoingMessage *)message
success:(void (^)(void))successHandler
failure:(void (^)(NSError *error))failureHandler
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure
{
OWSAssertDebug(dataSource);
NSString *albumMessageId = message.uniqueId;
OWSOutgoingAttachmentInfo *attachmentInfo = [[OWSOutgoingAttachmentInfo alloc] initWithDataSource:dataSource
contentType:contentType
sourceFilename:sourceFilename
caption:nil
albumMessageId:albumMessageId];
[OutgoingMessagePreparer prepareAttachments:@[ attachmentInfo ]
[self sendAttachments:@[
attachmentInfo,
]
inMessage:message
success:success
failure:failure];
}
- (void)sendAttachments:(NSArray<OWSOutgoingAttachmentInfo *> *)attachmentInfos
inMessage:(TSOutgoingMessage *)message
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure
{
OWSAssertDebug(attachmentInfos.count > 0);
[OutgoingMessagePreparer prepareAttachments:attachmentInfos
inMessage:message
completionHandler:^(NSError *_Nullable error) {
if (error) {
failureHandler(error);
failure(error);
return;
}
[self sendMessage:message success:successHandler failure:failureHandler];
[self sendMessage:message success:success failure:failure];
}];
}

View file

@ -26,7 +26,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
private var progressPoller: ProgressPoller?
var loadViewController: SAELoadViewController?
let shareViewNavigationController: OWSNavigationController = OWSNavigationController()
private var shareViewNavigationController: OWSNavigationController?
override open func loadView() {
super.loadView()
@ -70,6 +70,9 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
return
}
let shareViewNavigationController = OWSNavigationController()
self.shareViewNavigationController = shareViewNavigationController
let loadViewController = SAELoadViewController(delegate: self)
self.loadViewController = loadViewController
@ -331,7 +334,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
} else {
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else { return }
strongSelf.buildAttachmentAndPresentConversationPicker()
strongSelf.buildAttachmentsAndPresentConversationPicker()
}
}
@ -452,6 +455,10 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
// If user is registered, do nothing.
return
}
guard let shareViewNavigationController = shareViewNavigationController else {
owsFailDebug("Missing shareViewNavigationController")
return
}
guard let firstViewController = shareViewNavigationController.viewControllers.first else {
// If no view has been presented yet, do nothing.
return
@ -513,6 +520,10 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
private func showPrimaryViewController(_ viewController: UIViewController) {
AssertIsOnMainThread()
guard let shareViewNavigationController = shareViewNavigationController else {
owsFailDebug("Missing shareViewNavigationController")
return
}
shareViewNavigationController.setViewControllers([viewController], animated: false)
if self.presentedViewController == nil {
Logger.debug("presenting modally: \(viewController)")
@ -523,10 +534,10 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
}
}
private func buildAttachmentAndPresentConversationPicker() {
private func buildAttachmentsAndPresentConversationPicker() {
AssertIsOnMainThread()
self.buildAttachment().map { [weak self] attachment in
self.buildAttachments().map { [weak self] attachments in
AssertIsOnMainThread()
guard let strongSelf = self else { return }
@ -535,9 +546,9 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
let conversationPicker = SharingThreadPickerViewController(shareViewDelegate: strongSelf)
Logger.debug("presentConversationPicker: \(conversationPicker)")
conversationPicker.attachment = attachment
conversationPicker.attachments = attachments
strongSelf.showPrimaryViewController(conversationPicker)
Logger.info("showing picker with attachment: \(attachment)")
Logger.info("showing picker with attachments: \(attachments)")
}.catch { [weak self] error in
AssertIsOnMainThread()
guard let strongSelf = self else { return }
@ -574,6 +585,11 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
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 {
return itemMatchesSpecificUtiType(itemProvider: itemProvider,
utiType: kUTTypeURL as String)
@ -600,26 +616,6 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
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? {
if utiType == (kUTTypeURL as String) {
// Share URLs as oversize text messages whose text content is the URL.
@ -651,10 +647,28 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
}
}
private func buildAttachment() -> Promise<SignalAttachment> {
guard let inputItem: NSExtensionItem = self.extensionContext?.inputItems.first as? NSExtensionItem else {
let error = ShareViewControllerError.assertionError(description: "no input item")
return Promise(error: error)
private class func preferredItemProviders(inputItem: NSExtensionItem) -> [NSItemProvider]? {
guard let attachments = inputItem.attachments else {
return nil
}
var visualMediaItemProviders = [NSItemProvider]()
var hasNonVisualMedia = false
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)
} else {
hasNonVisualMedia = true
}
}
// Only allow multiple-attachment sends if all attachments
// are visual media.
if visualMediaItemProviders.count > 0 && !hasNonVisualMedia {
return visualMediaItemProviders
}
// A single inputItem can have multiple attachments, e.g. sharing from Firefox gives
@ -664,10 +678,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
// 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.
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)
}
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)")
// We need to be very careful about which UTI type we use.
@ -685,17 +764,12 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
}
Logger.debug("matched utiType: \(srcUtiType)")
let (promise, resolver) = Promise<(itemUrl: URL, utiType: String)>.pending()
var customFileName: String?
var isConvertibleToTextMessage = false
var isConvertibleToContactShare = false
let (promise, resolver) = Promise<LoadedItem>.pending()
let loadCompletion: NSItemProvider.CompletionHandler = { [weak self]
(value, error) in
guard let _ = self else { return }
guard error == nil else {
resolver.reject(error!)
return
@ -710,11 +784,13 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
Logger.info("value type: \(type(of: value))")
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
// sure they are shared with a reasonable filename.
if ShareViewController.itemMatchesSpecificUtiType(itemProvider: itemProvider,
utiType: kUTTypeVCard as String) {
customFileName = "Contact.vcf"
if Contact(vCardData: data) != nil {
isConvertibleToContactShare = true
@ -733,7 +809,11 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
return
}
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 {
Logger.debug("string provider: \(string)")
guard let data = string.filterStringForDisplay().data(using: String.Encoding.utf8) else {
@ -749,21 +829,33 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
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) {
resolver.fulfill((itemUrl: fileUrl, utiType: srcUtiType))
resolver.fulfill(LoadedItem(itemProvider: itemProvider,
itemUrl: fileUrl,
utiType: srcUtiType,
isConvertibleToTextMessage: isConvertibleToTextMessage))
} 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 {
// 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))
if isConvertibleToTextMessage {
resolver.fulfill((itemUrl: url, utiType: kUTTypeURL as String))
resolver.fulfill(LoadedItem(itemProvider: itemProvider,
itemUrl: url,
utiType: kUTTypeURL as String,
isConvertibleToTextMessage: isConvertibleToTextMessage))
} 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 {
if let data = UIImagePNGRepresentation(image) {
@ -771,7 +863,8 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
do {
let url = NSURL.fileURL(withPath: tempFilePath)
try data.write(to: url)
resolver.fulfill((url, srcUtiType))
resolver.fulfill(LoadedItem(itemProvider: itemProvider, itemUrl: url,
utiType: srcUtiType))
} catch {
resolver.reject(ShareViewControllerError.assertionError(description: "couldn't write UIImage: \(String(describing: error))"))
}
@ -788,76 +881,108 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
itemProvider.loadItem(forTypeIdentifier: srcUtiType, options: nil, completionHandler: loadCompletion)
return promise.then { [weak self] (itemUrl: URL, utiType: String) -> Promise<SignalAttachment> in
return promise
}
private func buildAttachment(forLoadedItem loadedItem: LoadedItem) -> Promise<SignalAttachment> {
let itemProvider = loadedItem.itemProvider
let itemUrl = loadedItem.itemUrl
let utiType = loadedItem.utiType
var url = itemUrl
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)
}
Logger.debug("building DataSource with url: \(url), utiType: \(utiType)")
guard let dataSource = ShareViewController.createDataSource(utiType: utiType, url: url, customFileName: loadedItem.customFileName) else {
let error = ShareViewControllerError.assertionError(description: "Unable to read attachment data")
return Promise(error: error)
}
// start with base utiType, but it might be something generic like "image"
var specificUTIType = utiType
if utiType == (kUTTypeURL as String) {
// 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 {
// This can happen, e.g. when sharing a quicktime-video from iCloud drive.
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?
// 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".
if let exportSession = exportSession {
let progressPoller = ProgressPoller(timeInterval: 0.1, ratioCompleteBlock: { return exportSession.progress })
self.progressPoller = progressPoller
progressPoller.startPolling()
guard let loadViewController = self.loadViewController else {
owsFailDebug("load view controller was unexpectedly nil")
return promise
}
DispatchQueue.main.async {
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)
}
private func buildAttachments() -> Promise<[SignalAttachment]> {
let promise = selectItemProviders().then { [weak self] (itemProviders) -> Promise<[SignalAttachment]> in
guard let strongSelf = self else {
let error = ShareViewControllerError.obsoleteShare
let error = ShareViewControllerError.assertionError(description: "expired")
return Promise(error: error)
}
let url: URL = try {
if strongSelf.isVideoNeedingRelocation(itemProvider: itemProvider, itemUrl: itemUrl) {
return try SignalAttachment.copyToVideoTempDir(url: itemUrl)
} else {
return itemUrl
var loadPromises = [Promise<SignalAttachment>]()
for itemProvider in itemProviders {
let loadPromise = strongSelf.loadItemProvider(itemProvider: itemProvider)
.then({ (loadedItem) -> Promise<SignalAttachment> in
return strongSelf.buildAttachment(forLoadedItem: loadedItem)
})
loadPromises.append(loadPromise)
}
return when(fulfilled: loadPromises)
}.map { (signalAttachments) -> [SignalAttachment] in
guard signalAttachments.count > 0 else {
let error = ShareViewControllerError.assertionError(description: "no valid attachments")
throw error
}
}()
Logger.debug("building DataSource with url: \(url), utiType: \(utiType)")
guard let dataSource = ShareViewController.createDataSource(utiType: utiType, url: url, customFileName: customFileName) else {
throw ShareViewControllerError.assertionError(description: "Unable to read attachment data")
return signalAttachments
}
// start with base utiType, but it might be something generic like "image"
var specificUTIType = utiType
if utiType == (kUTTypeURL as String) {
// 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 {
// This can happen, e.g. when sharing a quicktime-video from iCloud drive.
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?
// 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".
if let exportSession = exportSession {
let progressPoller = ProgressPoller(timeInterval: 0.1, ratioCompleteBlock: { return exportSession.progress })
strongSelf.progressPoller = progressPoller
progressPoller.startPolling()
guard let loadViewController = strongSelf.loadViewController else {
owsFailDebug("load view controller was unexpectedly nil")
return promise
}
DispatchQueue.main.async {
loadViewController.progress = progressPoller.progress
}
}
return promise
}
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium)
if isConvertibleToContactShare {
Logger.info("isConvertibleToContactShare")
attachment.isConvertibleToContactShare = isConvertibleToContactShare
} else if isConvertibleToTextMessage {
Logger.info("isConvertibleToTextMessage")
attachment.isConvertibleToTextMessage = isConvertibleToTextMessage
}
return Promise.value(attachment)
}
promise.retainUntilComplete()
return promise
}
// Some host apps (e.g. iOS Photos.app) sometimes auto-converts some video formats (e.g. com.apple.quicktime-movie)