//
// 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
private var progressPoller : ProgressPoller ?
var loadViewController : SAELoadViewController ?
let shareViewNavigationController : UINavigationController = UINavigationController ( )
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 .
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 ( )
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 .
if OWSPreferences . isRegistered ( ) {
showNotReadyView ( )
} else {
showNotRegisteredView ( )
}
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 ( )
} ) {
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 . 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 {
Logger . info ( " \( self . logTag ) dealloc " )
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 ( )
Environment . current ( ) . contactsManager . loadSignalAccountsFromCache ( )
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 ( )
}
@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 ) {
let viewController = SAEFailedViewController ( delegate : self , title : title , message : message )
self . showPrimaryViewController ( viewController )
}
// 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
// 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 )
conversationPicker . attachment = attachment
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. " )
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 { ( 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 " )
}
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
}
}
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
if completedUnitCount = = strongSelf . progressTotalUnitCount {
Logger . debug ( " \( strongSelf . TAG ) progress complete " )
timer . invalidate ( )
}
}
}
}