1 package expo.modules.updates 2 3 import android.content.Context 4 import expo.modules.updates.db.entity.AssetEntity 5 import expo.modules.updates.db.entity.UpdateEntity 6 import expo.modules.updates.launcher.DatabaseLauncher 7 import expo.modules.updates.launcher.Launcher.LauncherCallback 8 import expo.modules.updates.loader.Loader 9 import expo.modules.updates.loader.RemoteLoader 10 import expo.modules.updates.loader.UpdateDirective 11 import expo.modules.updates.loader.UpdateResponse 12 import expo.modules.updates.selectionpolicy.LauncherSelectionPolicySingleUpdate 13 import expo.modules.updates.selectionpolicy.ReaperSelectionPolicyDevelopmentClient 14 import expo.modules.updates.selectionpolicy.SelectionPolicy 15 import expo.modules.updatesinterface.UpdatesInterface 16 import expo.modules.updatesinterface.UpdatesInterface.QueryCallback 17 import expo.modules.updatesinterface.UpdatesInterface.UpdateCallback 18 import org.json.JSONObject 19 import java.util.* 20 21 /** 22 * Main entry point to expo-updates in development builds with expo-dev-client. Singleton that still 23 * makes use of [UpdatesController] for keeping track of updates state, but provides capabilities 24 * that are not usually exposed but that expo-dev-client needs (launching and downloading a specific 25 * update by URL, allowing dynamic configuration, introspecting the database). 26 * 27 * Implements the external UpdatesInterface from the expo-updates-interface package. This allows 28 * expo-dev-client to compile without needing expo-updates to be installed. 29 */ 30 class UpdatesDevLauncherController : UpdatesInterface { 31 private var mTempConfiguration: UpdatesConfiguration? = null resetnull32 override fun reset() { 33 UpdatesController.instance.setLauncher(null) 34 } 35 36 /** 37 * Fetch an update using a dynamically generated configuration object (including a potentially 38 * different update URL than the one embedded in the build). 39 */ fetchUpdateWithConfigurationnull40 override fun fetchUpdateWithConfiguration( 41 configuration: HashMap<String, Any>, 42 context: Context, 43 callback: UpdateCallback 44 ) { 45 val controller = UpdatesController.instance 46 val updatesConfiguration = UpdatesConfiguration(context, configuration) 47 if (updatesConfiguration.updateUrl == null || updatesConfiguration.scopeKey == null) { 48 callback.onFailure(Exception("Failed to load update: UpdatesConfiguration object must include a valid update URL")) 49 return 50 } 51 if (controller.updatesDirectory == null) { 52 callback.onFailure(controller.updatesDirectoryException) 53 return 54 } 55 56 // since controller is a singleton, save its config so we can reset to it if our request fails 57 mTempConfiguration = controller.updatesConfiguration 58 setDevelopmentSelectionPolicy() 59 controller.updatesConfiguration = updatesConfiguration 60 val databaseHolder = controller.databaseHolder 61 val loader = RemoteLoader( 62 context, 63 updatesConfiguration, 64 databaseHolder.database, 65 controller.fileDownloader, 66 controller.updatesDirectory, 67 null 68 ) 69 loader.start(object : Loader.LoaderCallback { 70 override fun onFailure(e: Exception) { 71 databaseHolder.releaseDatabase() 72 // reset controller's configuration to what it was before this request 73 controller.updatesConfiguration = mTempConfiguration!! 74 callback.onFailure(e) 75 } 76 77 override fun onSuccess(loaderResult: Loader.LoaderResult) { 78 // the dev launcher doesn't handle roll back to embedded commands 79 databaseHolder.releaseDatabase() 80 if (loaderResult.updateEntity == null) { 81 callback.onSuccess(null) 82 return 83 } 84 launchUpdate(loaderResult.updateEntity, updatesConfiguration, context, callback) 85 } 86 87 override fun onAssetLoaded( 88 asset: AssetEntity, 89 successfulAssetCount: Int, 90 failedAssetCount: Int, 91 totalAssetCount: Int 92 ) { 93 callback.onProgress(successfulAssetCount, failedAssetCount, totalAssetCount) 94 } 95 96 override fun onUpdateResponseLoaded(updateResponse: UpdateResponse): Loader.OnUpdateResponseLoadedResult { 97 val updateDirective = updateResponse.directiveUpdateResponsePart?.updateDirective 98 if (updateDirective != null) { 99 return Loader.OnUpdateResponseLoadedResult( 100 shouldDownloadManifestIfPresentInResponse = when (updateDirective) { 101 is UpdateDirective.RollBackToEmbeddedUpdateDirective -> false 102 is UpdateDirective.NoUpdateAvailableUpdateDirective -> false 103 } 104 ) 105 } 106 107 val updateManifest = updateResponse.manifestUpdateResponsePart?.updateManifest ?: return Loader.OnUpdateResponseLoadedResult(shouldDownloadManifestIfPresentInResponse = false) 108 return Loader.OnUpdateResponseLoadedResult(shouldDownloadManifestIfPresentInResponse = callback.onManifestLoaded(updateManifest.manifest.getRawJson())) 109 } 110 }) 111 } 112 launchUpdatenull113 private fun launchUpdate( 114 update: UpdateEntity, 115 configuration: UpdatesConfiguration, 116 context: Context, 117 callback: UpdateCallback 118 ) { 119 val controller = UpdatesController.instance 120 121 // ensure that we launch the update we want, even if it isn't the latest one 122 val currentSelectionPolicy = controller.selectionPolicy 123 // Calling `setNextSelectionPolicy` allows the Updates module's `reloadAsync` method to reload 124 // with a different (newer) update if one is downloaded, e.g. using `fetchUpdateAsync`. If we 125 // set the default selection policy here instead, the update we are launching here would keep 126 // being launched by `reloadAsync` even if a newer one is downloaded. 127 controller.setNextSelectionPolicy( 128 SelectionPolicy( 129 LauncherSelectionPolicySingleUpdate(update.id), 130 currentSelectionPolicy.loaderSelectionPolicy, 131 currentSelectionPolicy.reaperSelectionPolicy 132 ) 133 ) 134 135 val databaseHolder = controller.databaseHolder 136 val launcher = DatabaseLauncher( 137 configuration, 138 controller.updatesDirectory!!, 139 controller.fileDownloader, 140 controller.selectionPolicy 141 ) 142 launcher.launch( 143 databaseHolder.database, context, 144 object : LauncherCallback { 145 override fun onFailure(e: Exception) { 146 databaseHolder.releaseDatabase() 147 // reset controller's configuration to what it was before this request 148 controller.updatesConfiguration = mTempConfiguration!! 149 callback.onFailure(e) 150 } 151 152 override fun onSuccess() { 153 databaseHolder.releaseDatabase() 154 controller.setLauncher(launcher) 155 callback.onSuccess(object : UpdatesInterface.Update { 156 override fun getManifest(): JSONObject { 157 return launcher.launchedUpdate!!.manifest 158 } 159 160 override fun getLaunchAssetPath(): String { 161 return launcher.launchAssetFile!! 162 } 163 }) 164 controller.runReaper() 165 } 166 } 167 ) 168 } 169 storedUpdateIdsWithConfigurationnull170 override fun storedUpdateIdsWithConfiguration(configuration: HashMap<String, Any>, context: Context, callback: QueryCallback) { 171 val controller = UpdatesController.instance 172 val updatesConfiguration = UpdatesConfiguration(context, configuration) 173 if (updatesConfiguration.updateUrl == null || updatesConfiguration.scopeKey == null) { 174 callback.onFailure(Exception("Failed to load update: UpdatesConfiguration object must include a valid update URL")) 175 return 176 } 177 val updatesDirectory = controller.updatesDirectory 178 if (updatesDirectory == null) { 179 callback.onFailure(controller.updatesDirectoryException) 180 return 181 } 182 val databaseHolder = controller.databaseHolder 183 val launcher = DatabaseLauncher( 184 updatesConfiguration, 185 updatesDirectory, 186 controller.fileDownloader, 187 controller.selectionPolicy 188 ) 189 val readyUpdateIds = launcher.getReadyUpdateIds(databaseHolder.database) 190 controller.databaseHolder.releaseDatabase() 191 callback.onSuccess(readyUpdateIds) 192 } 193 194 companion object { 195 private var singletonInstance: UpdatesDevLauncherController? = null 196 val instance: UpdatesDevLauncherController 197 get() { <lambda>null198 return checkNotNull(singletonInstance) { "UpdatesDevLauncherController.instance was called before the module was initialized" } 199 } 200 initializenull201 @JvmStatic fun initialize(context: Context): UpdatesDevLauncherController { 202 if (singletonInstance == null) { 203 singletonInstance = UpdatesDevLauncherController() 204 } 205 UpdatesController.initializeWithoutStarting(context) 206 return instance 207 } 208 setDevelopmentSelectionPolicynull209 private fun setDevelopmentSelectionPolicy() { 210 val controller = UpdatesController.instance 211 controller.resetSelectionPolicyToDefault() 212 val currentSelectionPolicy = controller.selectionPolicy 213 controller.setDefaultSelectionPolicy( 214 SelectionPolicy( 215 currentSelectionPolicy.launcherSelectionPolicy, 216 currentSelectionPolicy.loaderSelectionPolicy, 217 ReaperSelectionPolicyDevelopmentClient() 218 ) 219 ) 220 controller.resetSelectionPolicyToDefault() 221 } 222 } 223 } 224