|  |  |  | import CoreServices | 
					
						
							|  |  |  | import PromiseKit | 
					
						
							|  |  |  | import SignalUtilitiesKit | 
					
						
							|  |  |  | 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) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         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() | 
					
						
							|  |  |  |                  | 
					
						
							|  |  |  |                 self?.versionMigrationsDidComplete() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // performUpdateCheck must be invoked after Environment has been initialized because | 
					
						
							|  |  |  |                 // upgrade process may depend on Environment. | 
					
						
							|  |  |  |                 self?.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("") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // 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. | 
					
						
							|  |  |  |         // 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() | 
					
						
							|  |  |  |             self?.showLockScreenOrMainContent() | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @objc | 
					
						
							|  |  |  |     public func applicationDidEnterBackground() { | 
					
						
							|  |  |  |         AssertIsOnMainThread() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Logger.info("") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if OWSScreenLock.shared.isScreenLockEnabled() { | 
					
						
							|  |  |  |             self.dismiss(animated: false) { [weak self] in | 
					
						
							|  |  |  |                 AssertIsOnMainThread() | 
					
						
							|  |  |  |                 self?.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 setCurrentAppMode(to appMode: AppMode) { | 
					
						
							|  |  |  |         return // Not applicable to share extensions | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     public func setAppModeToSystemDefault() { | 
					
						
							|  |  |  |         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() | 
					
						
							|  |  |  |         threadPickerVC.shareVC = self | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         setViewControllers([ threadPickerVC ], animated: false) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         let promise = buildAttachments() | 
					
						
							|  |  |  |         ModalActivityIndicatorViewController.present( | 
					
						
							|  |  |  |             fromViewController: self, | 
					
						
							|  |  |  |             canCancel: false, | 
					
						
							|  |  |  |             message: "vc_share_loading_message".localized()) { 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: "BUTTON_OK".localized(), 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 | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         guard let dataSource = DataSourcePath.dataSource(with: url, shouldDeleteOnDeallocation: false) else { | 
					
						
							|  |  |  |             return nil | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Fallback to the last part of the URL | 
					
						
							|  |  |  |         dataSource.sourceFilename = (customFileName ?? 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) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - LoadedItem | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |