@ -15,21 +15,15 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE
private var hasInitialRootViewController = false
private var isReadyForAppExtensions = false
var loadViewController : SAELoadViewController !
private var progressPoller : ProgressPoller ?
var loadViewController : SAELoadViewController ?
let shareViewNavigationController : UINavigationController = UINavigationController ( )
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 )
@ -63,7 +57,7 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE
// 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 {
guard isReadyForAppExtensions else {
// 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 .
@ -75,6 +69,20 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE
return
}
let loadViewController = SAELoadViewController ( delegate : self )
self . loadViewController = loadViewController
// D o n ' t d i s p l a y l o a d s c r e e n i m m e d i a t e l y , i n h o p e s t h a t w e c a n a v o i d i t a l t o g e t h e r .
after ( seconds : 0.5 ) . 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 " )
self . showPrimaryViewController ( loadViewController )
} . retainUntilComplete ( )
// 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 ( )
@ -86,8 +94,6 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE
// 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 .
@ -110,6 +116,7 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE
}
deinit {
Logger . info ( " \( self . logTag ) dealloc " )
NotificationCenter . default . removeObserver ( self )
}
@ -290,15 +297,8 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE
}
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 . s etViewControllers( [ viewController ] , animated : false )
self . showPrimaryViewController ( viewController )
}
// MARK: V i e w L i f e c y c l e
@ -363,20 +363,32 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE
// 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
// T h i s v i e w c o n t r o l l e r i s n o t v i s i b l e t o t h e u s e r . I t e x i s t s t o i n t e r c e p t t o u c h e s , s e t u p t h e
// e x t e n s i o n s d e p e n d e n c i e s , a n d e v e n t u a l l y p r e s e n t a v i s i b l e v i e w t o t h e u s e r .
// F o r s p e e d o f p r e s e n t a t i o n , w e o n l y p r e s e n t a s i n g l e m o d a l , a n d i f i t ' s a l r e a d y b e e n p r e s e n t e d
// w e s w a p o u t t h e c o n t e n t s .
// e . g . i f l o a d i n g i s t a k i n g a w h i l e , t h e u s e r w i l l s e e t h e l o a d s c r e e n p r e s e n t e d w i t h a m o d a l
// a n i m a t i o n . N e x t , w h e n l o a d i n g c o m p l e t e s , t h e l o a d v i e w w i l l b e s w i t c h e d o u t f o r t h e c o n t a c t
// p i c k e r v i e w .
private func showPrimaryViewController ( _ viewController : UIViewController ) {
shareViewNavigationController . setViewControllers ( [ viewController ] , animated : false )
if self . presentedViewController = = nil {
Logger . debug ( " \( self . logTag ) presenting modally: \( viewController ) " )
self . present ( shareViewNavigationController , animated : true )
} else {
Logger . debug ( " \( self . logTag ) modal already presented. swapping modal content for: \( viewController ) " )
assert ( self . presentedViewController = = shareViewNavigationController )
}
}
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
self . present ( navigationController , animated : true , completion : nil )
self . shareViewNavigationController . isNavigationBarHidden = true
self . progressPoller = nil
self . loadViewController = nil
self . showPrimaryViewController ( conversationPicker )
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. " )
@ -448,7 +460,18 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE
// 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
return promise . then { ( itemUrl : URL ) -> Promise < SignalAttachment > in
let url : URL = try {
if self . isVideoNeedingRelocation ( itemProvider : itemProvider , itemUrl : itemUrl ) {
return try SignalAttachment . copyToVideoTempDir ( url : itemUrl )
} else {
return itemUrl
}
} ( )
Logger . debug ( " \( self . logTag ) building DataSource with url: \( url ) " )
guard let dataSource = DataSourcePath . dataSource ( with : url ) else {
throw ShareViewControllerError . assertionError ( description : " Unable to read attachment data " )
}
@ -459,13 +482,114 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE
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 )
guard ! SignalAttachment . isInvalidVideo ( dataSource : dataSource , dataUTI : specificUTIType ) else {
// T h i s c a n h a p p e n , e . g . w h e n s h a r i n g a q u i c k t i m e - v i d e o f r o m i C l o u d d r i v e .
let ( promise , exportSession ) = SignalAttachment . compressVideoAsMp4 ( dataSource : dataSource , dataUTI : specificUTIType )
// TODO: H o w c a n w e m o v e w a i t i n g f o r t h i s e x p o r t t o t h e e n d o f t h e s h a r e f l o w r a t h e r t h a n h a v i n g t o d o i t u p f r o n t ?
// I d e a l l y w e ' d b e a b l e t o s t a r t i t h e r e , a n d n o t b l o c k t h e U I o n c o n v e r s i o n u n l e s s t h e r e ' s s t i l l w o r k t o b e d o n e
// w h e n t h e u s e r h i t s " s e n d " .
if let exportSession = exportSession {
let progressPoller = ProgressPoller ( timeInterval : 0.1 , ratioCompleteBlock : { return exportSession . progress } )
self . progressPoller = progressPoller
progressPoller . startPolling ( )
guard let loadViewController = self . loadViewController else {
owsFail ( " load view controller was unexpectedly nil " )
return promise
}
loadViewController . progress = progressPoller . progress
}
return promise
}
let attachment = SignalAttachment . attachment ( dataSource : dataSource , dataUTI : specificUTIType , imageQuality : . medium )
return Promise ( value : attachment )
}
}
// S o m e h o s t a p p s ( e . g . i O S P h o t o s . a p p ) s o m e t i m e s a u t o - c o n v e r t s s o m e v i d e o f o r m a t s ( e . g . c o m . a p p l e . q u i c k t i m e - m o v i e )
// i n t o m p 4 s a s p a r t o f t h e N S I t e m P r o v i d e r ` l o a d I t e m ` A P I . ( S o m e f i l e s t h e P h o t o ' s a p p d o e s n ' t a u t o - c o n v e r t )
//
// H o w e v e r , w h e n u s i n g t h i s u r l t o t h e c o n v e r t e d i t e m , A V F o u n d a t i o n o p e r a t i o n s s u c h a s g e n e r a t i n g a
// p r e v i e w i m a g e a n d p l a y i n g t h e u r l i n t h e A V M o v i e P l a y e r f a i l s w i t h a n u n h e l p f u l e r r o r : " T h e o p e r a t i o n c o u l d n o t b e c o m p l e t e d "
//
// W e c a n w o r k a r o u n d t h i s b y f i r s t c o p y i n g t h e m e d i a i n t o o u r c o n t a i n e r .
//
// I d o n ' t u n d e r s t a n d w h y t h i s i s , a n d I h a v e n ' t f o u n d a n y r e l e v a n t d o c u m e n t a t i o n i n t h e N S I t e m P r o v i d e r
// o r A V F o u n d a t i o n d o c s .
//
// N o t e s :
//
// T h e s e o p e r a t i o n s s u c c e e d w h e n s e n d i n g a v i d e o w h i c h i n i t i a l l y e x i s t e d o n d i s k a s a n m p 4 .
// ( e . g . A l i c e s e n d s a v i d e o t o B o b t h r o u g h t h e m a i n a p p , w h i c h e n s u r e s i t ' s a n m p 4 . B o b s a v e s i t , t h e n r e - s h a r e s i t )
//
// I * d i d * v e r i f y t h a t t h e s i z e a n d S H A 2 5 6 s u m o f t h e o r i g i n a l u r l m a t c h e s t h a t o f t h e c o p i e d u r l . S o t h e r e
// i s n o d i f f e r e n c e b e t w e e n t h e c o n t e n t s o f t h e f i l e , y e t o n e w o r k s o n e d o e s n ' t .
// P e r h a p s t h e A V F o u n d a t i o n A P I s r e q u i r e s o m e e x t r a f i l e s y s t e m p e r m s s i o n w e d o n ' t h a v e i n t h e
// p a s s e d t h r o u g h U R L .
private func isVideoNeedingRelocation ( itemProvider : NSItemProvider , itemUrl : URL ) -> Bool {
guard MIMETypeUtil . utiType ( forFileExtension : itemUrl . pathExtension ) = = kUTTypeMPEG4 as String else {
// E i t h e r i t ' s n o t a v i d e o o r i t w a s a v i d e o w h i c h w a s n o t a u t o - c o n v e r t e d t o m p 4 .
// N o t a f f e c t e d b y t h e i s s u e .
return false
}
// I f v i d e o f i l e a l r e a d y e x i s t e d o n d i s k a s a n m p 4 , t h e n t h e h o s t a p p d i d n ' t n e e d t o
// a p p l y a n y c o n v e r s i o n , s o n o n e e d t o r e l o c a t e t h e a p p .
return ! itemProvider . registeredTypeIdentifiers . contains ( kUTTypeMPEG4 as String )
}
}
// E x p o s e s a P r o g r e s s o b j e c t , w h o s e p r o g r e s s i s u p d a t e d b y p o l l i n g t h e r e t u r n o f a g i v e n b l o c k
private class ProgressPoller {
let TAG = " [ProgressPoller] "
let progress : Progress
private ( set ) var timer : Timer ?
// H i g h e r n u m b e r o f f e r s h i g h e r g a n u l a r i t y
let progressTotalUnitCount : Int64 = 10000
private let timeInterval : Double
private let ratioCompleteBlock : ( ) -> Float
init ( timeInterval : TimeInterval , ratioCompleteBlock : @ escaping ( ) -> Float ) {
self . timeInterval = timeInterval
self . ratioCompleteBlock = ratioCompleteBlock
self . progress = Progress ( )
progress . totalUnitCount = progressTotalUnitCount
progress . completedUnitCount = Int64 ( ratioCompleteBlock ( ) * Float ( progressTotalUnitCount ) )
}
func startPolling ( ) {
guard self . timer = = nil else {
owsFail ( " already started timer " )
return
}
self . timer = WeakTimer . scheduledTimer ( timeInterval : timeInterval , target : self , userInfo : nil , repeats : true ) { [ weak self ] ( timer ) in
guard let strongSelf = self else {
return
}
let completedUnitCount = Int64 ( strongSelf . ratioCompleteBlock ( ) * Float ( strongSelf . progressTotalUnitCount ) )
strongSelf . progress . completedUnitCount = completedUnitCount
return attachment
if completedUnitCount = = strongSelf . progressTotalUnitCount {
Logger . debug ( " \( strongSelf . TAG ) progress complete " )
timer . invalidate ( )
}
}
}
}