mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			647 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			647 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Swift
		
	
| import CoreServices
 | |
| import PromiseKit
 | |
| import SessionUIKit
 | |
| 
 | |
| final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerDelegate {
 | |
|     private var areVersionMigrationsComplete = false
 | |
|     public static var attachmentPrepPromise: Promise<[SignalAttachment]>?
 | |
|     
 | |
|     // MARK: Error
 | |
|     enum ShareViewControllerError: Error {
 | |
|         case assertionError(description: String)
 | |
|         case unsupportedMedia
 | |
|         case notRegistered
 | |
|         case obsoleteShare
 | |
|     }
 | |
|     
 | |
|     // MARK: Lifecycle
 | |
|     override func loadView() {
 | |
|         super.loadView()
 | |
| 
 | |
|         // This should be the first thing we do.
 | |
|         let appContext = ShareAppExtensionContext(rootViewController: self)
 | |
|         SetCurrentAppContext(appContext)
 | |
| 
 | |
|         AppModeManager.configure(delegate: self)
 | |
| 
 | |
|         DebugLogger.shared().enableTTYLogging()
 | |
|         if _isDebugAssertConfiguration() {
 | |
|             DebugLogger.shared().enableFileLogging()
 | |
|         } else if OWSPreferences.isLoggingEnabled() {
 | |
|             DebugLogger.shared().enableFileLogging()
 | |
|         }
 | |
| 
 | |
|         Logger.info("")
 | |
| 
 | |
|         _ = AppVersion.sharedInstance()
 | |
| 
 | |
|         Cryptography.seedRandom()
 | |
| 
 | |
|         // We don't need to use DeviceSleepManager in the SAE.
 | |
| 
 | |
|         // We don't need to use applySignalAppearence in the SAE.
 | |
| 
 | |
|         if CurrentAppContext().isRunningTests {
 | |
|             // TODO: Do we need to implement isRunningTests in the SAE context?
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         AppSetup.setupEnvironment(appSpecificSingletonBlock: {
 | |
|             SSKEnvironment.shared.notificationsManager = NoopNotificationsManager()
 | |
|         }, migrationCompletion: { [weak self] in
 | |
|             AssertIsOnMainThread()
 | |
| 
 | |
|             guard let strongSelf = self else { return }
 | |
| 
 | |
|             // performUpdateCheck must be invoked after Environment has been initialized because
 | |
|             // upgrade process may depend on Environment.
 | |
|             strongSelf.versionMigrationsDidComplete()
 | |
|         })
 | |
| 
 | |
|         // We don't need to use "screen protection" in the SAE.
 | |
| 
 | |
|         NotificationCenter.default.addObserver(self,
 | |
|                                                selector: #selector(storageIsReady),
 | |
|                                                name: .StorageIsReady,
 | |
|                                                object: nil)
 | |
|         NotificationCenter.default.addObserver(self,
 | |
|                                                selector: #selector(applicationDidEnterBackground),
 | |
|                                                name: .OWSApplicationDidEnterBackground,
 | |
|                                                object: nil)
 | |
|     }
 | |
| 
 | |
|     @objc
 | |
|     func versionMigrationsDidComplete() {
 | |
|         AssertIsOnMainThread()
 | |
| 
 | |
|         Logger.debug("")
 | |
| 
 | |
|         areVersionMigrationsComplete = true
 | |
| 
 | |
|         checkIsAppReady()
 | |
|     }
 | |
| 
 | |
|     @objc
 | |
|     func storageIsReady() {
 | |
|         AssertIsOnMainThread()
 | |
| 
 | |
|         Logger.debug("")
 | |
| 
 | |
|         checkIsAppReady()
 | |
|     }
 | |
| 
 | |
|     @objc
 | |
|     func checkIsAppReady() {
 | |
|         AssertIsOnMainThread()
 | |
| 
 | |
|         // App isn't ready until storage is ready AND all version migrations are complete.
 | |
|         guard areVersionMigrationsComplete else {
 | |
|             return
 | |
|         }
 | |
|         guard OWSStorage.isStorageReady() else {
 | |
|             return
 | |
|         }
 | |
|         guard !AppReadiness.isAppReady() else {
 | |
|             // Only mark the app as ready once.
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         SignalUtilitiesKit.Configuration.performMainSetup()
 | |
| 
 | |
|         Logger.debug("")
 | |
| 
 | |
|         // TODO: Once "app ready" logic is moved into AppSetup, move this line there.
 | |
|         OWSProfileManager.shared().ensureLocalProfileCached()
 | |
| 
 | |
|         // Note that this does much more than set a flag;
 | |
|         // it will also run all deferred blocks.
 | |
|         AppReadiness.setAppIsReady()
 | |
| 
 | |
|         // We don't need to use messageFetcherJob in the SAE.
 | |
| 
 | |
|         // We don't need to use SyncPushTokensJob in the SAE.
 | |
| 
 | |
|         // We don't need to use DeviceSleepManager in the SAE.
 | |
| 
 | |
|         AppVersion.sharedInstance().saeLaunchDidComplete()
 | |
| 
 | |
|         showLockScreenOrMainContent()
 | |
| 
 | |
|         // We don't need to use OWSMessageReceiver in the SAE.
 | |
|         // We don't need to use OWSBatchMessageProcessor in the SAE.
 | |
| 
 | |
|         OWSProfileManager.shared().ensureLocalProfileCached()
 | |
| 
 | |
|         // We don't need to use OWSOrphanDataCleaner in the SAE.
 | |
| 
 | |
|         // We don't need to fetch the local profile in the SAE
 | |
| 
 | |
|         OWSReadReceiptManager.shared().prepareCachedValues()
 | |
|     }
 | |
|     
 | |
|     override func viewDidLoad() {
 | |
|         super.viewDidLoad()
 | |
|         AppReadiness.runNowOrWhenAppDidBecomeReady { [weak self] in
 | |
|             AssertIsOnMainThread()
 | |
|             guard let strongSelf = self else { return }
 | |
|             strongSelf.showLockScreenOrMainContent()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     @objc
 | |
|     public func applicationDidEnterBackground() {
 | |
|         AssertIsOnMainThread()
 | |
| 
 | |
|         Logger.info("")
 | |
| 
 | |
|         if OWSScreenLock.shared.isScreenLockEnabled() {
 | |
| 
 | |
|             self.dismiss(animated: false) { [weak self] in
 | |
|                 AssertIsOnMainThread()
 | |
|                 guard let strongSelf = self else { return }
 | |
|                 strongSelf.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     deinit {
 | |
|         NotificationCenter.default.removeObserver(self)
 | |
| 
 | |
|         // Share extensions reside in a process that may be reused between usages.
 | |
|         // That isn't safe; the codebase is full of statics (e.g. singletons) which
 | |
|         // we can't easily clean up.
 | |
|         ExitShareExtension()
 | |
|     }
 | |
| 
 | |
|     // MARK: App Mode
 | |
|     public func getCurrentAppMode() -> AppMode {
 | |
|         guard let window = self.view.window else { return .light }
 | |
|         let userInterfaceStyle = window.traitCollection.userInterfaceStyle
 | |
|         let isLightMode = (userInterfaceStyle == .light || userInterfaceStyle == .unspecified)
 | |
|         return isLightMode ? .light : .dark
 | |
|     }
 | |
| 
 | |
|     public func setCurrentAppMode(to appMode: AppMode) {
 | |
|         return // Not applicable to share extensions
 | |
|     }
 | |
|     
 | |
|     // MARK: Updating
 | |
|     private func showLockScreenOrMainContent() {
 | |
|         if OWSScreenLock.shared.isScreenLockEnabled() {
 | |
|             showLockScreen()
 | |
|         } else {
 | |
|             showMainContent()
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     private func showLockScreen() {
 | |
|         let screenLockVC = SAEScreenLockViewController(shareViewDelegate: self)
 | |
|         setViewControllers([ screenLockVC ], animated: false)
 | |
|     }
 | |
|     
 | |
|     private func showMainContent() {
 | |
|         let threadPickerVC = ThreadPickerVC()
 | |
|         threadPickerVC.shareVC = self
 | |
|         setViewControllers([ threadPickerVC ], animated: false)
 | |
|         let promise = buildAttachments()
 | |
|         ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false, message: NSLocalizedString("vc_share_loading_message", comment: "")) { activityIndicator in
 | |
|             promise.done { _ in
 | |
|                 activityIndicator.dismiss { }
 | |
|             }.catch { _ in
 | |
|                 activityIndicator.dismiss { }
 | |
|             }
 | |
|         }
 | |
|         ShareVC.attachmentPrepPromise = promise
 | |
|     }
 | |
|     
 | |
|     func shareViewWasUnlocked() {
 | |
|         showMainContent()
 | |
|     }
 | |
|     
 | |
|     func shareViewWasCompleted() {
 | |
|         extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
 | |
|     }
 | |
|     
 | |
|     func shareViewWasCancelled() {
 | |
|         extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
 | |
|     }
 | |
|     
 | |
|     func shareViewFailed(error: Error) {
 | |
|         let alert = UIAlertController(title: "Session", message: error.localizedDescription, preferredStyle: .alert)
 | |
|         alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: { _ in
 | |
|             self.extensionContext!.cancelRequest(withError: error)
 | |
|         }))
 | |
|         present(alert, animated: true, completion: nil)
 | |
|     }
 | |
|     
 | |
|     // MARK: Attachment Prep
 | |
|     private class func itemMatchesSpecificUtiType(itemProvider: NSItemProvider, utiType: String) -> Bool {
 | |
|         // URLs, contacts and other special items have to be detected separately.
 | |
|         // Many shares (e.g. pdfs) will register many UTI types and/or conform to kUTTypeData.
 | |
|         guard itemProvider.registeredTypeIdentifiers.count == 1 else {
 | |
|             return false
 | |
|         }
 | |
|         guard let firstUtiType = itemProvider.registeredTypeIdentifiers.first else {
 | |
|             return false
 | |
|         }
 | |
|         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)
 | |
|     }
 | |
| 
 | |
|     private class func isContactItem(itemProvider: NSItemProvider) -> Bool {
 | |
|         return itemMatchesSpecificUtiType(itemProvider: itemProvider,
 | |
|                                           utiType: kUTTypeContact as String)
 | |
|     }
 | |
| 
 | |
|     private class func utiType(itemProvider: NSItemProvider) -> String? {
 | |
|         Logger.info("utiTypeForItem: \(itemProvider.registeredTypeIdentifiers)")
 | |
| 
 | |
|         if isUrlItem(itemProvider: itemProvider) {
 | |
|             return kUTTypeURL as String
 | |
|         } else if isContactItem(itemProvider: itemProvider) {
 | |
|             return kUTTypeContact as String
 | |
|         }
 | |
| 
 | |
|         // Use the first UTI that conforms to "data".
 | |
|         let matchingUtiType = itemProvider.registeredTypeIdentifiers.first { (utiType: String) -> Bool in
 | |
|             UTTypeConformsTo(utiType as CFString, kUTTypeData)
 | |
|         }
 | |
|         return matchingUtiType
 | |
|     }
 | |
| 
 | |
|     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.
 | |
|             //
 | |
|             // NOTE: SharingThreadPickerViewController will try to unpack them
 | |
|             //       and send them as normal text messages if possible.
 | |
|             let urlString = url.absoluteString
 | |
|             return DataSourceValue.dataSource(withOversizeText: urlString)
 | |
|         } else if UTTypeConformsTo(utiType as CFString, kUTTypeText) {
 | |
|             // Share text as oversize text messages.
 | |
|             //
 | |
|             // NOTE: SharingThreadPickerViewController will try to unpack them
 | |
|             //       and send them as normal text messages if possible.
 | |
|             return DataSourcePath.dataSource(with: url,
 | |
|                                              shouldDeleteOnDeallocation: false)
 | |
|         } else {
 | |
|             guard let dataSource = DataSourcePath.dataSource(with: url,
 | |
|                                                              shouldDeleteOnDeallocation: false) else {
 | |
|                 return nil
 | |
|             }
 | |
| 
 | |
|             if let customFileName = customFileName {
 | |
|                 dataSource.sourceFilename = customFileName
 | |
|             } else {
 | |
|                 // Ignore the filename for URLs.
 | |
|                 dataSource.sourceFilename = url.lastPathComponent
 | |
|             }
 | |
|             return dataSource
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     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 {
 | |
|             if isVisualMediaItem(itemProvider: attachment) {
 | |
|                 visualMediaItemProviders.append(attachment)
 | |
|             } 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
 | |
|         // one url attachment and another text attachment, where the the url would be https://some-news.com/articles/123-cat-stuck-in-tree
 | |
|         // and the text attachment would be something like "Breaking news - cat stuck in tree"
 | |
|         //
 | |
|         // 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.
 | |
| 
 | |
|         // 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)
 | |
|         }) {
 | |
|             return [preferredAttachment]
 | |
|         }
 | |
| 
 | |
|         // else return whatever is available
 | |
|         if let itemProvider = inputItem.attachments?.first {
 | |
|             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 = ShareVC.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.
 | |
|         //
 | |
|         // * In the case of "textual" shares (e.g. web URLs and text snippets), we want to
 | |
|         //   coerce the UTI type to kUTTypeURL or kUTTypeText.
 | |
|         // * We want to treat shared files as file attachments.  Therefore we do not
 | |
|         //   want to treat file URLs like web URLs.
 | |
|         // * UTIs aren't very descriptive (there are far more MIME types than UTI types)
 | |
|         //   so in the case of file attachments we try to refine the attachment type
 | |
|         //   using the file extension.
 | |
|         guard let srcUtiType = ShareVC.utiType(itemProvider: itemProvider) else {
 | |
|             let error = ShareViewControllerError.unsupportedMedia
 | |
|             return Promise(error: error)
 | |
|         }
 | |
|         Logger.debug("matched utiType: \(srcUtiType)")
 | |
| 
 | |
|         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
 | |
|             }
 | |
| 
 | |
|             guard let value = value else {
 | |
|                 let missingProviderError = ShareViewControllerError.assertionError(description: "missing item provider")
 | |
|                 resolver.reject(missingProviderError)
 | |
|                 return
 | |
|             }
 | |
| 
 | |
|             Logger.info("value type: \(type(of: value))")
 | |
| 
 | |
|             if let data = value as? Data {
 | |
|                 let customFileName = "Contact.vcf"
 | |
| 
 | |
|                 let customFileExtension = MIMETypeUtil.fileExtension(forUTIType: srcUtiType)
 | |
|                 guard let tempFilePath = OWSFileSystem.writeData(toTemporaryFile: data, fileExtension: customFileExtension) else {
 | |
|                     let writeError = ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))")
 | |
|                     resolver.reject(writeError)
 | |
|                     return
 | |
|                 }
 | |
|                 let fileUrl = URL(fileURLWithPath: tempFilePath)
 | |
|                 resolver.fulfill(LoadedItem(itemProvider: itemProvider,
 | |
|                                             itemUrl: fileUrl,
 | |
|                                             utiType: srcUtiType,
 | |
|                                             customFileName: customFileName,
 | |
|                                             isConvertibleToContactShare: false))
 | |
|             } else if let string = value as? String {
 | |
|                 Logger.debug("string provider: \(string)")
 | |
|                 guard let data = string.filterStringForDisplay().data(using: String.Encoding.utf8) else {
 | |
|                     let writeError = ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))")
 | |
|                     resolver.reject(writeError)
 | |
|                     return
 | |
|                 }
 | |
|                 guard let tempFilePath = OWSFileSystem.writeData(toTemporaryFile: data, fileExtension: "txt") else {
 | |
|                     let writeError = ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))")
 | |
|                     resolver.reject(writeError)
 | |
|                     return
 | |
|                 }
 | |
| 
 | |
|                 let fileUrl = URL(fileURLWithPath: tempFilePath)
 | |
| 
 | |
|                 let isConvertibleToTextMessage = !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String)
 | |
| 
 | |
|                 if UTTypeConformsTo(srcUtiType as CFString, kUTTypeText) {
 | |
|                     resolver.fulfill(LoadedItem(itemProvider: itemProvider,
 | |
|                                                 itemUrl: fileUrl,
 | |
|                                                 utiType: srcUtiType,
 | |
|                                                 isConvertibleToTextMessage: isConvertibleToTextMessage))
 | |
|                 } else {
 | |
|                     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.
 | |
|                 let isConvertibleToTextMessage = (itemProvider.registeredTypeIdentifiers.contains(kUTTypeURL as String) &&
 | |
|                     !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String))
 | |
|                 if isConvertibleToTextMessage {
 | |
|                     resolver.fulfill(LoadedItem(itemProvider: itemProvider,
 | |
|                                                 itemUrl: url,
 | |
|                                                 utiType: kUTTypeURL as String,
 | |
|                                                 isConvertibleToTextMessage: isConvertibleToTextMessage))
 | |
|                 } else {
 | |
|                     resolver.fulfill(LoadedItem(itemProvider: itemProvider,
 | |
|                                                 itemUrl: url,
 | |
|                                                 utiType: srcUtiType,
 | |
|                                                 isConvertibleToTextMessage: isConvertibleToTextMessage))
 | |
|                 }
 | |
|             } else if let image = value as? UIImage {
 | |
|                 if let data = image.pngData() {
 | |
|                     let tempFilePath = OWSFileSystem.temporaryFilePath(withFileExtension: "png")
 | |
|                     do {
 | |
|                         let url = NSURL.fileURL(withPath: tempFilePath)
 | |
|                         try data.write(to: url)
 | |
|                         resolver.fulfill(LoadedItem(itemProvider: itemProvider, itemUrl: url,
 | |
|                                                     utiType: srcUtiType))
 | |
|                     } catch {
 | |
|                         resolver.reject(ShareViewControllerError.assertionError(description: "couldn't write UIImage: \(String(describing: error))"))
 | |
|                     }
 | |
|                 } else {
 | |
|                     resolver.reject(ShareViewControllerError.assertionError(description: "couldn't convert UIImage to PNG: \(String(describing: error))"))
 | |
|                 }
 | |
|             } else {
 | |
|                 // It's unavoidable that we may sometimes receives data types that we
 | |
|                 // don't know how to handle.
 | |
|                 let unexpectedTypeError = ShareViewControllerError.assertionError(description: "unexpected value: \(String(describing: value))")
 | |
|                 resolver.reject(unexpectedTypeError)
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         itemProvider.loadItem(forTypeIdentifier: srcUtiType, options: nil, completionHandler: loadCompletion)
 | |
| 
 | |
|         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 = ShareVC.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, _) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType)
 | |
|             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]> {
 | |
|         return selectItemProviders().then {  [weak self] (itemProviders) -> Promise<[SignalAttachment]> in
 | |
|             guard let strongSelf = self else {
 | |
|                 let error = ShareViewControllerError.assertionError(description: "expired")
 | |
|                 return Promise(error: error)
 | |
|             }
 | |
| 
 | |
|             var loadPromises = [Promise<SignalAttachment>]()
 | |
| 
 | |
|             for itemProvider in itemProviders.prefix(SignalAttachment.maxAttachmentsAllowed) {
 | |
|                 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
 | |
|             }
 | |
|             return signalAttachments
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // 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.
 | |
|     private func isVideoNeedingRelocation(itemProvider: NSItemProvider, itemUrl: URL) -> Bool {
 | |
|         let pathExtension = itemUrl.pathExtension
 | |
|         guard pathExtension.count > 0 else {
 | |
|             Logger.verbose("item URL has no file extension: \(itemUrl).")
 | |
|             return false
 | |
|         }
 | |
|         guard let utiTypeForURL = MIMETypeUtil.utiType(forFileExtension: pathExtension) else {
 | |
|             Logger.verbose("item has unknown UTI type: \(itemUrl).")
 | |
|             return false
 | |
|         }
 | |
|         Logger.verbose("utiTypeForURL: \(utiTypeForURL)")
 | |
|         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
 | |
|         }
 | |
| 
 | |
|         // 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 app.
 | |
|         return !itemProvider.registeredTypeIdentifiers.contains(kUTTypeMPEG4 as String)
 | |
|     }
 | |
| }
 |