1 // Copyright © 2019 650 Industries. All rights reserved. 2 3 // swiftlint:disable identifier_name 4 // swiftlint:disable legacy_objc_type 5 6 // this class uses an abstract class pattern 7 // swiftlint:disable unavailable_function 8 9 import Foundation 10 11 /** 12 * Subclass of AppLoader which handles copying the embedded update's assets into the 13 * expo-updates cache location. 14 * 15 * Rather than launching the embedded update directly from its location in the app bundle/apk, we 16 * first try to read it into the expo-updates cache and database and launch it like any other 17 * update. The benefits of this include (a) a single code path for launching most updates and (b) 18 * assets included in embedded updates and copied into the cache in this way do not need to be 19 * redownloaded if included in future updates. 20 */ 21 @objc(EXUpdatesEmbeddedAppLoader) 22 @objcMembers 23 public final class EmbeddedAppLoader: AppLoader { 24 public static let EXUpdatesEmbeddedManifestName = "app" 25 public static let EXUpdatesEmbeddedManifestType = "manifest" 26 public static let EXUpdatesEmbeddedBundleFilename = "app" 27 public static let EXUpdatesEmbeddedBundleFileType = "bundle" 28 public static let EXUpdatesBareEmbeddedBundleFilename = "main" 29 public static let EXUpdatesBareEmbeddedBundleFileType = "jsbundle" 30 31 private static let ErrorDomain = "EXUpdatesEmbeddedAppLoader" 32 33 private static var embeddedManifestInternal: Update? embeddedManifestnull34 public static func embeddedManifest(withConfig config: UpdatesConfig, database: UpdatesDatabase?) -> Update? { 35 guard config.hasEmbeddedUpdate else { 36 return nil 37 } 38 if let embeddedManifestInternal = embeddedManifestInternal { 39 return embeddedManifestInternal 40 } 41 42 var manifestNSData: NSData? 43 44 let frameworkBundle = Bundle(for: EmbeddedAppLoader.self) 45 if let resourceUrl = frameworkBundle.resourceURL, 46 let bundle = Bundle(url: resourceUrl.appendingPathComponent("EXUpdates.bundle")), 47 let path = bundle.path( 48 forResource: EmbeddedAppLoader.EXUpdatesEmbeddedManifestName, 49 ofType: EmbeddedAppLoader.EXUpdatesEmbeddedManifestType 50 ) { 51 manifestNSData = NSData(contentsOfFile: path) 52 } 53 54 // Fallback to main bundle if the embedded manifest is not found in EXUpdates.bundle. This is a special case 55 // to support the existing structure of Expo "shell apps" 56 if manifestNSData == nil, 57 let path = Bundle.main.path( 58 forResource: EmbeddedAppLoader.EXUpdatesEmbeddedManifestName, 59 ofType: EmbeddedAppLoader.EXUpdatesEmbeddedManifestType 60 ) { 61 manifestNSData = NSData(contentsOfFile: path) 62 } 63 64 let manifestData = manifestNSData.let { it in 65 it as Data 66 } 67 68 // Not found in EXUpdates.bundle or main bundle 69 guard let manifestData = manifestData else { 70 NSException( 71 name: .internalInconsistencyException, 72 reason: "The embedded manifest is invalid or could not be read. Make sure you have configured expo-updates correctly in your Xcode Build Phases." 73 ) 74 .raise() 75 return nil 76 } 77 78 guard let manifest = try? JSONSerialization.jsonObject(with: manifestData) else { 79 NSException( 80 name: .internalInconsistencyException, 81 reason: "The embedded manifest is invalid or could not be read. Make sure you have configured expo-updates correctly in your Xcode Build Phases." 82 ) 83 .raise() 84 return nil 85 } 86 87 guard let manifestDictionary = manifest as? [String: Any] else { 88 NSException( 89 name: .internalInconsistencyException, 90 reason: "embedded manifest should be a valid JSON file" 91 ) 92 .raise() 93 return nil 94 } 95 96 var mutableManifest = manifestDictionary 97 // automatically verify embedded manifest since it was already codesigned 98 mutableManifest["isVerified"] = true 99 embeddedManifestInternal = Update.update(withEmbeddedManifest: mutableManifest, config: config, database: database) 100 return embeddedManifestInternal 101 } 102 103 internal func loadUpdateResponseFromEmbeddedManifest( 104 withCallback updateResponseBlock: @escaping AppLoaderUpdateResponseBlock, 105 asset assetBlock: @escaping AppLoaderAssetBlock, 106 success successBlock: @escaping AppLoaderSuccessBlock, 107 error errorBlock: @escaping AppLoaderErrorBlock 108 ) { 109 guard let embeddedManifest = EmbeddedAppLoader.embeddedManifest(withConfig: config, database: database) else { 110 errorBlock(NSError( 111 domain: EmbeddedAppLoader.ErrorDomain, 112 code: 1008, 113 userInfo: [ 114 NSLocalizedDescriptionKey: "Failed to load embedded manifest. Make sure you have configured expo-updates correctly." 115 ] 116 )) 117 return 118 } 119 120 self.updateResponseBlock = updateResponseBlock 121 self.assetBlock = assetBlock 122 self.successBlock = successBlock 123 self.errorBlock = errorBlock 124 startLoading(fromUpdateResponse: UpdateResponse( 125 responseHeaderData: nil, 126 manifestUpdateResponsePart: ManifestUpdateResponsePart(updateManifest: embeddedManifest), 127 directiveUpdateResponsePart: nil 128 )) 129 } 130 downloadAssetnull131 override public func downloadAsset(_ asset: UpdateAsset) { 132 let destinationUrl = directory.appendingPathComponent(asset.filename) 133 FileDownloader.assetFilesQueue.async { 134 if FileManager.default.fileExists(atPath: destinationUrl.path) { 135 DispatchQueue.global().async { 136 self.handleAssetDownloadAlreadyExists(asset) 137 } 138 } else { 139 let mainBundleFilename = asset.mainBundleFilename.require("embedded asset mainBundleFilename must be nonnull") 140 let bundlePath = UpdatesUtils.path(forBundledAsset: asset).require( 141 String( 142 format: "Could not find the expected embedded asset in NSBundle %@.%@. " + 143 "Check that expo-updates is installed correctly, and verify that assets are present in the ipa file.", 144 mainBundleFilename, 145 asset.type ?? "" 146 ) 147 ) 148 149 do { 150 try FileManager.default.copyItem(atPath: bundlePath, toPath: destinationUrl.path) 151 let data = try NSData(contentsOfFile: bundlePath) as Data 152 DispatchQueue.global().async { 153 self.handleAssetDownload(withData: data, response: nil, asset: asset) 154 } 155 } catch { 156 DispatchQueue.global().async { 157 self.handleAssetDownload(withError: error, asset: asset) 158 } 159 } 160 } 161 } 162 } 163 164 override public func loadUpdate( 165 fromURL url: URL, 166 onUpdateResponse updateResponseBlock: @escaping AppLoaderUpdateResponseBlock, 167 asset assetBlock: @escaping AppLoaderAssetBlock, 168 success successBlock: @escaping AppLoaderSuccessBlock, 169 error errorBlock: @escaping AppLoaderErrorBlock 170 ) { 171 preconditionFailure("Should not call EmbeddedAppLoader#loadUpdateFromUrl") 172 } 173 } 174