remove old NSItemProvider API usage

This commit is contained in:
Ehren Kret 2024-04-05 12:28:15 -05:00
parent 3550a0704f
commit 02f736ec26
9 changed files with 196 additions and 420 deletions

View File

@ -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 = "<group>"; };
3444E6BA264EDFF200B32E3B /* CVColorOrGradientView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CVColorOrGradientView.swift; sourceTree = "<group>"; };
34480B371FD092A900BC14EF /* SignalShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Bridging-Header.h"; sourceTree = "<group>"; };
34480B381FD092E300BC14EF /* SignalShareExtension-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Prefix.pch"; sourceTree = "<group>"; };
34480B4D1FD0A7A300BC14EF /* DebugLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugLogger.h; sourceTree = "<group>"; };
34480B4E1FD0A7A300BC14EF /* DebugLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugLogger.m; sourceTree = "<group>"; };
344A761024B366F4009D69A5 /* FlagsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlagsViewController.swift; sourceTree = "<group>"; };
@ -3302,7 +3299,6 @@
34EB0CEA26289D8800B62DC3 /* MessageTimerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageTimerView.swift; sourceTree = "<group>"; };
34EB0DF42628D3B200B62DC3 /* ConversationInternalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationInternalViewController.swift; sourceTree = "<group>"; };
34EB0E712629DC2B00B62DC3 /* MessageSelectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageSelectionView.swift; sourceTree = "<group>"; };
34ED55A023D0D59700446E39 /* NSItemProvider+Promises.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSItemProvider+Promises.swift"; sourceTree = "<group>"; };
34EEECF125E846EC00574F0D /* SendPaymentMemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendPaymentMemoViewController.swift; sourceTree = "<group>"; };
34F0566923DA209300265283 /* GroupsV2IncomingChanges.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupsV2IncomingChanges.swift; sourceTree = "<group>"; };
34F1071F26D005340053EF4D /* BatchUpdate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchUpdate.swift; sourceTree = "<group>"; };
@ -3454,8 +3450,6 @@
4CBBFE492306F5D300B37450 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = "<group>"; };
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 = "<group>"; };
4CCB567B23C8D89C004A5731 /* NSItemProvider+TypedAccessors.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSItemProvider+TypedAccessors.h"; sourceTree = "<group>"; };
4CCB567C23C8D89C004A5731 /* NSItemProvider+TypedAccessors.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSItemProvider+TypedAccessors.m"; sourceTree = "<group>"; };
4CD675BD22E7BE35008010D2 /* MediaDismissAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDismissAnimationController.swift; sourceTree = "<group>"; };
4CD675C422E7CF22008010D2 /* ConversationViewController+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+OWS.swift"; sourceTree = "<group>"; };
4CD675C622E7D393008010D2 /* MediaPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPresentationContext.swift; sourceTree = "<group>"; };
@ -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 */,

View File

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

View File

@ -51,17 +51,40 @@
<string>INSendMessageIntent</string>
</array>
<key>NSExtensionActivationRule</key>
<string>SUBQUERY (
extensionItems,
$extensionItem,
<string>
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 &gt;= 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 &gt;= 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 &gt;= 1
).@count == 2
AND SUBQUERY (
extensionItems,
$extensionItem,
SUBQUERY (
$extensionItem.attachments,
$attachment,
ANY $attachment.registeredTypeIdentifiers UTI-EQUALS "public.url"
).@count &gt;= 1
).@count == 1
)
</string>
</dict>
<key>NSExtensionMainStoryboard</key>

View File

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

View File

@ -1,38 +0,0 @@
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
#import <Foundation/Foundation.h>
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

View File

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

View File

@ -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<T>(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 {

View File

@ -7,7 +7,6 @@
#import <UIKit/UIKit.h>
// Separate iOS Frameworks from other imports.
#import "NSItemProvider+TypedAccessors.h"
#import <SignalCoreKit/NSObject+OWS.h>
#import <SignalCoreKit/OWSAsserts.h>
#import <SignalCoreKit/OWSLogs.h>

View File

@ -1,16 +0,0 @@
//
// Copyright 2014 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
#import <Availability.h>
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <SignalCoreKit/OWSAsserts.h>
#import <SignalCoreKit/NSObject+OWS.h>
#import <SignalCoreKit/SignalCoreKit-Swift.h>
#import <SignalCoreKit/SignalCoreKit.h>
#endif