1 // Copyright 2015-present 650 Industries. All rights reserved. 2 3 import Foundation 4 import EXUpdates 5 6 @objc(EXHomeAppLoaderTaskDelegate) 7 public protocol HomeAppLoaderTaskDelegate: AnyObject { homeAppLoaderTasknull8 func homeAppLoaderTask(_: HomeAppLoaderTask, didFinishWithLauncher launcher: AppLauncher) 9 func homeAppLoaderTask(_: HomeAppLoaderTask, didFinishWithError error: Error) 10 } 11 12 /** 13 Derivation of AppLoaderTask from expo-updates that just does a simple one-loader using HomeAppLoader. 14 */ 15 @objc(EXHomeAppLoaderTask) 16 @objcMembers 17 public final class HomeAppLoaderTask: NSObject { 18 private static let ErrorDomain = "HomeAppLoaderTask" 19 20 public weak var delegate: HomeAppLoaderTaskDelegate? 21 22 private let manifestAndAssetRequestHeaders: ManifestAndAssetRequestHeaders 23 private let config: UpdatesConfig 24 private let database: UpdatesDatabase 25 private let directory: URL 26 private let selectionPolicy: SelectionPolicy 27 private let delegateQueue: DispatchQueue 28 29 private let loaderTaskQueue: DispatchQueue 30 31 public required init( 32 manifestAndAssetRequestHeaders: ManifestAndAssetRequestHeaders, 33 config: UpdatesConfig, 34 database: UpdatesDatabase, 35 directory: URL, 36 selectionPolicy: SelectionPolicy, 37 delegateQueue: DispatchQueue 38 ) { 39 self.manifestAndAssetRequestHeaders = manifestAndAssetRequestHeaders 40 self.config = config 41 self.database = database 42 self.directory = directory 43 self.selectionPolicy = selectionPolicy 44 self.delegateQueue = delegateQueue 45 self.loaderTaskQueue = DispatchQueue(label: "expo.loader.LoaderTaskQueue") 46 } 47 48 public func start() { 49 self.loadHomeUpdate { remoteError, remoteUpdate in 50 self.loaderTaskQueue.async { 51 self.launchUpdate(remoteUpdate?.manifestUpdateResponsePart?.updateManifest, error: remoteError) 52 } 53 } 54 } 55 56 private func finish(withLauncher launcher: AppLauncher?, error: Error?) { 57 dispatchPrecondition(condition: .onQueue(loaderTaskQueue)) 58 59 if let delegate = delegate { 60 delegateQueue.async { 61 // swiftlint:disable force_unwrapping 62 if let launcher = launcher, 63 launcher.launchAssetUrl != nil || launcher.launchedUpdate!.status == .StatusDevelopment { 64 delegate.homeAppLoaderTask(self, didFinishWithLauncher: launcher) 65 } else { 66 delegate.homeAppLoaderTask( 67 self, 68 didFinishWithError: error ?? NSError( 69 domain: HomeAppLoaderTask.ErrorDomain, 70 code: 1031, 71 userInfo: [ 72 NSLocalizedDescriptionKey: "HomeAppLoaderTask encountered an unexpected error and could not launch an update." 73 ] 74 ) 75 ) 76 } 77 // swiftlint:enable force_unwrapping 78 } 79 } 80 } 81 82 private func runReaper(withLauncher launcher: AppLauncher) { 83 if let launchedUpdate = launcher.launchedUpdate { 84 UpdatesReaper.reapUnusedUpdates( 85 withConfig: config, 86 database: database, 87 directory: directory, 88 selectionPolicy: selectionPolicy, 89 launchedUpdate: launchedUpdate 90 ) 91 } 92 } 93 94 private func loadHomeUpdate(withCompletion completion: @escaping (_ remoteError: Error?, _ updateResponse: UpdateResponse?) -> Void) { 95 HomeAppLoader( 96 manifestAndAssetRequestHeaders: self.manifestAndAssetRequestHeaders, 97 config: config, 98 database: database, 99 directory: directory, 100 launchedUpdate: nil, 101 completionQueue: loaderTaskQueue 102 ).loadHome { _ in 103 return true 104 } asset: { _, _, _, _ in 105 // no-op 106 } success: { updateResponse in 107 completion(nil, updateResponse) 108 } error: { error in 109 completion(error, nil) 110 } 111 } 112 113 private func launchUpdate(_ updateBeingLaunched: Update?, error: Error?) { 114 if let updateBeingLaunched = updateBeingLaunched { 115 let launcher = AppLauncherWithDatabase( 116 config: self.config, 117 database: self.database, 118 directory: self.directory, 119 completionQueue: self.loaderTaskQueue 120 ) 121 launcher.launchUpdate(withSelectionPolicy: self.selectionPolicy) { error, success in 122 if success { 123 self.finish(withLauncher: launcher, error: nil) 124 } else { 125 self.finish(withLauncher: nil, error: error) 126 NSLog("Downloaded update but failed to launch: %@", error?.localizedDescription ?? "") 127 } 128 self.runReaper(withLauncher: launcher) 129 } 130 } else { 131 // there's no update, there should be an error 132 self.finish(withLauncher: nil, error: error) 133 } 134 } 135 } 136