session-ios/SignalShareExtension/ShareViewController.swift

480 lines
19 KiB
Swift
Raw Normal View History

//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import UIKit
import SignalMessaging
import PureLayout
import SignalServiceKit
import PromiseKit
2017-12-04 18:38:44 +01:00
@objc
public class ShareViewController: UINavigationController, ShareViewDelegate, SAEFailedViewDelegate {
2017-12-05 22:49:21 +01:00
private var hasInitialRootViewController = false
2017-12-05 23:08:22 +01:00
private var isReadyForAppExtensions = false
2017-12-05 22:49:21 +01:00
2017-12-04 18:38:44 +01:00
override open func loadView() {
super.loadView()
2017-12-07 16:42:10 +01:00
Logger.debug("\(self.logTag) \(#function)")
2017-12-04 18:38:44 +01:00
// This should be the first thing we do.
2017-12-06 21:53:19 +01:00
let appContext = ShareAppExtensionContext(rootViewController:self)
SetCurrentAppContext(appContext)
DebugLogger.shared().enableTTYLogging()
if _isDebugAssertConfiguration() {
DebugLogger.shared().enableFileLogging()
2017-12-06 21:53:19 +01:00
} else if OWSPreferences.isLoggingEnabled() {
DebugLogger.shared().enableFileLogging()
}
_ = AppVersion()
startupLogging()
SetRandFunctionSeed()
2017-12-05 22:49:21 +01:00
// We don't need to use DeviceSleepManager in the SAE.
2017-12-04 18:38:44 +01:00
// TODO:
2017-12-05 22:49:21 +01:00
// [UIUtil applySignalAppearence];
2017-12-04 18:38:44 +01:00
if CurrentAppContext().isRunningTests {
2017-12-04 18:38:44 +01:00
// TODO: Do we need to implement isRunningTests in the SAE context?
return
}
2017-12-07 03:47:03 +01:00
// 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.
2017-12-05 23:08:22 +01:00
isReadyForAppExtensions = OWSPreferences.isReadyForAppExtensions()
if !isReadyForAppExtensions {
2017-12-07 03:47:03 +01:00
// 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.
2017-12-06 21:53:19 +01:00
if OWSPreferences.isRegistered() {
2017-12-06 19:53:01 +01:00
showNotReadyView()
} else {
showNotRegisteredView()
}
2017-12-05 23:08:22 +01:00
return
}
// Don't display load screen immediately, in hopes that we can avoid it altogether.
after(seconds: 2).then { () -> Void in
guard self.presentedViewController == nil else {
Logger.debug("\(self.logTag) setup completed quickly, no need to present load view controller.")
return
}
Logger.debug("\(self.logTag) setup is slow - showing loading screen")
let loadViewController = SAELoadViewController(delegate: self)
let navigationController = UINavigationController(rootViewController: loadViewController)
self.present(navigationController, animated: true)
}.retainUntilComplete()
2017-12-07 22:29:24 +01:00
// We shouldn't set up our environment until after we've consulted isReadyForAppExtensions.
2017-12-07 22:04:52 +01:00
AppSetup.setupEnvironment({
return NoopCallMessageHandler()
}) {
return NoopNotificationsManager()
}
2017-12-04 18:38:44 +01:00
// performUpdateCheck must be invoked after Environment has been initialized because
// upgrade process may depend on Environment.
VersionMigrations.performUpdateCheck()
self.isNavigationBarHidden = true
2017-12-04 18:38:44 +01:00
2017-12-05 22:49:21 +01:00
// We don't need to use "screen protection" in the SAE.
2017-12-04 18:38:44 +01:00
2017-12-07 19:53:13 +01:00
// Ensure OWSContactsSyncing is instantiated.
2017-12-06 21:53:19 +01:00
OWSContactsSyncing.sharedManager()
2017-12-04 18:38:44 +01:00
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)
}
2017-12-05 22:49:21 +01:00
private func activate() {
2017-12-07 16:42:10 +01:00
Logger.debug("\(self.logTag) \(#function)")
2017-12-05 22:49:21 +01:00
// 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()
2017-12-05 23:08:22 +01:00
// We don't need to use RTCInitializeSSL() in the SAE.
2017-12-05 22:49:21 +01:00
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)")
2017-12-05 23:08:22 +01:00
// We don't need to use OWSDisappearingMessagesJob in the SAE.
2017-12-05 22:49:21 +01:00
// 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()
2017-12-05 23:08:22 +01:00
// We don't need to use OWSFailedMessagesJob in the SAE.
2017-12-05 22:49:21 +01:00
2017-12-05 23:08:22 +01:00
// We don't need to use OWSFailedAttachmentDownloadsJob in the SAE.
2017-12-05 22:49:21 +01:00
}
} else {
Logger.info("\(self.logTag) running post launch block for unregistered user.")
2017-12-05 23:08:22 +01:00
// We don't need to update the app icon badge number in the SAE.
2017-12-05 22:49:21 +01:00
2017-12-05 23:08:22 +01:00
// We don't need to prod the TSSocketManager in the SAE.
2017-12-05 22:49:21 +01:00
}
2017-12-05 23:13:52 +01:00
// TODO: Do we want to move this logic into the notification handler for "SAE will appear".
2017-12-05 22:49:21 +01:00
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)")
2017-12-05 23:08:22 +01:00
// We don't need to use the TSSocketManager in the SAE.
2017-12-05 22:49:21 +01:00
Environment.current().contactsManager.fetchSystemContactsOnceIfAlreadyAuthorized()
// We don't need to fetch messages in the SAE.
// We don't need to use OWSSyncPushTokensJob in the SAE.
}
}
}
2017-12-04 18:38:44 +01:00
@objc
func databaseViewRegistrationComplete() {
AssertIsOnMainThread()
2017-12-07 16:42:10 +01:00
Logger.debug("\(self.logTag) \(#function)")
2017-12-05 22:49:21 +01:00
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.
2017-12-05 23:13:52 +01:00
OWSProfileManager.shared().fetchLocalUsersProfile()
OWSReadReceiptManager.shared().prepareCachedValues()
Environment.current().contactsManager.loadLastKnownContactRecipientIds()
2017-12-04 18:38:44 +01:00
}
@objc
func registrationStateDidChange() {
AssertIsOnMainThread()
2017-12-07 16:42:10 +01:00
Logger.debug("\(self.logTag) \(#function)")
2017-12-04 18:38:44 +01:00
2017-12-05 22:49:21 +01:00
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() {
2017-12-07 16:42:10 +01:00
Logger.debug("\(self.logTag) \(#function)")
2017-12-05 22:49:21 +01:00
guard !TSDatabaseView.hasPendingViewRegistrations() else {
return
}
guard !hasInitialRootViewController else {
return
}
hasInitialRootViewController = true
Logger.info("Presenting initial root view controller")
if !TSAccountManager.isRegistered() {
2017-12-06 19:53:01 +01:00
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()
2017-12-05 22:49:21 +01:00
}
// 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")
}
}
2017-12-06 19:53:01 +01:00
// 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) {
let viewController = SAEFailedViewController(delegate:self, title:title, message:message)
self.setViewControllers([viewController], animated: false)
}
// MARK: View Lifecycle
2017-12-04 18:38:44 +01:00
override open func viewDidLoad() {
super.viewDidLoad()
2017-12-07 16:42:10 +01:00
Logger.debug("\(self.logTag) \(#function)")
2017-12-05 23:08:22 +01:00
if isReadyForAppExtensions {
activate()
}
}
2017-12-04 18:38:44 +01:00
override open func viewWillAppear(_ animated: Bool) {
2017-12-07 16:42:10 +01:00
Logger.debug("\(self.logTag) \(#function)")
super.viewWillAppear(animated)
}
2017-12-04 18:38:44 +01:00
override open func viewDidAppear(_ animated: Bool) {
2017-12-07 16:42:10 +01:00
Logger.debug("\(self.logTag) \(#function)")
super.viewDidAppear(animated)
}
2017-12-05 23:08:22 +01:00
override open func viewWillDisappear(_ animated: Bool) {
2017-12-07 16:42:10 +01:00
Logger.debug("\(self.logTag) \(#function)")
2017-12-05 23:08:22 +01:00
super.viewWillDisappear(animated)
Logger.flush()
}
override open func viewDidDisappear(_ animated: Bool) {
2017-12-07 16:42:10 +01:00
Logger.debug("\(self.logTag) \(#function)")
2017-12-05 23:08:22 +01:00
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() {
self.buildAttachment().then { attachment -> Void in
let conversationPicker = SharingThreadPickerViewController(shareViewDelegate: self)
let navigationController = UINavigationController(rootViewController: conversationPicker)
navigationController.isNavigationBarHidden = true
conversationPicker.attachment = attachment
if let presentedViewController = self.presentedViewController {
Logger.debug("\(self.logTag) dismissing \(presentedViewController) before presenting conversation picker")
self.dismiss(animated: true) {
self.present(navigationController, animated: true)
}
} else {
Logger.debug("\(self.logTag) no other modal, presenting conversation picker immediately")
self.present(navigationController, animated: true)
}
2017-12-07 16:21:16 +01:00
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<SignalAttachment> {
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<URL>.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
2017-12-10 22:43:24 +01:00
return promise.then { (url: URL) -> Promise<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) {
2017-12-10 22:17:35 +01:00
Logger.debug("\(self.logTag) utiType based on extension: \(typeExtension)")
specificUTIType = typeExtension
}
}
2017-12-10 22:43:24 +01:00
guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, dataUTI: specificUTIType) else {
// TODO show progress with exportSession
let (promise, exportSession) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium)
// TODO use `exportSession.progress` to show a more precise progress indicator in the loadView, maybe sharing the "sending" progress UI.
// TODO expose "Cancel"
// Can we move this process to the end of the share flow rather than up front?
// Currently we aren't able to generate a proper thumbnail or play the video in the app extension without first converting it.
2017-12-10 22:43:24 +01:00
return promise
}
2017-12-10 22:43:24 +01:00
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType)
return Promise(value: attachment)
}
}
}