// // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // import UIKit import SignalMessaging import PureLayout import SignalServiceKit import PromiseKit @objc public class ShareViewController: UINavigationController, ShareViewDelegate, SAEFailedViewDelegate { private var hasInitialRootViewController = false private var isReadyForAppExtensions = false var loadViewController: SAELoadViewController! override open func loadView() { super.loadView() Logger.debug("\(self.logTag) \(#function)") // We can't show the conversation picker until the DB is set up. // Normally this will only take a moment, so rather than flickering and then hiding the loading screen // We start as invisible, and only fade it in if it's going to take a while self.view.alpha = 0 UIView.animate(withDuration: 0.1, delay: 0.5, options: [.curveEaseInOut], animations: { self.view.alpha = 1 }, completion: nil) // This should be the first thing we do. let appContext = ShareAppExtensionContext(rootViewController:self) SetCurrentAppContext(appContext) DebugLogger.shared().enableTTYLogging() if _isDebugAssertConfiguration() { DebugLogger.shared().enableFileLogging() } else if OWSPreferences.isLoggingEnabled() { DebugLogger.shared().enableFileLogging() } _ = AppVersion() startupLogging() SetRandFunctionSeed() // We don't need to use DeviceSleepManager in the SAE. // TODO: // [UIUtil applySignalAppearence]; if CurrentAppContext().isRunningTests { // TODO: Do we need to implement isRunningTests in the SAE context? return } // If we haven't migrated the database file to the shared data // directory we can't load it, and therefore can't init TSSStorageManager, // and therefore don't want to setup most of our machinery (Environment, // most of the singletons, etc.). We just want to show an error view and // abort. isReadyForAppExtensions = OWSPreferences.isReadyForAppExtensions() if !isReadyForAppExtensions { // If we don't have TSSStorageManager, we can't consult TSAccountManager // for isRegistered, so we use OWSPreferences which is usually-accurate // copy of that state. if OWSPreferences.isRegistered() { showNotReadyView() } else { showNotRegisteredView() } return } // We shouldn't set up our environment until after we've consulted isReadyForAppExtensions. AppSetup.setupEnvironment({ return NoopCallMessageHandler() }) { return NoopNotificationsManager() } // performUpdateCheck must be invoked after Environment has been initialized because // upgrade process may depend on Environment. VersionMigrations.performUpdateCheck() self.loadViewController = SAELoadViewController(delegate:self) self.pushViewController(loadViewController, animated: false) self.isNavigationBarHidden = true // We don't need to use "screen protection" in the SAE. // Ensure OWSContactsSyncing is instantiated. OWSContactsSyncing.sharedManager() NotificationCenter.default.addObserver(self, selector: #selector(databaseViewRegistrationComplete), name: .DatabaseViewRegistrationComplete, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(registrationStateDidChange), name: .RegistrationStateDidChange, object: nil) Logger.info("\(self.logTag) application: didFinishLaunchingWithOptions completed.") OWSAnalytics.appLaunchDidBegin() } deinit { NotificationCenter.default.removeObserver(self) } private func activate() { Logger.debug("\(self.logTag) \(#function)") // We don't need to use "screen protection" in the SAE. ensureRootViewController() // Always check prekeys after app launches, and sometimes check on app activation. TSPreKeyManager.checkPreKeysIfNecessary() // We don't need to use RTCInitializeSSL() in the SAE. if TSAccountManager.isRegistered() { // At this point, potentially lengthy DB locking migrations could be running. // Avoid blocking app launch by putting all further possible DB access in async block DispatchQueue.global().async { [weak self] in guard let strongSelf = self else { return } Logger.info("\(strongSelf.logTag) running post launch block for registered user: \(TSAccountManager.localNumber)") // We don't need to use OWSDisappearingMessagesJob in the SAE. // TODO remove this once we're sure our app boot process is coherent. // Currently this happens *before* db registration is complete when // launching the app directly, but *after* db registration is complete when // the app is launched in the background, e.g. from a voip notification. OWSProfileManager.shared().ensureLocalProfileCached() // We don't need to use OWSFailedMessagesJob in the SAE. // We don't need to use OWSFailedAttachmentDownloadsJob in the SAE. } } else { Logger.info("\(self.logTag) running post launch block for unregistered user.") // We don't need to update the app icon badge number in the SAE. // We don't need to prod the TSSocketManager in the SAE. } // TODO: Do we want to move this logic into the notification handler for "SAE will appear". if TSAccountManager.isRegistered() { DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return } Logger.info("\(strongSelf.logTag) running post launch block for registered user: \(TSAccountManager.localNumber)") // We don't need to use the TSSocketManager in the SAE. Environment.current().contactsManager.fetchSystemContactsOnceIfAlreadyAuthorized() // We don't need to fetch messages in the SAE. // We don't need to use OWSSyncPushTokensJob in the SAE. } } } @objc func databaseViewRegistrationComplete() { AssertIsOnMainThread() Logger.debug("\(self.logTag) \(#function)") if TSAccountManager.isRegistered() { Logger.info("\(self.logTag) localNumber: \(TSAccountManager.localNumber)") // 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. // TODO: Should we distinguish main app and SAE "completion"? AppVersion.instance().appLaunchDidComplete() ensureRootViewController() // 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 OWSOrphanedDataCleaner in the SAE. OWSProfileManager.shared().fetchLocalUsersProfile() OWSReadReceiptManager.shared().prepareCachedValues() Environment.current().contactsManager.loadLastKnownContactRecipientIds() } @objc func registrationStateDidChange() { AssertIsOnMainThread() Logger.debug("\(self.logTag) \(#function)") if TSAccountManager.isRegistered() { Logger.info("\(self.logTag) localNumber: \(TSAccountManager.localNumber)") // We don't need to use ExperienceUpgradeFinder in the SAE. // We don't need to use OWSDisappearingMessagesJob in the SAE. OWSProfileManager.shared().ensureLocalProfileCached() } } private func ensureRootViewController() { Logger.debug("\(self.logTag) \(#function)") guard !TSDatabaseView.hasPendingViewRegistrations() else { return } guard !hasInitialRootViewController else { return } hasInitialRootViewController = true Logger.info("Presenting initial root view controller") if !TSAccountManager.isRegistered() { showNotRegisteredView() } else if !OWSProfileManager.shared().localProfileExists() { // This is a rare edge case, but we want to ensure that the user // is has already saved their local profile key in the main app. showNotReadyView() } else { presentConversationPicker() } // We don't use the AppUpdateNag in the SAE. } func startupLogging() { Logger.info("iOS Version: \(UIDevice.current.systemVersion)}") let locale = NSLocale.current as NSLocale if let localeIdentifier = locale.object(forKey:NSLocale.Key.identifier) as? String, localeIdentifier.count > 0 { Logger.info("Locale Identifier: \(localeIdentifier)") } else { owsFail("Locale Identifier: Unknown") } if let countryCode = locale.object(forKey:NSLocale.Key.countryCode) as? String, countryCode.count > 0 { Logger.info("Country Code: \(countryCode)") } else { owsFail("Country Code: Unknown") } if let languageCode = locale.object(forKey:NSLocale.Key.languageCode) as? String, languageCode.count > 0 { Logger.info("Language Code: \(languageCode)") } else { owsFail("Language Code: Unknown") } } // MARK: Error Views private func showNotReadyView() { let failureTitle = NSLocalizedString("SHARE_EXTENSION_NOT_YET_MIGRATED_TITLE", comment: "Title indicating that the share extension cannot be used until the main app has been launched at least once.") let failureMessage = NSLocalizedString("SHARE_EXTENSION_NOT_YET_MIGRATED_MESSAGE", comment: "Message indicating that the share extension cannot be used until the main app has been launched at least once.") showErrorView(title:failureTitle, message:failureMessage) } private func showNotRegisteredView() { let failureTitle = NSLocalizedString("SHARE_EXTENSION_NOT_REGISTERED_TITLE", comment: "Title indicating that the share extension cannot be used until the user has registered in the main app.") let failureMessage = NSLocalizedString("SHARE_EXTENSION_NOT_REGISTERED_MESSAGE", comment: "Message indicating that the share extension cannot be used until the user has registered in the main app.") showErrorView(title:failureTitle, message:failureMessage) } private func showErrorView(title: String, message: String) { // ensure view is visible. self.view.layer.removeAllAnimations() UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseInOut], animations: { self.view.alpha = 1 }, completion: nil) let viewController = SAEFailedViewController(delegate:self, title:title, message:message) self.setViewControllers([viewController], animated: false) } // MARK: View Lifecycle override open func viewDidLoad() { super.viewDidLoad() Logger.debug("\(self.logTag) \(#function)") if isReadyForAppExtensions { activate() } } override open func viewWillAppear(_ animated: Bool) { Logger.debug("\(self.logTag) \(#function)") super.viewWillAppear(animated) } override open func viewDidAppear(_ animated: Bool) { Logger.debug("\(self.logTag) \(#function)") super.viewDidAppear(animated) } override open func viewWillDisappear(_ animated: Bool) { Logger.debug("\(self.logTag) \(#function)") super.viewWillDisappear(animated) Logger.flush() } override open func viewDidDisappear(_ animated: Bool) { Logger.debug("\(self.logTag) \(#function)") super.viewDidDisappear(animated) Logger.flush() } // MARK: ShareViewDelegate, SAEFailedViewDelegate public func shareViewWasCompleted() { self.dismiss(animated: true) { self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) } } public func shareViewWasCancelled() { self.dismiss(animated: true) { self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) } } public func shareViewFailed(error: Error) { self.dismiss(animated: true) { self.extensionContext!.cancelRequest(withError: error) } } // MARK: Helpers private func presentConversationPicker() { // pause any animation revealing the "loading" screen self.view.layer.removeAllAnimations() // Once we've presented the conversation picker, we hide the loading VC // so that it's not revealed when we eventually dismiss the share extension. loadViewController.view.isHidden = true self.buildAttachment().then { attachment -> Void in let conversationPicker = SharingThreadPickerViewController(shareViewDelegate: self) let navigationController = UINavigationController(rootViewController: conversationPicker) navigationController.isNavigationBarHidden = true conversationPicker.attachment = attachment self.present(navigationController, animated: true, completion: nil) Logger.info("showing picker with attachment: \(attachment)") }.catch { error in let alertTitle = NSLocalizedString("SHARE_EXTENSION_UNABLE_TO_BUILD_ATTACHMENT_ALERT_TITLE", comment: "Shown when trying to share content to a Signal user for the share extension. Followed by failure details.") OWSAlerts.showAlert(withTitle: alertTitle, message: error.localizedDescription, buttonTitle: CommonStrings.cancelButton) { _ in self.shareViewWasCancelled() } owsFail("\(self.logTag) building attachment failed with error: \(error)") }.retainUntilComplete() } enum ShareViewControllerError: Error { case assertionError(description: String) case unsupportedMedia } private func buildAttachment() -> Promise { guard let inputItem: NSExtensionItem = self.extensionContext?.inputItems.first as? NSExtensionItem else { let error = ShareViewControllerError.assertionError(description: "no input item") return Promise(error: error) } // TODO Multiple attachments. In that case I'm unclear if we'll // be given multiple inputItems or a single inputItem with multiple attachments. guard let itemProvider: NSItemProvider = inputItem.attachments?.first as? NSItemProvider else { let error = ShareViewControllerError.assertionError(description: "No item provider in input item attachments") return Promise(error: error) } Logger.info("\(self.logTag) attachment: \(itemProvider)") // Order matters if we want to take advantage of share conversion in loadItem, // Though currently we just use "data" for most things and rely on our SignalAttachment // class to convert types for us. let utiTypes: [String] = [kUTTypeImage as String, kUTTypeURL as String, kUTTypeData as String] let matchingUtiType = utiTypes.first { (utiType: String) -> Bool in itemProvider.hasItemConformingToTypeIdentifier(utiType) } guard let utiType = matchingUtiType else { let error = ShareViewControllerError.unsupportedMedia return Promise(error: error) } Logger.debug("\(logTag) matched utiType: \(utiType)") let (promise, fulfill, reject) = Promise.pending() itemProvider.loadItem(forTypeIdentifier: utiType, options: nil, completionHandler: { (provider, error) in guard error == nil else { reject(error!) return } guard let url = provider as? URL else { let unexpectedTypeError = ShareViewControllerError.assertionError(description: "unexpected item type: \(String(describing: provider))") reject(unexpectedTypeError) return } fulfill(url) }) // TODO accept other data types // TODO whitelist attachment types // TODO coerce when necessary and possible return promise.then { (url: URL) -> SignalAttachment in guard let dataSource = DataSourcePath.dataSource(with: url) else { throw ShareViewControllerError.assertionError(description: "Unable to read attachment data") } dataSource.sourceFilename = url.lastPathComponent // start with base utiType, but it might be something generic like "image" var specificUTIType = utiType if url.pathExtension.count > 0 { // Determine a more specific utiType based on file extension if let typeExtension = MIMETypeUtil.utiType(forFileExtension: url.pathExtension) { Logger.debug("\(self.logTag) utiType based on extension: \(typeExtension)") specificUTIType = typeExtension } } let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality:.medium) return attachment } } }