1 package expo.modules.updates.db 2 3 import android.net.Uri 4 import expo.modules.jsonutils.getNullable 5 import expo.modules.updates.UpdatesConfiguration 6 import org.json.JSONObject 7 8 /** 9 * The build data stored by the configuration is subject to change when 10 * a user updates the binary. 11 * 12 * This can lead to inconsistent update loading behavior, for 13 * example: https://github.com/expo/expo/issues/14372 14 * 15 * This singleton wipes the updates when any of the tracked build data 16 * changes. This leaves the user in the same situation as a fresh install. 17 * 18 * So far we only know that `releaseChannel` and 19 * `requestHeaders[expo-channel-name]` are dangerous to change, but have 20 * included a few more that both seem unlikely to change (so we clear 21 * the updates cache rarely) and likely to 22 * cause bugs when they do. The tracked fields are: 23 * 24 * UPDATES_CONFIGURATION_RELEASE_CHANNEL_KEY 25 * UPDATES_CONFIGURATION_UPDATE_URL_KEY 26 * 27 * and all of the values in json 28 * 29 * UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY 30 */ 31 object BuildData { 32 private var staticBuildDataKey = "staticBuildData" 33 ensureBuildDataIsConsistentnull34 fun ensureBuildDataIsConsistent( 35 updatesConfiguration: UpdatesConfiguration, 36 database: UpdatesDatabase, 37 ) { 38 val scopeKey = updatesConfiguration.scopeKey 39 ?: 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.") 40 val buildJSON = getBuildDataFromDatabase(database, scopeKey) 41 if (buildJSON == null) { 42 setBuildDataInDatabase(database, updatesConfiguration) 43 } else if (!isBuildDataConsistent(updatesConfiguration, buildJSON)) { 44 clearAllUpdatesFromDatabase(database) 45 setBuildDataInDatabase(database, updatesConfiguration) 46 } 47 } 48 clearAllUpdatesFromDatabasenull49 fun clearAllUpdatesFromDatabase(database: UpdatesDatabase) { 50 val allUpdates = database.updateDao().loadAllUpdates() 51 database.updateDao().deleteUpdates(allUpdates) 52 } 53 isBuildDataConsistentnull54 fun isBuildDataConsistent( 55 updatesConfiguration: UpdatesConfiguration, 56 databaseBuildData: JSONObject 57 ): Boolean { 58 val configBuildData = getBuildDataFromConfig(updatesConfiguration) 59 60 val releaseChannelKey = UpdatesConfiguration.UPDATES_CONFIGURATION_RELEASE_CHANNEL_KEY 61 val updateUrlKey = UpdatesConfiguration.UPDATES_CONFIGURATION_UPDATE_URL_KEY 62 val requestHeadersKey = UpdatesConfiguration.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY 63 64 // check equality of the two JSONObjects. The build data object is string valued with the 65 // exception of "requestHeaders" which is a string valued object. 66 return mutableListOf<Boolean>().apply { 67 add(databaseBuildData.getNullable<String>(releaseChannelKey) == configBuildData.get(releaseChannelKey)) 68 add(databaseBuildData.get(updateUrlKey).let { Uri.parse(it.toString()) } == configBuildData.get(updateUrlKey)) 69 70 // loop through keys from both requestHeaders objects. 71 for (key in configBuildData.getJSONObject(requestHeadersKey).keys()) { 72 add(databaseBuildData.getJSONObject(requestHeadersKey).getNullable<String>(key) == configBuildData.getJSONObject(requestHeadersKey).getNullable(key)) 73 } 74 for (key in databaseBuildData.getJSONObject(requestHeadersKey).keys()) { 75 add(databaseBuildData.getJSONObject(requestHeadersKey).getNullable<String>(key) == configBuildData.getJSONObject(requestHeadersKey).getNullable(key)) 76 } 77 }.all { it } 78 } 79 setBuildDataInDatabasenull80 fun setBuildDataInDatabase( 81 database: UpdatesDatabase, 82 updatesConfiguration: UpdatesConfiguration, 83 ) { 84 val buildDataJSON = getBuildDataFromConfig(updatesConfiguration) 85 database.jsonDataDao()?.setJSONStringForKey(staticBuildDataKey, buildDataJSON.toString(), updatesConfiguration.scopeKey as String) 86 } 87 getBuildDataFromDatabasenull88 fun getBuildDataFromDatabase(database: UpdatesDatabase, scopeKey: String): JSONObject? { 89 val buildJSONString = database.jsonDataDao()?.loadJSONStringForKey(staticBuildDataKey, scopeKey) 90 return if (buildJSONString == null) null else JSONObject(buildJSONString) 91 } 92 getBuildDataFromConfignull93 private fun getBuildDataFromConfig(updatesConfiguration: UpdatesConfiguration): JSONObject { 94 val requestHeadersJSON = JSONObject().apply { 95 for ((key, value) in updatesConfiguration.requestHeaders) put(key, value) 96 } 97 val buildData = JSONObject().apply { 98 put(UpdatesConfiguration.UPDATES_CONFIGURATION_RELEASE_CHANNEL_KEY, updatesConfiguration.releaseChannel) 99 put(UpdatesConfiguration.UPDATES_CONFIGURATION_UPDATE_URL_KEY, updatesConfiguration.updateUrl) 100 put(UpdatesConfiguration.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY, requestHeadersJSON) 101 } 102 return buildData 103 } 104 } 105