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