//
// 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 , SAELoadViewDelegate , SAEFailedViewDelegate {
private var contactsSyncing : OWSContactsSyncing ?
private var hasInitialRootViewController = false
private var isReadyForAppExtensions = false
override open func loadView ( ) {
super . loadView ( )
Logger . debug ( " \( self . logTag ( ) ) \( #function ) " )
// 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 .
SetCurrentAppContext ( ShareAppExtensionContext ( rootViewController : self ) )
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 .
setupEnvironment ( )
// 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
}
// 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 ( )
let loadViewController = SAELoadViewController ( delegate : self )
self . pushViewController ( loadViewController , animated : false )
self . isNavigationBarHidden = false
// 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 .
contactsSyncing = OWSContactsSyncing ( contactsManager : Environment . current ( ) . contactsManager ,
identityManager : OWSIdentityManager . shared ( ) ,
messageSender : Environment . current ( ) . messageSender ,
profileManager : OWSProfileManager . shared ( ) )
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 ( ) {
// H o m e V i e w C o n t r o l l e r * h o m e V i e w = [ H o m e V i e w C o n t r o l l e r n e w ] ;
// S i g n a l s N a v i g a t i o n C o n t r o l l e r * n a v i g a t i o n C o n t r o l l e r =
// [ [ S i g n a l s N a v i g a t i o n C o n t r o l l e r a l l o c ] i n i t W i t h R o o t V i e w C o n t r o l l e r : h o m e V i e w ] ;
// s e l f . w i n d o w . r o o t V i e w C o n t r o l l e r = n a v i g a t i o n C o n t r o l l e r ;
presentConversationPicker ( )
} else {
showNotRegisteredView ( )
}
// 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 " )
}
}
func setupEnvironment ( ) {
let environment = Release . releaseEnvironment ( )
Environment . setCurrent ( environment )
// E n c r y p t i o n / D e c r y p t i o n m u t a t e s s e s s i o n s t a t e a n d m u s t b e s y n c h r o n i z e d o n a s e r i a l q u e u e .
SessionCipher . setSessionCipherDispatchQueue ( OWSDispatch . sessionStoreQueue ( ) )
let sharedEnv = TextSecureKitEnv ( callMessageHandler : SAECallMessageHandler ( ) ,
contactsManager : Environment . current ( ) . contactsManager ,
messageSender : Environment . current ( ) . messageSender ,
notificationsManager : SAENotificationsManager ( ) ,
profileManager : OWSProfileManager . shared ( ) )
TextSecureKitEnv . setShared ( sharedEnv )
TSStorageManager . shared ( ) . setupDatabase ( safeBlockingMigrations : {
VersionMigrations . runSafeBlockingMigrations ( )
} )
Environment . current ( ) . contactsManager . startObserving ( )
}
// 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 ) {
let viewController = SAEFailedViewController ( delegate : self , title : title , message : message )
self . setViewControllers ( [ viewController ] , animated : false )
self . isNavigationBarHidden = 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 A E L o a d 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 shareExtensionWasCancelled ( ) {
self . extensionContext ! . completeRequest ( returningItems : [ ] , completionHandler : nil )
}
// MARK: H e l p e r s
private func presentConversationPicker ( ) {
let conversationPicker = SendExternalFileViewController ( )
buildAttachment ( ) . then { attachment -> Void in
conversationPicker . attachment = attachment
self . pushViewController ( conversationPicker , animated : true )
Logger . info ( " presented conversation picker with attachment: \( attachment ) " )
} . catch { error in
OWSAlerts . showAlert ( withTitle : 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. " ) ,
message : error . localizedDescription ,
buttonTitle : CommonStrings . cancelButton ) { _ in
self . shareExtensionWasCancelled ( )
}
owsFail ( " \( self . logTag ) building attachment failed with error: \( error ) " )
} . retainUntilComplete ( )
}
enum ShareViewControllerError : Error {
case assertionError ( description : String )
}
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 ) " )
// T O D O s u p p o r t o t h e r u t i T y p e s
let utiType = kUTTypeImage as String
guard itemProvider . hasItemConformingToTypeIdentifier ( utiType ) else {
let error = ShareViewControllerError . assertionError ( description : " only supporting images for now " )
return Promise ( error : error )
}
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 ) {
specificUTIType = typeExtension
}
}
let attachment = SignalAttachment . attachment ( dataSource : dataSource , dataUTI : specificUTIType )
return attachment
}
}
}