//
// C o p y r i g h t ( c ) 2 0 1 7 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
//
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 ) " )
// W e c a n ' t s h o w t h e c o n v e r s a t i o n p i c k e r u n t i l t h e D B i s s e t u p .
// N o r m a l l y t h i s w i l l o n l y t a k e a m o m e n t , s o r a t h e r t h a n f l i c k e r i n g a n d t h e n h i d i n g t h e l o a d i n g s c r e e n
// W e s t a r t a s i n v i s i b l e , a n d o n l y f a d e i t i n i f i t ' s g o i n g t o t a k e a w h i l e
self . view . alpha = 0
UIView . animate ( withDuration : 0.1 , delay : 0.5 , options : [ . curveEaseInOut ] , animations : {
self . view . alpha = 1
} , completion : nil )
// T h i s s h o u l d b e t h e f i r s t t h i n g w e d o .
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 ( )
// W e d o n ' t n e e d t o u s e D e v i c e S l e e p M a n a g e r i n t h e S A E .
// TODO:
// [ U I U t i l a p p l y S i g n a l A p p e a r e n c e ] ;
if CurrentAppContext ( ) . isRunningTests {
// TODO: D o w e n e e d t o i m p l e m e n t i s R u n n i n g T e s t s i n t h e S A E c o n t e x t ?
return
}
// I f w e h a v e n ' t m i g r a t e d t h e d a t a b a s e f i l e t o t h e s h a r e d d a t a
// d i r e c t o r y w e c a n ' t l o a d i t , a n d t h e r e f o r e c a n ' t i n i t T S S S t o r a g e M a n a g e r ,
// a n d t h e r e f o r e d o n ' t w a n t t o s e t u p m o s t o f o u r m a c h i n e r y ( E n v i r o n m e n t ,
// m o s t o f t h e s i n g l e t o n s , e t c . ) . W e j u s t w a n t t o s h o w a n e r r o r v i e w a n d
// a b o r t .
isReadyForAppExtensions = OWSPreferences . isReadyForAppExtensions ( )
if ! isReadyForAppExtensions {
// I f w e d o n ' t h a v e T S S S t o r a g e M a n a g e r , w e c a n ' t c o n s u l t T S A c c o u n t M a n a g e r
// f o r i s R e g i s t e r e d , s o w e u s e O W S P r e f e r e n c e s w h i c h i s u s u a l l y - a c c u r a t e
// c o p y o f t h a t s t a t e .
if OWSPreferences . isRegistered ( ) {
showNotReadyView ( )
} else {
showNotRegisteredView ( )
}
return
}
// W e s h o u l d n ' t s e t u p o u r e n v i r o n m e n t u n t i l a f t e r w e ' v e c o n s u l t e d i s R e a d y F o r A p p E x t e n s i o n s .
AppSetup . setupEnvironment ( {
return NoopCallMessageHandler ( )
} ) {
return NoopNotificationsManager ( )
}
// p e r f o r m U p d a t e C h e c k m u s t b e i n v o k e d a f t e r E n v i r o n m e n t h a s b e e n i n i t i a l i z e d b e c a u s e
// u p g r a d e p r o c e s s m a y d e p e n d o n E n v i r o n m e n t .
VersionMigrations . performUpdateCheck ( )
self . loadViewController = SAELoadViewController ( delegate : self )
self . pushViewController ( loadViewController , animated : false )
self . isNavigationBarHidden = true
// W e d o n ' t n e e d t o u s e " s c r e e n p r o t e c t i o n " i n t h e S A E .
// E n s u r e O W S C o n t a c t s S y n c i n g i s i n s t a n t i a t e d .
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 ) " )
// W e d o n ' t n e e d t o u s e " s c r e e n p r o t e c t i o n " i n t h e S A E .
ensureRootViewController ( )
// A l w a y s c h e c k p r e k e y s a f t e r a p p l a u n c h e s , a n d s o m e t i m e s c h e c k o n a p p a c t i v a t i o n .
TSPreKeyManager . checkPreKeysIfNecessary ( )
// W e d o n ' t n e e d t o u s e R T C I n i t i a l i z e S S L ( ) i n t h e S A E .
if TSAccountManager . isRegistered ( ) {
// A t t h i s p o i n t , p o t e n t i a l l y l e n g t h y D B l o c k i n g m i g r a t i o n s c o u l d b e r u n n i n g .
// A v o i d b l o c k i n g a p p l a u n c h b y p u t t i n g a l l f u r t h e r p o s s i b l e D B a c c e s s i n a s y n c b l o c k
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 ) " )
// W e d o n ' t n e e d t o u s e O W S D i s a p p e a r i n g M e s s a g e s J o b i n t h e S A E .
// T O D O r e m o v e t h i s o n c e w e ' r e s u r e o u r a p p b o o t p r o c e s s i s c o h e r e n t .
// C u r r e n t l y t h i s h a p p e n s * b e f o r e * d b r e g i s t r a t i o n i s c o m p l e t e w h e n
// l a u n c h i n g t h e a p p d i r e c t l y , b u t * a f t e r * d b r e g i s t r a t i o n i s c o m p l e t e w h e n
// t h e a p p i s l a u n c h e d i n t h e b a c k g r o u n d , e . g . f r o m a v o i p n o t i f i c a t i o n .
OWSProfileManager . shared ( ) . ensureLocalProfileCached ( )
// W e d o n ' t n e e d t o u s e O W S F a i l e d M e s s a g e s J o b i n t h e S A E .
// W e d o n ' t n e e d t o u s e O W S F a i l e d A t t a c h m e n t D o w n l o a d s J o b i n t h e S A E .
}
} else {
Logger . info ( " \( self . logTag ) running post launch block for unregistered user. " )
// W e d o n ' t n e e d t o u p d a t e t h e a p p i c o n b a d g e n u m b e r i n t h e S A E .
// W e d o n ' t n e e d t o p r o d t h e T S S o c k e t M a n a g e r i n t h e S A E .
}
// TODO: D o w e w a n t t o m o v e t h i s l o g i c i n t o t h e n o t i f i c a t i o n h a n d l e r f o r " S A E w i l l a p p e a r " .
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 ) " )
// W e d o n ' t n e e d t o u s e t h e T S S o c k e t M a n a g e r i n t h e S A E .
Environment . current ( ) . contactsManager . fetchSystemContactsOnceIfAlreadyAuthorized ( )
// W e d o n ' t n e e d t o f e t c h m e s s a g e s i n t h e S A E .
// W e d o n ' t n e e d t o u s e O W S S y n c P u s h T o k e n s J o b i n t h e S A E .
}
}
}
@objc
func databaseViewRegistrationComplete ( ) {
AssertIsOnMainThread ( )
Logger . debug ( " \( self . logTag ) \( #function ) " )
if TSAccountManager . isRegistered ( ) {
Logger . info ( " \( self . logTag ) localNumber: \( TSAccountManager . localNumber ) " )
// W e d o n ' t n e e d t o u s e m e s s a g e F e t c h e r J o b i n t h e S A E .
// W e d o n ' t n e e d t o u s e S y n c P u s h T o k e n s J o b i n t h e S A E .
}
// W e d o n ' t n e e d t o u s e D e v i c e S l e e p M a n a g e r i n t h e S A E .
// TODO: S h o u l d w e d i s t i n g u i s h m a i n a p p a n d S A E " c o m p l e t i o n " ?
AppVersion . instance ( ) . appLaunchDidComplete ( )
ensureRootViewController ( )
// W e d o n ' t n e e d t o u s e O W S M e s s a g e R e c e i v e r i n t h e S A E .
// W e d o n ' t n e e d t o u s e O W S B a t c h M e s s a g e P r o c e s s o r i n t h e S A E .
OWSProfileManager . shared ( ) . ensureLocalProfileCached ( )
// W e d o n ' t n e e d t o u s e O W S O r p h a n e d D a t a C l e a n e r i n t h e S A E .
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 ) " )
// W e d o n ' t n e e d t o u s e E x p e r i e n c e U p g r a d e F i n d e r i n t h e S A E .
// W e d o n ' t n e e d t o u s e O W S D i s a p p e a r i n g M e s s a g e s J o b i n t h e S A E .
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 ( ) {
// T h i s i s a r a r e e d g e c a s e , b u t w e w a n t t o e n s u r e t h a t t h e u s e r
// i s h a s a l r e a d y s a v e d t h e i r l o c a l p r o f i l e k e y i n t h e m a i n a p p .
showNotReadyView ( )
} else {
presentConversationPicker ( )
}
// W e d o n ' t u s e t h e A p p U p d a t e N a g i n t h e S A E .
}
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: E r r o r V i e w s
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 ) {
// e n s u r e v i e w i s v i s i b l e .
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: V i e w L i f e c y c l e
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: S h a r e V i e w D e l e g a t e , S A E F a i l e d V i e w D e l e g a t e
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: H e l p e r s
private func presentConversationPicker ( ) {
// p a u s e a n y a n i m a t i o n r e v e a l i n g t h e " l o a d i n g " s c r e e n
self . view . layer . removeAllAnimations ( )
// O n c e w e ' v e p r e s e n t e d t h e c o n v e r s a t i o n p i c k e r , w e h i d e t h e l o a d i n g V C
// s o t h a t i t ' s n o t r e v e a l e d w h e n w e e v e n t u a l l y d i s m i s s t h e s h a r e e x t e n s i o n .
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 < SignalAttachment > {
guard let inputItem : NSExtensionItem = self . extensionContext ? . inputItems . first as ? NSExtensionItem else {
let error = ShareViewControllerError . assertionError ( description : " no input item " )
return Promise ( error : error )
}
// T O D O M u l t i p l e a t t a c h m e n t s . I n t h a t c a s e I ' m u n c l e a r i f w e ' l l
// b e g i v e n m u l t i p l e i n p u t I t e m s o r a s i n g l e i n p u t I t e m w i t h m u l t i p l e a t t a c h m e n t s .
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 ) " )
// O r d e r m a t t e r s i f w e w a n t t o t a k e a d v a n t a g e o f s h a r e c o n v e r s i o n i n l o a d I t e m ,
// T h o u g h c u r r e n t l y w e j u s t u s e " d a t a " f o r m o s t t h i n g s a n d r e l y o n o u r S i g n a l A t t a c h m e n t
// c l a s s t o c o n v e r t t y p e s f o r u s .
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 )
} )
// T O D O a c c e p t o t h e r d a t a t y p e s
// T O D O w h i t e l i s t a t t a c h m e n t t y p e s
// T O D O c o e r c e w h e n n e c e s s a r y a n d p o s s i b l e
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
// s t a r t w i t h b a s e u t i T y p e , b u t i t m i g h t b e s o m e t h i n g g e n e r i c l i k e " i m a g e "
var specificUTIType = utiType
if url . pathExtension . count > 0 {
// D e t e r m i n e a m o r e s p e c i f i c u t i T y p e b a s e d o n f i l e e x t e n s i o n
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
}
}
}