From 02f736ec264b5a0aaa6c7813c123ac705df4fcae Mon Sep 17 00:00:00 2001 From: Ehren Kret Date: Fri, 5 Apr 2024 12:28:15 -0500 Subject: [PATCH] remove old NSItemProvider API usage --- Signal.xcodeproj/project.pbxproj | 12 - .../Attachments/SignalAttachment.swift | 11 +- SignalShareExtension/Info.plist | 41 +- .../NSItemProvider+Promises.swift | 84 ---- .../NSItemProvider+TypedAccessors.h | 38 -- .../NSItemProvider+TypedAccessors.m | 42 -- .../ShareViewController.swift | 371 ++++++++---------- .../SignalShareExtension-Bridging-Header.h | 1 - .../SignalShareExtension-Prefix.pch | 16 - 9 files changed, 196 insertions(+), 420 deletions(-) delete mode 100644 SignalShareExtension/NSItemProvider+Promises.swift delete mode 100644 SignalShareExtension/NSItemProvider+TypedAccessors.h delete mode 100644 SignalShareExtension/NSItemProvider+TypedAccessors.m delete mode 100644 SignalShareExtension/SignalShareExtension-Prefix.pch diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 5e5efda967..16ec7c8b89 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -415,7 +415,6 @@ 34EB0CEB26289D8800B62DC3 /* MessageTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EB0CEA26289D8800B62DC3 /* MessageTimerView.swift */; }; 34EB0DF52628D3B300B62DC3 /* ConversationInternalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EB0DF42628D3B200B62DC3 /* ConversationInternalViewController.swift */; }; 34EB0E722629DC2B00B62DC3 /* MessageSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EB0E712629DC2B00B62DC3 /* MessageSelectionView.swift */; }; - 34ED55A123D0D59700446E39 /* NSItemProvider+Promises.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34ED55A023D0D59700446E39 /* NSItemProvider+Promises.swift */; }; 34EEECF225E846ED00574F0D /* SendPaymentMemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EEECF125E846EC00574F0D /* SendPaymentMemoViewController.swift */; }; 34F1072026D005340053EF4D /* BatchUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F1071F26D005340053EF4D /* BatchUpdate.swift */; }; 34F1072226D045290053EF4D /* BatchUpdateTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F1072126D045290053EF4D /* BatchUpdateTest.swift */; }; @@ -556,7 +555,6 @@ 4CBBFE4A2306F5D300B37450 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBBFE492306F5D300B37450 /* LogViewController.swift */; }; 4CC1ECF9211A47CE00CC13BE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */; }; - 4CCB567D23C8D89C004A5731 /* NSItemProvider+TypedAccessors.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CCB567C23C8D89C004A5731 /* NSItemProvider+TypedAccessors.m */; }; 4CD4E7D523E8CCFE00834B1B /* IndividualCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D23D0D23CEBF6000B0E74B /* IndividualCall.swift */; }; 4CD4E7D623E8CCFE00834B1B /* AudioSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D23D2D23CEC1BE00B0E74B /* AudioSource.swift */; }; 4CD675BE22E7BE35008010D2 /* MediaDismissAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD675BD22E7BE35008010D2 /* MediaDismissAnimationController.swift */; }; @@ -2962,7 +2960,6 @@ 34429B3C273440420050D3EA /* DebugUIMisc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugUIMisc.swift; sourceTree = ""; }; 3444E6BA264EDFF200B32E3B /* CVColorOrGradientView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CVColorOrGradientView.swift; sourceTree = ""; }; 34480B371FD092A900BC14EF /* SignalShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Bridging-Header.h"; sourceTree = ""; }; - 34480B381FD092E300BC14EF /* SignalShareExtension-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Prefix.pch"; sourceTree = ""; }; 34480B4D1FD0A7A300BC14EF /* DebugLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugLogger.h; sourceTree = ""; }; 34480B4E1FD0A7A300BC14EF /* DebugLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugLogger.m; sourceTree = ""; }; 344A761024B366F4009D69A5 /* FlagsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlagsViewController.swift; sourceTree = ""; }; @@ -3302,7 +3299,6 @@ 34EB0CEA26289D8800B62DC3 /* MessageTimerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageTimerView.swift; sourceTree = ""; }; 34EB0DF42628D3B200B62DC3 /* ConversationInternalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationInternalViewController.swift; sourceTree = ""; }; 34EB0E712629DC2B00B62DC3 /* MessageSelectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageSelectionView.swift; sourceTree = ""; }; - 34ED55A023D0D59700446E39 /* NSItemProvider+Promises.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSItemProvider+Promises.swift"; sourceTree = ""; }; 34EEECF125E846EC00574F0D /* SendPaymentMemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendPaymentMemoViewController.swift; sourceTree = ""; }; 34F0566923DA209300265283 /* GroupsV2IncomingChanges.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupsV2IncomingChanges.swift; sourceTree = ""; }; 34F1071F26D005340053EF4D /* BatchUpdate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchUpdate.swift; sourceTree = ""; }; @@ -3454,8 +3450,6 @@ 4CBBFE492306F5D300B37450 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = ""; }; 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateNag.swift; sourceTree = ""; }; - 4CCB567B23C8D89C004A5731 /* NSItemProvider+TypedAccessors.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSItemProvider+TypedAccessors.h"; sourceTree = ""; }; - 4CCB567C23C8D89C004A5731 /* NSItemProvider+TypedAccessors.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSItemProvider+TypedAccessors.m"; sourceTree = ""; }; 4CD675BD22E7BE35008010D2 /* MediaDismissAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDismissAnimationController.swift; sourceTree = ""; }; 4CD675C422E7CF22008010D2 /* ConversationViewController+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+OWS.swift"; sourceTree = ""; }; 4CD675C622E7D393008010D2 /* MediaPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPresentationContext.swift; sourceTree = ""; }; @@ -6765,9 +6759,6 @@ children = ( 4535186F1FC635DD00210559 /* Info.plist */, 4535186C1FC635DD00210559 /* MainInterface.storyboard */, - 34ED55A023D0D59700446E39 /* NSItemProvider+Promises.swift */, - 4CCB567B23C8D89C004A5731 /* NSItemProvider+TypedAccessors.h */, - 4CCB567C23C8D89C004A5731 /* NSItemProvider+TypedAccessors.m */, 347850561FD86544007B8332 /* SAEFailedViewController.swift */, 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */, 7677E40E29F79BF300AC6A75 /* SAEScreenLockViewController.swift */, @@ -6777,7 +6768,6 @@ 88EFF4FB25AD4230000FAFBA /* SharingThreadPickerViewController.swift */, 881FF30723B5B16F0023B620 /* SignalShareExtension-AppStore.entitlements */, 34480B371FD092A900BC14EF /* SignalShareExtension-Bridging-Header.h */, - 34480B381FD092E300BC14EF /* SignalShareExtension-Prefix.pch */, 34B0796E1FD07B1E00E248C2 /* SignalShareExtension.entitlements */, ); path = SignalShareExtension; @@ -12683,8 +12673,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 34ED55A123D0D59700446E39 /* NSItemProvider+Promises.swift in Sources */, - 4CCB567D23C8D89C004A5731 /* NSItemProvider+TypedAccessors.m in Sources */, 347850571FD86544007B8332 /* SAEFailedViewController.swift in Sources */, 3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */, 7677E40F29F79BF300AC6A75 /* SAEScreenLockViewController.swift in Sources */, diff --git a/SignalServiceKit/Attachments/SignalAttachment.swift b/SignalServiceKit/Attachments/SignalAttachment.swift index 5cf4f6cbe8..026af7b920 100644 --- a/SignalServiceKit/Attachments/SignalAttachment.swift +++ b/SignalServiceKit/Attachments/SignalAttachment.swift @@ -1239,14 +1239,13 @@ public class SignalAttachment: NSObject { } } - public class func isVideoThatNeedsCompression(dataSource: DataSource, dataUTI: String) -> Bool { - guard videoUTISet.contains(dataUTI) else { - // not a video - return false - } + public func isVideoThatNeedsCompression() -> Bool { + Self.isVideoThatNeedsCompression(dataSource: self.dataSource, dataUTI: self.dataUTI) + } + public class func isVideoThatNeedsCompression(dataSource: DataSource, dataUTI: String) -> Bool { // Today we re-encode all videos for the most consistent experience. - return true + return videoUTISet.contains(dataUTI) } private class func isValidOutputVideo(dataSource: DataSource?, dataUTI: String) -> Bool { diff --git a/SignalShareExtension/Info.plist b/SignalShareExtension/Info.plist index 54c5c0ebcb..bb7842be6d 100644 --- a/SignalShareExtension/Info.plist +++ b/SignalShareExtension/Info.plist @@ -51,17 +51,40 @@ INSendMessageIntent NSExtensionActivationRule - SUBQUERY ( - extensionItems, - $extensionItem, + SUBQUERY ( - $extensionItem.attachments, - $attachment, - ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data" - || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" - || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.apple.pkpass" - ).@count >= 1 + extensionItems, + $extensionItem, + SUBQUERY ( + $extensionItem.attachments, + $attachment, + ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data" + OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" + OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.apple.pkpass" + ).@count >= 1 ).@count == 1 + OR ( + SUBQUERY ( + extensionItems, + $extensionItem, + SUBQUERY ( + $extensionItem.attachments, + $attachment, + ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data" + OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" + OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.apple.pkpass" + ).@count >= 1 + ).@count == 2 + AND SUBQUERY ( + extensionItems, + $extensionItem, + SUBQUERY ( + $extensionItem.attachments, + $attachment, + ANY $attachment.registeredTypeIdentifiers UTI-EQUALS "public.url" + ).@count >= 1 + ).@count == 1 + ) NSExtensionMainStoryboard diff --git a/SignalShareExtension/NSItemProvider+Promises.swift b/SignalShareExtension/NSItemProvider+Promises.swift deleted file mode 100644 index 3a7eb4ba64..0000000000 --- a/SignalShareExtension/NSItemProvider+Promises.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Foundation - -extension NSItemProvider { - @MainActor - func loadUrl(forTypeIdentifier typeIdentifier: String) async throws -> URL { - try await withCheckedThrowingContinuation { continuation in - self.ows_loadUrl(forTypeIdentifier: typeIdentifier, options: nil) { url, error in - if let error = error { - continuation.resume(throwing: error) - return - } - - guard let url = url else { - continuation.resume(throwing: OWSAssertionError("url was unexpectedly nil")) - return - } - - continuation.resume(returning: url) - } - } - } - - @MainActor - func loadData(forTypeIdentifier typeIdentifier: String) async throws -> Data { - try await withCheckedThrowingContinuation { continuation in - self.ows_loadData(forTypeIdentifier: typeIdentifier, options: nil) { data, error in - if let error = error { - continuation.resume(throwing: error) - return - } - - guard let data = data else { - continuation.resume(throwing: OWSAssertionError("data was unexpectedly nil")) - return - } - - continuation.resume(returning: data) - } - } - } - - @MainActor - func loadText(forTypeIdentifier typeIdentifier: String) async throws -> String { - try await withCheckedThrowingContinuation { continuation in - self.ows_loadText(forTypeIdentifier: typeIdentifier, options: nil) { text, error in - if let error = error { - continuation.resume(throwing: error) - return - } - - guard let text = text else { - continuation.resume(throwing: OWSAssertionError("text was unexpectedly nil")) - return - } - - continuation.resume(returning: text) - } - } - } - - @MainActor - func loadImage(forTypeIdentifier typeIdentifier: String) async throws -> UIImage { - try await withCheckedThrowingContinuation { continuation in - self.ows_loadImage(forTypeIdentifier: typeIdentifier, options: nil) { image, error in - if let error = error { - continuation.resume(throwing: error) - return - } - - guard let image = image else { - continuation.resume(throwing: OWSAssertionError("image was unexpectedly nil")) - return - } - - continuation.resume(returning: image) - } - } - } -} diff --git a/SignalShareExtension/NSItemProvider+TypedAccessors.h b/SignalShareExtension/NSItemProvider+TypedAccessors.h deleted file mode 100644 index f5eb0eb551..0000000000 --- a/SignalShareExtension/NSItemProvider+TypedAccessors.h +++ /dev/null @@ -1,38 +0,0 @@ -// -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -/// The value yield by NSItemProvider.loadItemForTypeIdentifier depends on the signature of the -/// completion handler you pass in. However, the Swift compiler mandates that the completion handler exactly matches the -/// signature, which yields an NSSecureCoding instance. -/// -/// This would generally yield a usable object (Data, URL, String, etc), but in some cases, -/// e.g. sharing a large PDF from Mail.app, we were yielded an unusable private Apple class. -/// -/// To address this, we define a bespoke ObjC method for each type we'd want to be yielded. -@interface NSItemProvider (TypedAccessors) - -- (void)ows_loadUrlForTypeIdentifier:(NSString *)typeIdentifier - options:(nullable NSDictionary *)options - completionHandler:(void (^_Nullable)(NSURL *_Nullable, NSError *_Nullable))completionHandler; - -- (void)ows_loadDataForTypeIdentifier:(NSString *)typeIdentifier - options:(nullable NSDictionary *)options - completionHandler:(void (^_Nullable)(NSData *_Nullable, NSError *_Nullable))completionHandler; - -- (void)ows_loadTextForTypeIdentifier:(NSString *)typeIdentifier - options:(nullable NSDictionary *)options - completionHandler:(void (^_Nullable)(NSString *_Nullable, NSError *_Nullable))completionHandler; - -- (void)ows_loadImageForTypeIdentifier:(NSString *)typeIdentifier - options:(nullable NSDictionary *)options - completionHandler:(void (^_Nullable)(UIImage *_Nullable, NSError *_Nullable))completionHandler; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalShareExtension/NSItemProvider+TypedAccessors.m b/SignalShareExtension/NSItemProvider+TypedAccessors.m deleted file mode 100644 index efb17dddea..0000000000 --- a/SignalShareExtension/NSItemProvider+TypedAccessors.m +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -#import "NSItemProvider+TypedAccessors.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation NSItemProvider (TypedAccessors) - -- (void)ows_loadUrlForTypeIdentifier:(NSString *)typeIdentifier - options:(nullable NSDictionary *)options - completionHandler:(void (^_Nullable)(NSURL *_Nullable, NSError *_Nullable))completionHandler -{ - [self loadItemForTypeIdentifier:typeIdentifier options:options completionHandler:completionHandler]; -} - -- (void)ows_loadDataForTypeIdentifier:(NSString *)typeIdentifier - options:(nullable NSDictionary *)options - completionHandler:(void (^_Nullable)(NSData *_Nullable, NSError *_Nullable))completionHandler -{ - [self loadItemForTypeIdentifier:typeIdentifier options:options completionHandler:completionHandler]; -} - -- (void)ows_loadTextForTypeIdentifier:(NSString *)typeIdentifier - options:(nullable NSDictionary *)options - completionHandler:(void (^_Nullable)(NSString *_Nullable, NSError *_Nullable))completionHandler -{ - [self loadItemForTypeIdentifier:typeIdentifier options:options completionHandler:completionHandler]; -} - -- (void)ows_loadImageForTypeIdentifier:(NSString *)typeIdentifier - options:(nullable NSDictionary *)options - completionHandler:(void (^_Nullable)(UIImage *_Nullable, NSError *_Nullable))completionHandler -{ - [self loadItemForTypeIdentifier:typeIdentifier options:options completionHandler:completionHandler]; -} - -NS_ASSUME_NONNULL_END - -@end diff --git a/SignalShareExtension/ShareViewController.swift b/SignalShareExtension/ShareViewController.swift index cef91ea88a..5086082b9b 100644 --- a/SignalShareExtension/ShareViewController.swift +++ b/SignalShareExtension/ShareViewController.swift @@ -20,8 +20,16 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed case tooManyAttachments case nilInputItems case noInputItems + case noConformingInputItem case nilAttachments case noAttachments + case cannotLoadURLObject + case loadURLObjectFailed + case cannotLoadStringObject + case loadStringObjectFailed + case loadDataRepresentationFailed + case loadInPlaceFileRepresentationFailed + case nonFileUrl } private var hasInitialRootViewController = false @@ -474,9 +482,18 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed guard let inputItems = self.extensionContext?.inputItems as? [NSExtensionItem] else { throw ShareViewControllerError.nilInputItems } - guard let inputItem = inputItems.first else { - throw ShareViewControllerError.noInputItems + #if DEBUG + for (inputItemIndex, inputItem) in inputItems.enumerated() { + Logger.debug("- inputItems[\(inputItemIndex)]") + for (itemProvidersIndex, itemProviders) in inputItem.attachments!.enumerated() { + Logger.debug(" - itemProviders[\(itemProvidersIndex)]") + for typeIdentifier in itemProviders.registeredTypeIdentifiers { + Logger.debug(" - \(typeIdentifier)") + } + } } + #endif + let inputItem = try Self.selectExtensionItem(inputItems) guard let itemProviders = inputItem.attachments else { throw ShareViewControllerError.nilAttachments } @@ -486,8 +503,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed let typedItemProviders = try Self.typedItemProviders(for: itemProviders) self.conversationPicker.areAttachmentStoriesCompatPrecheck = typedItemProviders.allSatisfy { $0.isStoriesCompatible } - let loadedItems = try await self.loadItems(unloadedItems: typedItemProviders) - let attachments = try await self.buildAttachments(loadedItems: loadedItems) + let attachments = try await self.buildAttachments(for: typedItemProviders) try Task.checkCancellation() // Make sure the user is not trying to share more than our attachment limit. @@ -542,6 +558,27 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed Logger.info("showing screen lock") } + private static func selectExtensionItem(_ extensionItems: [NSExtensionItem]) throws -> NSExtensionItem { + if extensionItems.isEmpty { + throw ShareViewControllerError.noInputItems + } + if extensionItems.count == 1 { + return extensionItems.first! + } + + // Handle safari sharing images and PDFs as two separate items one with the object to share and the other as the URL of the data. + for extensionItem in extensionItems { + for attachment in extensionItem.attachments ?? [] { + if attachment.hasItemConformingToTypeIdentifier(kUTTypeData as String) + || attachment.hasItemConformingToTypeIdentifier(kUTTypeFileURL as String) + || attachment.hasItemConformingToTypeIdentifier("com.apple.pkpass") { + return extensionItem + } + } + } + throw ShareViewControllerError.noConformingInputItem + } + private struct TypedItemProvider { enum ItemType { case movie @@ -552,7 +589,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed case text case pdf case pkPass - case other + case data var typeIdentifier: String { switch self { @@ -572,8 +609,8 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed kUTTypePDF as String case .pkPass: "com.apple.pkpass" - case .other: - "" + case .data: + kUTTypeData as String } } } @@ -593,7 +630,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed switch itemType { case .movie, .image, .webUrl, .text: return true - case .fileUrl, .contact, .pdf, .pkPass, .other: + case .fileUrl, .contact, .pdf, .pkPass, .data: return false } } @@ -601,15 +638,15 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed private static func typedItemProviders(for itemProviders: [NSItemProvider]) throws -> [TypedItemProvider] { // due to UT conformance fallbacks the order these are checked is important; more specific types need to come earlier in the list than their fallbacks - let itemTypeOrder: [TypedItemProvider.ItemType] = [.movie, .image, .fileUrl, .webUrl, .contact, .text, .pdf, .pkPass] - let candidates: [TypedItemProvider] = itemProviders.map { itemProvider in + let itemTypeOrder: [TypedItemProvider.ItemType] = [.movie, .image, .contact, .text, .pdf, .pkPass, .fileUrl, .webUrl, .data] + let candidates: [TypedItemProvider] = try itemProviders.map { itemProvider in for itemType in itemTypeOrder { if itemProvider.hasItemConformingToTypeIdentifier(itemType.typeIdentifier) { return TypedItemProvider(itemProvider: itemProvider, itemType: itemType) } } owsFailDebug("unexpected share item: \(itemProvider)") - return TypedItemProvider(itemProvider: itemProvider, itemType: .other) + throw ShareViewControllerError.unsupportedMedia } // URL shares can come in with text preview and favicon attachments so we ignore other attachments with a URL @@ -622,232 +659,142 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed return visualMediaCandidates.isEmpty ? Array(candidates.prefix(1)) : visualMediaCandidates } - private struct LoadedItem { - enum LoadedItemPayload { - case fileUrl(_ fileUrl: URL, registeredTypeIdentifiers: [String]) - case inMemoryImage(_ image: UIImage) - case webUrl(_ webUrl: URL) - case contact(_ contactData: Data) - case text(_ text: String) - case pdf(_ data: Data) - case pkPass(_ data: Data) - } - - let payload: LoadedItemPayload - } - - private func loadItems(unloadedItems: [TypedItemProvider]) async throws -> [LoadedItem] { - try await withThrowingTaskGroup(of: LoadedItem.self) { group in - for unloadedItem in unloadedItems { - _ = group.addTaskUnlessCancelled { - try await self.loadItem(unloadedItem: unloadedItem) - } - } - - var result: [LoadedItem] = [] - for try await loadedItem in group { - result.append(loadedItem) - } - return result - } - } - - private func loadItem(unloadedItem: TypedItemProvider) async throws -> LoadedItem { - Logger.info("unloadedItem: \(unloadedItem)") - - let itemProvider = unloadedItem.itemProvider - - switch unloadedItem.itemType { - case .movie: - return LoadedItem(payload: .fileUrl(try await itemProvider.loadUrl(forTypeIdentifier: kUTTypeMovie as String), - registeredTypeIdentifiers: itemProvider.registeredTypeIdentifiers)) - case .image: - // When multiple image formats are available, kUTTypeImage will - // defer to jpeg when possible. On iPhone 12 Pro, when 'heic' - // and 'jpeg' are the available options, the 'jpeg' data breaks - // UIImage (and underlying) in some unclear way such that trying - // to perform any kind of transformation on the image (such as - // resizing) causes memory to balloon uncontrolled. Luckily, - // iOS 14 provides native UIImage support for heic and iPhone - // 12s can only be running iOS 14+, so we can request the heic - // format directly, which behaves correctly for all our needs. - // A radar has been opened with apple reporting this issue. - let desiredTypeIdentifier: String - if #available(iOS 14, *), itemProvider.registeredTypeIdentifiers.contains("public.heic") { - desiredTypeIdentifier = "public.heic" - } else { - desiredTypeIdentifier = kUTTypeImage as String - } - do { - return LoadedItem(payload: .fileUrl(try await itemProvider.loadUrl(forTypeIdentifier: desiredTypeIdentifier), - registeredTypeIdentifiers: itemProvider.registeredTypeIdentifiers)) - } catch let error as NSError where error.domain == NSItemProvider.errorDomain && error.code == NSItemProvider.ErrorCode.unexpectedValueClassError.rawValue { - // If a URL wasn't available, fall back to an in-memory image. - // One place this happens is when sharing from the screenshot app on iOS13. - return LoadedItem(payload: .inMemoryImage(try await itemProvider.loadImage(forTypeIdentifier: kUTTypeImage as String))) - } - case .webUrl: - return LoadedItem(payload: .webUrl(try await itemProvider.loadUrl(forTypeIdentifier: kUTTypeURL as String))) - case .fileUrl: - return LoadedItem(payload: .fileUrl(try await itemProvider.loadUrl(forTypeIdentifier: kUTTypeFileURL as String), - registeredTypeIdentifiers: itemProvider.registeredTypeIdentifiers)) - case .contact: - return LoadedItem(payload: .contact(try await itemProvider.loadData(forTypeIdentifier: kUTTypeContact as String))) - case .text: - return LoadedItem(payload: .text(try await itemProvider.loadText(forTypeIdentifier: kUTTypeText as String))) - case .pdf: - return LoadedItem(payload: .pdf(try await itemProvider.loadData(forTypeIdentifier: kUTTypePDF as String))) - case .pkPass: - return LoadedItem(payload: .pkPass(try await itemProvider.loadData(forTypeIdentifier: "com.apple.pkpass"))) - case .other: - return LoadedItem(payload: .fileUrl(try await itemProvider.loadUrl(forTypeIdentifier: kUTTypeFileURL as String), - registeredTypeIdentifiers: itemProvider.registeredTypeIdentifiers)) - } - } - - nonisolated private func buildAttachments(loadedItems: [LoadedItem]) async throws -> [SignalAttachment] { + nonisolated private func buildAttachments(for typedItemProviders: [TypedItemProvider]) async throws -> [SignalAttachment] { try await withThrowingTaskGroup(of: SignalAttachment.self) { group in - for loadedItem in loadedItems { + for typedItemProvider in typedItemProviders { _ = group.addTaskUnlessCancelled { - try await self.buildAttachment(loadedItem: loadedItem) + try await self.buildAttachment(for: typedItemProvider) } } var result: [SignalAttachment] = [] - for try await signalAttachment in group { - result.append(signalAttachment) + for try await attachment in group { + result.append(attachment) } return result } } - /// Creates an attachment with from a generic "loaded item". The data source - /// backing the returned attachment must "own" the data it provides - i.e., - /// it must not refer to data/files that other components refer to. - nonisolated private func buildAttachment(loadedItem: LoadedItem) async throws -> SignalAttachment { - switch loadedItem.payload { - case .webUrl(let webUrl): - let dataSource = DataSourceValue.dataSource(withOversizeText: webUrl.absoluteString) - let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeText as String) - attachment.isConvertibleToTextMessage = true - return attachment - case .contact(let contactData): + nonisolated private func buildAttachment(for typedItemProvider: TypedItemProvider) async throws -> SignalAttachment { + let itemProvider = typedItemProvider.itemProvider + switch typedItemProvider.itemType { + case .movie, .image, .pdf, .data: + return try await self.buildFileAttachment(fromItemProvider: itemProvider, forTypeIdentifier: typedItemProvider.itemType.typeIdentifier) + case .fileUrl: + let url: URL = try await Self.loadObject(fromItemProvider: itemProvider, cannotLoadError: .cannotLoadURLObject, failedLoadError: .loadURLObjectFailed) + let attachment = try Self.copyAttachment(fromUrl: url) + return try await self.compressVideo(attachment: attachment) + case .webUrl: + let url: URL = try await Self.loadObject(fromItemProvider: itemProvider, cannotLoadError: .cannotLoadURLObject, failedLoadError: .loadURLObjectFailed) + return Self.createAttachment(withText: url.absoluteString) + case .contact: + let contactData = try await Self.loadDataRepresentation(fromItemProvider: itemProvider, forTypeIdentifier: kUTTypeContact as String) let dataSource = DataSourceValue.dataSource(with: contactData, utiType: kUTTypeContact as String) let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeContact as String) attachment.isConvertibleToContactShare = true return attachment - case .text(let text): - let dataSource = DataSourceValue.dataSource(withOversizeText: text) - let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeText as String) - attachment.isConvertibleToTextMessage = true - return attachment - case let .fileUrl(originalItemUrl, registeredTypeIdentifiers): - var itemUrl = originalItemUrl - do { - if Self.isVideoNeedingRelocation(registeredTypeIdentifiers: registeredTypeIdentifiers, itemUrl: itemUrl) { - itemUrl = try SignalAttachment.copyToVideoTempDir(url: itemUrl) - } - } catch { - throw ShareViewControllerError.assertionError(description: "Could not copy video") - } - - guard let dataSource = try? DataSourcePath.dataSource(with: itemUrl, shouldDeleteOnDeallocation: false) else { - throw ShareViewControllerError.assertionError(description: "Attachment URL was not a file URL") - } - dataSource.sourceFilename = itemUrl.lastPathComponent - - let utiType = MIMETypeUtil.utiType(forFileExtension: itemUrl.pathExtension) ?? kUTTypeData as String - - if SignalAttachment.isVideoThatNeedsCompression(dataSource: dataSource, dataUTI: utiType) { - // This can happen, e.g. when sharing a quicktime-video from iCloud drive. - - // 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". - return try await SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: utiType, sessionCallback: { exportSession in - Task { @MainActor in - let progressPoller = ProgressPoller(timeInterval: 0.1, ratioCompleteBlock: { return exportSession.progress }) - - self.progressPoller = progressPoller - progressPoller.startPolling() - - self.loadViewController.progress = progressPoller.progress - } - }) - } - - let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: utiType) - - // If we already own the attachment's data - i.e. we have copied it - // from the URL originally passed in, and therefore no one else can - // be referencing it - we can return the attachment as-is... - if attachment.dataUrl != originalItemUrl { - return attachment - } - - // ...otherwise, we should clone the attachment to ensure we aren't - // touching data someone else might be referencing. - do { - return try attachment.cloneAttachment() - } catch { - throw ShareViewControllerError.assertionError(description: "Failed to clone attachment") - } - case .inMemoryImage(let image): - guard let pngData = image.pngData() else { - throw OWSAssertionError("pngData was unexpectedly nil") - } - let dataSource = DataSourceValue.dataSource(with: pngData, fileExtension: "png") - let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypePNG as String) - return attachment - case .pdf(let pdf): - let dataSource = DataSourceValue.dataSource(with: pdf, fileExtension: "pdf") - let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypePDF as String) - return attachment - case .pkPass(let pkPass): + case .text: + let text: String = try await Self.loadObject(fromItemProvider: itemProvider, cannotLoadError: .cannotLoadStringObject, failedLoadError: .loadStringObjectFailed) + return Self.createAttachment(withText: text) + case .pkPass: + let typeIdentifier = "com.apple.pkpass" + let pkPass = try await Self.loadDataRepresentation(fromItemProvider: itemProvider, forTypeIdentifier: typeIdentifier) let dataSource = DataSourceValue.dataSource(with: pkPass, fileExtension: "pkpass") - let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: "com.apple.pkpass") + let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: typeIdentifier) return attachment } } - // Some host apps (e.g. iOS Photos.app) sometimes auto-converts some video formats (e.g. com.apple.quicktime-movie) - // into mp4s as part of the NSItemProvider `loadItem` API. (Some files the Photo's app doesn't auto-convert) - // - // However, when using this url to the converted item, AVFoundation operations such as generating a - // preview image and playing the url in the AVMoviePlayer fails with an unhelpful error: "The operation could not be completed" - // - // We can work around this by first copying the media into our container. - // - // I don't understand why this is, and I haven't found any relevant documentation in the NSItemProvider - // or AVFoundation docs. - // - // Notes: - // - // These operations succeed when sending a video which initially existed on disk as an mp4. - // (e.g. Alice sends a video to Bob through the main app, which ensures it's an mp4. Bob saves it, then re-shares it) - // - // I *did* verify that the size and SHA256 sum of the original url matches that of the copied url. So there - // is no difference between the contents of the file, yet one works one doesn't. - // Perhaps the AVFoundation APIs require some extra file system permssion we don't have in the - // passed through URL. - nonisolated static private func isVideoNeedingRelocation(registeredTypeIdentifiers: [String], itemUrl: URL) -> Bool { - let pathExtension = itemUrl.pathExtension - if pathExtension.isEmpty { - return false + nonisolated private static func copyAttachment(fromUrl url: URL, defaultTypeIdentifier: String = kUTTypeData as String) throws -> SignalAttachment { + guard let dataSource = try? DataSourcePath.dataSource(with: url, shouldDeleteOnDeallocation: false) else { + throw ShareViewControllerError.nonFileUrl } - guard let utiTypeForURL = MIMETypeUtil.utiType(forFileExtension: pathExtension) else { - return false + dataSource.sourceFilename = url.lastPathComponent + let utiType = MIMETypeUtil.utiType(forFileExtension: url.pathExtension) ?? defaultTypeIdentifier + let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: utiType) + return try attachment.cloneAttachment() + } + + nonisolated private func compressVideo(attachment: SignalAttachment) async throws -> SignalAttachment { + if attachment.isVideoThatNeedsCompression() { + // TODO: Move waiting for this export to the end of the share flow rather than up front + return try await SignalAttachment.compressVideoAsMp4(dataSource: attachment.dataSource, dataUTI: attachment.dataUTI, sessionCallback: { exportSession in + Task { @MainActor in + let progressPoller = ProgressPoller(timeInterval: 0.1, ratioCompleteBlock: { return exportSession.progress }) + + self.progressPoller = progressPoller + progressPoller.startPolling() + + self.loadViewController.progress = progressPoller.progress + } + }) + } else { + return attachment } - guard utiTypeForURL == kUTTypeMPEG4 as String else { - // Either it's not a video or it was a video which was not auto-converted to mp4. - // Not affected by the issue. - return false + } + + nonisolated private func buildFileAttachment(fromItemProvider itemProvider: NSItemProvider, forTypeIdentifier typeIdentifier: String) async throws -> SignalAttachment { + let attachment: SignalAttachment = try await withCheckedThrowingContinuation { continuation in + _ = itemProvider.loadInPlaceFileRepresentation(forTypeIdentifier: typeIdentifier, completionHandler: { fileUrl, _, error in + if let error { + continuation.resume(throwing: error) + } else if let fileUrl { + do { + // NOTE: Compression here rather than creating an additional temp file would be nice but blocking this completion handler for video encoding is probably not a good way to go. + continuation.resume(returning: try Self.copyAttachment(fromUrl: fileUrl, defaultTypeIdentifier: typeIdentifier)) + } catch { + continuation.resume(throwing: error) + } + } else { + continuation.resume(throwing: ShareViewControllerError.loadInPlaceFileRepresentationFailed) + } + }) } - // If video file already existed on disk as an mp4, then the host app didn't need to - // apply any conversion, so no need to relocate the file. - return !registeredTypeIdentifiers.contains(kUTTypeMPEG4 as String) + return try await self.compressVideo(attachment: attachment) } + + nonisolated private static func loadDataRepresentation(fromItemProvider itemProvider: NSItemProvider, forTypeIdentifier typeIdentifier: String) async throws -> Data { + try await withCheckedThrowingContinuation { continuation in + _ = itemProvider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { data, error in + if let error { + continuation.resume(throwing: error) + } else if let data { + continuation.resume(returning: data) + } else { + continuation.resume(throwing: ShareViewControllerError.loadDataRepresentationFailed) + } + } + } + } + + nonisolated private static func loadObject(fromItemProvider itemProvider: NSItemProvider, + cannotLoadError: ShareViewControllerError, + failedLoadError: ShareViewControllerError) async throws -> T + where T: _ObjectiveCBridgeable, T._ObjectiveCType: NSItemProviderReading { + guard itemProvider.canLoadObject(ofClass: T.self) else { + throw cannotLoadError + } + return try await withCheckedThrowingContinuation { continuation in + _ = itemProvider.loadObject(ofClass: T.self) { object, error in + if let error { + continuation.resume(throwing: error) + } else if let object { + continuation.resume(returning: object) + } else { + continuation.resume(throwing: failedLoadError) + } + } + } + } + + nonisolated private static func createAttachment(withText text: String) -> SignalAttachment { + let dataSource = DataSourceValue.dataSource(withOversizeText: text) + let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeText as String) + attachment.isConvertibleToTextMessage = true + return attachment + } + } extension ShareViewController: UIAdaptivePresentationControllerDelegate { diff --git a/SignalShareExtension/SignalShareExtension-Bridging-Header.h b/SignalShareExtension/SignalShareExtension-Bridging-Header.h index 91baa3c72d..ea1b5ef99b 100644 --- a/SignalShareExtension/SignalShareExtension-Bridging-Header.h +++ b/SignalShareExtension/SignalShareExtension-Bridging-Header.h @@ -7,7 +7,6 @@ #import // Separate iOS Frameworks from other imports. -#import "NSItemProvider+TypedAccessors.h" #import #import #import diff --git a/SignalShareExtension/SignalShareExtension-Prefix.pch b/SignalShareExtension/SignalShareExtension-Prefix.pch deleted file mode 100644 index 2a559dc4e9..0000000000 --- a/SignalShareExtension/SignalShareExtension-Prefix.pch +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright 2014 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -#import - -#ifdef __OBJC__ - #import - #import - - #import - #import - #import - #import -#endif