1 package expo.modules.updates 2 3 import android.content.Context 4 import android.net.Uri 5 import android.os.AsyncTask 6 import android.os.Handler 7 import android.os.HandlerThread 8 import android.os.Looper 9 import android.util.Log 10 import com.facebook.react.ReactApplication 11 import com.facebook.react.ReactInstanceManager 12 import com.facebook.react.ReactNativeHost 13 import com.facebook.react.bridge.Arguments 14 import com.facebook.react.bridge.JSBundleLoader 15 import com.facebook.react.bridge.WritableMap 16 import expo.modules.updates.db.BuildData 17 import expo.modules.updates.db.DatabaseHolder 18 import expo.modules.updates.db.Reaper 19 import expo.modules.updates.db.UpdatesDatabase 20 import expo.modules.updates.db.entity.AssetEntity 21 import expo.modules.updates.db.entity.UpdateEntity 22 import expo.modules.updates.errorrecovery.ErrorRecovery 23 import expo.modules.updates.errorrecovery.ErrorRecoveryDelegate 24 import expo.modules.updates.launcher.DatabaseLauncher 25 import expo.modules.updates.launcher.Launcher 26 import expo.modules.updates.launcher.Launcher.LauncherCallback 27 import expo.modules.updates.launcher.NoDatabaseLauncher 28 import expo.modules.updates.loader.* 29 import expo.modules.updates.loader.LoaderTask.RemoteUpdateStatus 30 import expo.modules.updates.loader.LoaderTask.LoaderTaskCallback 31 import expo.modules.updates.logging.UpdatesErrorCode 32 import expo.modules.updates.logging.UpdatesLogReader 33 import expo.modules.updates.logging.UpdatesLogger 34 import expo.modules.updates.manifest.UpdateManifest 35 import expo.modules.updates.selectionpolicy.SelectionPolicy 36 import expo.modules.updates.selectionpolicy.SelectionPolicyFactory 37 import expo.modules.updates.statemachine.UpdatesStateChangeEventSender 38 import expo.modules.updates.statemachine.UpdatesStateContext 39 import expo.modules.updates.statemachine.UpdatesStateEvent 40 import expo.modules.updates.statemachine.UpdatesStateEventType 41 import expo.modules.updates.statemachine.UpdatesStateMachine 42 import expo.modules.updates.statemachine.UpdatesStateValue 43 import java.io.File 44 import java.lang.ref.WeakReference 45 46 /** 47 * Main entry point to expo-updates in normal release builds (development clients, including Expo 48 * Go, use a different entry point). Singleton that keeps track of updates state, holds references 49 * to instances of other updates classes, and is the central hub for all updates-related tasks. 50 * 51 * The `start` method in this class should be invoked early in the application lifecycle, via 52 * [UpdatesPackage]. It delegates to an instance of [LoaderTask] to start the process of loading and 53 * launching an update, then responds appropriately depending on the callbacks that are invoked. 54 * 55 * This class also provides getter methods to access information about the updates state, which are 56 * used by the exported [UpdatesModule] through [UpdatesService]. Such information includes 57 * references to: the database, the [UpdatesConfiguration] object, the path on disk to the updates 58 * directory, any currently active [LoaderTask], the current [SelectionPolicy], the error recovery 59 * handler, and the current launched update. This class is intended to be the source of truth for 60 * these objects, so other classes shouldn't retain any of them indefinitely. 61 * 62 * This class also optionally holds a reference to the app's [ReactNativeHost], which allows 63 * expo-updates to reload JS and send events through the bridge. 64 */ 65 class UpdatesController private constructor( 66 context: Context, 67 var updatesConfiguration: UpdatesConfiguration 68 ) : UpdatesStateChangeEventSender { 69 private var reactNativeHost: WeakReference<ReactNativeHost>? = if (context is ReactApplication) { 70 WeakReference((context as ReactApplication).reactNativeHost) 71 } else { 72 null 73 } 74 75 var updatesDirectory: File? = null 76 var updatesDirectoryException: Exception? = null 77 var stateMachine: UpdatesStateMachine = UpdatesStateMachine(context, this) 78 79 private var launcher: Launcher? = null 80 val databaseHolder = DatabaseHolder(UpdatesDatabase.getInstance(context)) 81 82 // TODO: move away from DatabaseHolder pattern to Handler thread 83 private val databaseHandlerThread = HandlerThread("expo-updates-database") 84 private lateinit var databaseHandler: Handler initializeDatabaseHandlernull85 private fun initializeDatabaseHandler() { 86 if (!::databaseHandler.isInitialized) { 87 databaseHandlerThread.start() 88 databaseHandler = Handler(databaseHandlerThread.looper) 89 } 90 } 91 purgeUpdatesLogsOlderThanOneDaynull92 private fun purgeUpdatesLogsOlderThanOneDay(context: Context) { 93 UpdatesLogReader(context).purgeLogEntries { 94 if (it != null) { 95 Log.e(TAG, "UpdatesLogReader: error in purgeLogEntries", it) 96 } 97 } 98 } 99 100 private val logger = UpdatesLogger(context) 101 private var isStarted = false 102 private var loaderTask: LoaderTask? = null 103 private var remoteLoadStatus = ErrorRecoveryDelegate.RemoteLoadStatus.IDLE 104 105 private var mSelectionPolicy: SelectionPolicy? = null 106 private var defaultSelectionPolicy: SelectionPolicy = SelectionPolicyFactory.createFilterAwarePolicy( 107 UpdatesUtils.getRuntimeVersion(updatesConfiguration) 108 ) 109 val fileDownloader: FileDownloader = FileDownloader(context) 110 private val errorRecovery: ErrorRecovery = ErrorRecovery(context) 111 setRemoteLoadStatusnull112 private fun setRemoteLoadStatus(status: ErrorRecoveryDelegate.RemoteLoadStatus) { 113 remoteLoadStatus = status 114 errorRecovery.notifyNewRemoteLoadStatus(status) 115 } 116 117 // launch conditions 118 private var isLoaderTaskFinished = false 119 var isEmergencyLaunch = false 120 private set 121 onDidCreateReactInstanceManagernull122 fun onDidCreateReactInstanceManager(reactInstanceManager: ReactInstanceManager) { 123 if (isEmergencyLaunch || !updatesConfiguration.isEnabled) { 124 return 125 } 126 errorRecovery.startMonitoring(reactInstanceManager) 127 } 128 129 /** 130 * If UpdatesController.initialize() is not provided with a [ReactApplication], this method 131 * can be used to set a [ReactNativeHost] on the class. This is optional, but required in 132 * order for `Updates.reload()` and some Updates module events to work. 133 * @param reactNativeHost the ReactNativeHost of the application running the Updates module 134 */ setReactNativeHostnull135 fun setReactNativeHost(reactNativeHost: ReactNativeHost) { 136 this.reactNativeHost = WeakReference(reactNativeHost) 137 } 138 139 /** 140 * Returns the path on disk to the launch asset (JS bundle) file for the React Native host to use. 141 * Blocks until the configured timeout runs out, or a new update has been downloaded and is ready 142 * to use (whichever comes sooner). ReactNativeHost.getJSBundleFile() should call into this. 143 * 144 * If this returns null, something has gone wrong and expo-updates has not been able to launch or 145 * find an update to use. In (and only in) this case, `getBundleAssetName()` will return a nonnull 146 * fallback value to use. 147 */ 148 @get:Synchronized 149 val launchAssetFile: String? 150 get() { 151 while (!isLoaderTaskFinished) { 152 try { 153 (this as java.lang.Object).wait() 154 } catch (e: InterruptedException) { 155 Log.e(TAG, "Interrupted while waiting for launch asset file", e) 156 } 157 } 158 return launcher?.launchAssetFile 159 } 160 161 /** 162 * Returns the filename of the launch asset (JS bundle) file embedded in the APK bundle, which can 163 * be read using `context.getAssets()`. This is only nonnull if `getLaunchAssetFile` is null and 164 * should only be used in such a situation. ReactNativeHost.getBundleAssetName() should call into 165 * this. 166 */ 167 val bundleAssetName: String? 168 get() = launcher?.bundleAssetName 169 170 /** 171 * Returns a map of the locally downloaded assets for the current update. Keys are the remote URLs 172 * of the assets and values are local paths. This should be exported by the Updates JS module and 173 * can be used by `expo-asset` or a similar module to override React Native's asset resolution and 174 * use the locally downloaded assets. 175 */ 176 val localAssetFiles: Map<AssetEntity, String>? 177 get() = launcher?.localAssetFiles 178 179 val isUsingEmbeddedAssets: Boolean 180 get() = launcher?.isUsingEmbeddedAssets ?: false 181 182 /** 183 * Any process that calls this *must* manually release the lock by calling `releaseDatabase()` in 184 * every possible case (success, error) as soon as it is finished. 185 */ getDatabasenull186 fun getDatabase(): UpdatesDatabase = databaseHolder.database 187 188 fun releaseDatabase() { 189 databaseHolder.releaseDatabase() 190 } 191 192 val updateUrl: Uri? 193 get() = updatesConfiguration.updateUrl 194 195 val launchedUpdate: UpdateEntity? 196 get() = launcher?.launchedUpdate 197 198 val selectionPolicy: SelectionPolicy 199 get() = mSelectionPolicy ?: defaultSelectionPolicy 200 201 // Internal Setters 202 203 /** 204 * For external modules that want to modify the selection policy used at runtime. 205 * 206 * This method does not provide any guarantees about how long the provided selection policy will 207 * persist; sometimes expo-updates will reset the selection policy in situations where it makes 208 * sense to have explicit control (e.g. if the developer/user has programmatically fetched an 209 * update, expo-updates will reset the selection policy so the new update is launched on th 210 * next reload). 211 * @param selectionPolicy The SelectionPolicy to use next, until overridden by expo-updates 212 */ setNextSelectionPolicynull213 fun setNextSelectionPolicy(selectionPolicy: SelectionPolicy?) { 214 mSelectionPolicy = selectionPolicy 215 } 216 resetSelectionPolicyToDefaultnull217 fun resetSelectionPolicyToDefault() { 218 mSelectionPolicy = null 219 } 220 setDefaultSelectionPolicynull221 fun setDefaultSelectionPolicy(selectionPolicy: SelectionPolicy) { 222 defaultSelectionPolicy = selectionPolicy 223 } 224 setLaunchernull225 fun setLauncher(launcher: Launcher?) { 226 this.launcher = launcher 227 } 228 229 /** 230 * Starts the update process to launch a previously-loaded update and (if configured to do so) 231 * check for a new update from the server. This method should be called as early as possible in 232 * the application's lifecycle. 233 * @param context the base context of the application, ideally a [ReactApplication] 234 */ 235 @Synchronized startnull236 fun start(context: Context) { 237 if (isStarted) { 238 return 239 } 240 isStarted = true 241 242 if (!updatesConfiguration.isEnabled) { 243 launcher = NoDatabaseLauncher(context, updatesConfiguration) 244 notifyController() 245 return 246 } 247 if (updatesConfiguration.updateUrl == null || updatesConfiguration.scopeKey == null) { 248 throw AssertionError("expo-updates is enabled, but no valid URL is configured in AndroidManifest.xml. If you are making a release build for the first time, make sure you have run `expo publish` at least once.") 249 } 250 if (updatesDirectory == null) { 251 launcher = NoDatabaseLauncher(context, updatesConfiguration, updatesDirectoryException) 252 isEmergencyLaunch = true 253 notifyController() 254 return 255 } 256 257 purgeUpdatesLogsOlderThanOneDay(context) 258 259 initializeDatabaseHandler() 260 initializeErrorRecovery(context) 261 262 val databaseLocal = getDatabase() 263 BuildData.ensureBuildDataIsConsistent(updatesConfiguration, databaseLocal) 264 releaseDatabase() 265 266 loaderTask = LoaderTask( 267 updatesConfiguration, 268 databaseHolder, 269 updatesDirectory, 270 fileDownloader, 271 selectionPolicy, 272 object : LoaderTaskCallback { 273 override fun onFailure(e: Exception) { 274 logger.error("UpdatesController loaderTask onFailure: ${e.localizedMessage}", UpdatesErrorCode.None) 275 launcher = NoDatabaseLauncher(context, updatesConfiguration, e) 276 isEmergencyLaunch = true 277 notifyController() 278 } 279 280 override fun onCachedUpdateLoaded(update: UpdateEntity): Boolean { 281 return true 282 } 283 284 override fun onRemoteCheckForUpdateStarted() { 285 stateMachine.processEvent(UpdatesStateEvent.Check()) 286 } 287 288 override fun onRemoteCheckForUpdateFinished(result: LoaderTask.RemoteCheckResult) { 289 val event = when (result) { 290 is LoaderTask.RemoteCheckResult.NoUpdateAvailable -> UpdatesStateEvent.CheckCompleteUnavailable() 291 is LoaderTask.RemoteCheckResult.UpdateAvailable -> UpdatesStateEvent.CheckCompleteWithUpdate(result.manifest) 292 is LoaderTask.RemoteCheckResult.RollBackToEmbedded -> UpdatesStateEvent.CheckCompleteWithRollback(result.commitTime) 293 } 294 stateMachine.processEvent(event) 295 } 296 297 override fun onRemoteUpdateManifestResponseManifestLoaded(updateManifest: UpdateManifest) { 298 remoteLoadStatus = ErrorRecoveryDelegate.RemoteLoadStatus.NEW_UPDATE_LOADING 299 } 300 301 override fun onSuccess(launcher: Launcher, isUpToDate: Boolean) { 302 if (remoteLoadStatus == ErrorRecoveryDelegate.RemoteLoadStatus.NEW_UPDATE_LOADING && isUpToDate) { 303 remoteLoadStatus = ErrorRecoveryDelegate.RemoteLoadStatus.IDLE 304 } 305 this@UpdatesController.launcher = launcher 306 notifyController() 307 } 308 309 override fun onRemoteUpdateLoadStarted() { 310 stateMachine.processEvent(UpdatesStateEvent.Download()) 311 } 312 313 override fun onRemoteUpdateAssetLoaded( 314 asset: AssetEntity, 315 successfulAssetCount: Int, 316 failedAssetCount: Int, 317 totalAssetCount: Int 318 ) { 319 val body = mapOf( 320 "assetInfo" to mapOf( 321 "name" to asset.embeddedAssetFilename, 322 "successfulAssetCount" to successfulAssetCount, 323 "failedAssetCount" to failedAssetCount, 324 "totalAssetCount" to totalAssetCount 325 ) 326 ) 327 logger.info("AppController appLoaderTask didLoadAsset: $body", UpdatesErrorCode.None, null, asset.expectedHash) 328 } 329 330 override fun onRemoteUpdateFinished( 331 status: RemoteUpdateStatus, 332 update: UpdateEntity?, 333 exception: Exception? 334 ) { 335 when (status) { 336 RemoteUpdateStatus.ERROR -> { 337 if (exception == null) { 338 throw AssertionError("Background update with error status must have a nonnull exception object") 339 } 340 logger.error("UpdatesController onBackgroundUpdateFinished: Error: ${exception.localizedMessage}", UpdatesErrorCode.Unknown, exception) 341 remoteLoadStatus = ErrorRecoveryDelegate.RemoteLoadStatus.IDLE 342 val params = Arguments.createMap() 343 params.putString("message", exception.message) 344 sendLegacyUpdateEventToJS(UPDATE_ERROR_EVENT, params) 345 346 // Since errors can happen through a number of paths, we do these checks 347 // to make sure the state machine is valid 348 when (stateMachine.state) { 349 UpdatesStateValue.Idle -> { 350 stateMachine.processEvent(UpdatesStateEvent.Download()) 351 stateMachine.processEvent( 352 UpdatesStateEvent.DownloadError(exception.message ?: "") 353 ) 354 } 355 UpdatesStateValue.Checking -> { 356 stateMachine.processEvent( 357 UpdatesStateEvent.CheckError(exception.message ?: "") 358 ) 359 } 360 else -> { 361 // .downloading 362 stateMachine.processEvent( 363 UpdatesStateEvent.DownloadError(exception.message ?: "") 364 ) 365 } 366 } 367 } 368 RemoteUpdateStatus.UPDATE_AVAILABLE -> { 369 if (update == null) { 370 throw AssertionError("Background update with error status must have a nonnull update object") 371 } 372 remoteLoadStatus = ErrorRecoveryDelegate.RemoteLoadStatus.NEW_UPDATE_LOADED 373 logger.info("UpdatesController onBackgroundUpdateFinished: Update available", UpdatesErrorCode.None) 374 val params = Arguments.createMap() 375 params.putString("manifestString", update.manifest.toString()) 376 sendLegacyUpdateEventToJS(UPDATE_AVAILABLE_EVENT, params) 377 stateMachine.processEvent( 378 UpdatesStateEvent.DownloadCompleteWithUpdate(update.manifest) 379 ) 380 } 381 RemoteUpdateStatus.NO_UPDATE_AVAILABLE -> { 382 remoteLoadStatus = ErrorRecoveryDelegate.RemoteLoadStatus.IDLE 383 logger.error("UpdatesController onBackgroundUpdateFinished: No update available", UpdatesErrorCode.NoUpdatesAvailable) 384 sendLegacyUpdateEventToJS(UPDATE_NO_UPDATE_AVAILABLE_EVENT, null) 385 // TODO: handle rollbacks properly, but this works for now 386 if (stateMachine.state == UpdatesStateValue.Downloading) { 387 stateMachine.processEvent(UpdatesStateEvent.DownloadComplete()) 388 } 389 } 390 } 391 errorRecovery.notifyNewRemoteLoadStatus(remoteLoadStatus) 392 } 393 } 394 ) 395 loaderTask!!.start(context) 396 } 397 398 @Synchronized notifyControllernull399 private fun notifyController() { 400 if (launcher == null) { 401 throw AssertionError("UpdatesController.notifyController was called with a null launcher, which is an error. This method should only be called when an update is ready to launch.") 402 } 403 isLoaderTaskFinished = true 404 (this as java.lang.Object).notify() 405 } 406 initializeErrorRecoverynull407 private fun initializeErrorRecovery(context: Context) { 408 errorRecovery.initialize(object : ErrorRecoveryDelegate { 409 override fun loadRemoteUpdate() { 410 if (loaderTask?.isRunning == true) { 411 return 412 } 413 remoteLoadStatus = ErrorRecoveryDelegate.RemoteLoadStatus.NEW_UPDATE_LOADING 414 val database = getDatabase() 415 val remoteLoader = RemoteLoader(context, updatesConfiguration, database, fileDownloader, updatesDirectory, launchedUpdate) 416 remoteLoader.start(object : Loader.LoaderCallback { 417 override fun onFailure(e: Exception) { 418 logger.error("UpdatesController loadRemoteUpdate onFailure: ${e.localizedMessage}", UpdatesErrorCode.UpdateFailedToLoad, launchedUpdate?.loggingId, null) 419 setRemoteLoadStatus(ErrorRecoveryDelegate.RemoteLoadStatus.IDLE) 420 releaseDatabase() 421 } 422 423 override fun onSuccess(loaderResult: Loader.LoaderResult) { 424 setRemoteLoadStatus( 425 if (loaderResult.updateEntity != null || loaderResult.updateDirective is UpdateDirective.RollBackToEmbeddedUpdateDirective) ErrorRecoveryDelegate.RemoteLoadStatus.NEW_UPDATE_LOADED 426 else ErrorRecoveryDelegate.RemoteLoadStatus.IDLE 427 ) 428 releaseDatabase() 429 } 430 431 override fun onAssetLoaded(asset: AssetEntity, successfulAssetCount: Int, failedAssetCount: Int, totalAssetCount: Int) { } 432 433 override fun onUpdateResponseLoaded(updateResponse: UpdateResponse): Loader.OnUpdateResponseLoadedResult { 434 val updateDirective = updateResponse.directiveUpdateResponsePart?.updateDirective 435 if (updateDirective != null) { 436 return Loader.OnUpdateResponseLoadedResult( 437 shouldDownloadManifestIfPresentInResponse = when (updateDirective) { 438 is UpdateDirective.RollBackToEmbeddedUpdateDirective -> false 439 is UpdateDirective.NoUpdateAvailableUpdateDirective -> false 440 } 441 ) 442 } 443 444 val updateManifest = updateResponse.manifestUpdateResponsePart?.updateManifest ?: return Loader.OnUpdateResponseLoadedResult(shouldDownloadManifestIfPresentInResponse = false) 445 return Loader.OnUpdateResponseLoadedResult(shouldDownloadManifestIfPresentInResponse = selectionPolicy.shouldLoadNewUpdate(updateManifest.updateEntity, launchedUpdate, updateResponse.responseHeaderData?.manifestFilters)) 446 } 447 }) 448 } 449 450 override fun relaunch(callback: LauncherCallback) { relaunchReactApplication(context, false, callback) } 451 override fun throwException(exception: Exception) { throw exception } 452 453 override fun markFailedLaunchForLaunchedUpdate() { 454 if (isEmergencyLaunch) { 455 return 456 } 457 databaseHandler.post { 458 val launchedUpdate = launchedUpdate ?: return@post 459 val database = getDatabase() 460 database.updateDao().incrementFailedLaunchCount(launchedUpdate) 461 releaseDatabase() 462 } 463 } 464 465 override fun markSuccessfulLaunchForLaunchedUpdate() { 466 if (isEmergencyLaunch) { 467 return 468 } 469 databaseHandler.post { 470 val launchedUpdate = launchedUpdate ?: return@post 471 val database = getDatabase() 472 database.updateDao().incrementSuccessfulLaunchCount(launchedUpdate) 473 releaseDatabase() 474 } 475 } 476 477 override fun getRemoteLoadStatus() = remoteLoadStatus 478 override fun getCheckAutomaticallyConfiguration() = updatesConfiguration.checkOnLaunch 479 override fun getLaunchedUpdateSuccessfulLaunchCount() = launchedUpdate?.successfulLaunchCount ?: 0 480 }) 481 } 482 runReapernull483 fun runReaper() { 484 AsyncTask.execute { 485 val databaseLocal = getDatabase() 486 Reaper.reapUnusedUpdates( 487 updatesConfiguration, 488 databaseLocal, 489 updatesDirectory, 490 launchedUpdate, 491 selectionPolicy 492 ) 493 releaseDatabase() 494 } 495 } 496 relaunchReactApplicationnull497 fun relaunchReactApplication(context: Context, callback: LauncherCallback) { 498 relaunchReactApplication(context, true, callback) 499 } 500 relaunchReactApplicationnull501 private fun relaunchReactApplication(context: Context, shouldRunReaper: Boolean, callback: LauncherCallback) { 502 val host = reactNativeHost?.get() 503 if (host == null) { 504 callback.onFailure(Exception("Could not reload application. Ensure you have passed the correct instance of ReactApplication into UpdatesController.initialize().")) 505 return 506 } 507 508 stateMachine.processEvent(UpdatesStateEvent.Restart()) 509 510 val oldLaunchAssetFile = launcher!!.launchAssetFile 511 512 val databaseLocal = getDatabase() 513 val newLauncher = DatabaseLauncher( 514 updatesConfiguration, 515 updatesDirectory!!, 516 fileDownloader, 517 selectionPolicy 518 ) 519 newLauncher.launch( 520 databaseLocal, context, 521 object : LauncherCallback { 522 override fun onFailure(e: Exception) { 523 callback.onFailure(e) 524 } 525 526 override fun onSuccess() { 527 launcher = newLauncher 528 releaseDatabase() 529 530 val instanceManager = host.reactInstanceManager 531 532 val newLaunchAssetFile = launcher!!.launchAssetFile 533 if (newLaunchAssetFile != null && newLaunchAssetFile != oldLaunchAssetFile) { 534 // Unfortunately, even though RN exposes a way to reload an application, 535 // it assumes that the JS bundle will stay at the same location throughout 536 // the entire lifecycle of the app. Since we need to change the location of 537 // the bundle, we need to use reflection to set an otherwise inaccessible 538 // field of the ReactInstanceManager. 539 try { 540 val newJSBundleLoader = JSBundleLoader.createFileLoader(newLaunchAssetFile) 541 val jsBundleLoaderField = instanceManager.javaClass.getDeclaredField("mBundleLoader") 542 jsBundleLoaderField.isAccessible = true 543 jsBundleLoaderField[instanceManager] = newJSBundleLoader 544 } catch (e: Exception) { 545 Log.e(TAG, "Could not reset JSBundleLoader in ReactInstanceManager", e) 546 } 547 } 548 callback.onSuccess() 549 val handler = Handler(Looper.getMainLooper()) 550 handler.post { instanceManager.recreateReactContextInBackground() } 551 if (shouldRunReaper) { 552 runReaper() 553 } 554 stateMachine.reset() 555 } 556 } 557 ) 558 } 559 sendUpdateStateChangeEventToBridgenull560 override fun sendUpdateStateChangeEventToBridge(eventType: UpdatesStateEventType, context: UpdatesStateContext) { 561 sendEventToJS(UPDATES_STATE_CHANGE_EVENT_NAME, eventType.type, context.writableMap) 562 } 563 sendLegacyUpdateEventToJSnull564 fun sendLegacyUpdateEventToJS(eventType: String, params: WritableMap?) { 565 sendEventToJS(UPDATES_EVENT_NAME, eventType, params) 566 } 567 sendEventToJSnull568 private fun sendEventToJS(eventName: String, eventType: String, params: WritableMap?) { 569 UpdatesUtils.sendEventToReactNative(reactNativeHost, logger, eventName, eventType, params) 570 } 571 572 companion object { 573 private val TAG = UpdatesController::class.java.simpleName 574 575 private const val UPDATE_AVAILABLE_EVENT = "updateAvailable" 576 private const val UPDATE_NO_UPDATE_AVAILABLE_EVENT = "noUpdateAvailable" 577 private const val UPDATE_ERROR_EVENT = "error" 578 579 private const val UPDATES_EVENT_NAME = "Expo.nativeUpdatesEvent" 580 private const val UPDATES_STATE_CHANGE_EVENT_NAME = "Expo.nativeUpdatesStateChangeEvent" 581 582 private var singletonInstance: UpdatesController? = null 583 @JvmStatic val instance: UpdatesController 584 get() { <lambda>null585 return checkNotNull(singletonInstance) { "UpdatesController.instance was called before the module was initialized" } 586 } 587 initializeWithoutStartingnull588 @JvmStatic fun initializeWithoutStarting(context: Context) { 589 if (singletonInstance == null) { 590 val updatesConfiguration = UpdatesConfiguration(context, null) 591 singletonInstance = UpdatesController(context, updatesConfiguration) 592 } 593 } 594 595 /** 596 * Initializes the UpdatesController singleton. This should be called as early as possible in the 597 * application's lifecycle. 598 * @param context the base context of the application, ideally a [ReactApplication] 599 */ initializenull600 @JvmStatic fun initialize(context: Context) { 601 if (singletonInstance == null) { 602 initializeWithoutStarting(context) 603 singletonInstance!!.start(context) 604 } 605 } 606 607 /** 608 * Initializes the UpdatesController singleton. This should be called as early as possible in the 609 * application's lifecycle. Use this method to set or override configuration values at runtime 610 * rather than from AndroidManifest.xml. 611 * @param context the base context of the application, ideally a [ReactApplication] 612 */ initializenull613 @JvmStatic fun initialize(context: Context, configuration: Map<String, Any>) { 614 if (singletonInstance == null) { 615 val updatesConfiguration = UpdatesConfiguration(context, configuration) 616 singletonInstance = UpdatesController(context, updatesConfiguration) 617 singletonInstance!!.start(context) 618 } 619 } 620 } 621 622 init { 623 try { 624 updatesDirectory = UpdatesUtils.getOrCreateUpdatesDirectory(context) 625 } catch (e: Exception) { 626 updatesDirectoryException = e 627 updatesDirectory = null 628 } 629 } 630 } 631