1 package expo.modules.updates
2
3 import android.content.Context
4 import android.content.pm.PackageManager
5 import android.net.Uri
6 import android.util.Log
7 import expo.modules.core.errors.InvalidArgumentException
8 import expo.modules.updates.codesigning.CodeSigningConfiguration
9
10 /**
11 * Holds global, immutable configuration values for updates, as well as doing some rudimentary
12 * validation.
13 *
14 * In most apps, these configuration values are baked into the build, and this class functions as a
15 * utility for reading and memoizing the values.
16 *
17 * In development clients (including Expo Go) where this configuration is intended to be dynamic at
18 * runtime and updates from multiple scopes can potentially be opened, multiple instances of this
19 * class may be created over the lifetime of the app, but only one should be active at a time.
20 */
21 data class UpdatesConfiguration(
22 val isEnabled: Boolean,
23 val expectsSignedManifest: Boolean,
24 val scopeKey: String?,
25 val updateUrl: Uri?,
26 val sdkVersion: String?,
27 val runtimeVersion: String?,
28 val releaseChannel: String,
29 val launchWaitMs: Int,
30 val checkOnLaunch: CheckAutomaticallyConfiguration,
31 val hasEmbeddedUpdate: Boolean, // used only for expo-updates development
32 val requestHeaders: Map<String, String>,
33 val codeSigningCertificate: String?,
34 val codeSigningMetadata: Map<String, String>?,
35 val codeSigningIncludeManifestResponseCertificateChain: Boolean,
36 private val codeSigningAllowUnsignedManifests: Boolean,
37 val enableExpoUpdatesProtocolV0CompatibilityMode: Boolean, // used only in Expo Go to prevent loading rollbacks and other directives, which don't make much sense in the context of Expo Go
38 ) {
39 enum class CheckAutomaticallyConfiguration {
40 NEVER {
toJSStringnull41 override fun toJSString() = "NEVER"
42 },
43 ERROR_RECOVERY_ONLY {
44 override fun toJSString() = "ERROR_RECOVERY_ONLY"
45 },
46 WIFI_ONLY {
toJSStringnull47 override fun toJSString() = "WIFI_ONLY"
48 },
49 ALWAYS {
50 override fun toJSString() = "ALWAYS"
51 };
toJSStringnull52 open fun toJSString(): String {
53 throw InvalidArgumentException("Unsupported CheckAutomaticallyConfiguration value")
54 }
55 }
56
57 constructor(context: Context?, overrideMap: Map<String, Any>?) : this(
58 isEnabled = overrideMap?.readValueCheckingType<Boolean>(UPDATES_CONFIGURATION_ENABLED_KEY) ?: context?.getMetadataValue("expo.modules.updates.ENABLED") ?: true,
59 expectsSignedManifest = overrideMap?.readValueCheckingType(UPDATES_CONFIGURATION_EXPECTS_EXPO_SIGNED_MANIFEST) ?: false,
60 scopeKey = maybeGetDefaultScopeKey(
61 overrideMap?.readValueCheckingType<String>(UPDATES_CONFIGURATION_SCOPE_KEY_KEY) ?: context?.getMetadataValue("expo.modules.updates.EXPO_SCOPE_KEY"),
<lambda>null62 updateUrl = overrideMap?.readValueCheckingType<Uri>(UPDATES_CONFIGURATION_UPDATE_URL_KEY) ?: context?.getMetadataValue<String>("expo.modules.updates.EXPO_UPDATE_URL")?.let { Uri.parse(it) },
63 ),
<lambda>null64 updateUrl = overrideMap?.readValueCheckingType<Uri>(UPDATES_CONFIGURATION_UPDATE_URL_KEY) ?: context?.getMetadataValue<String>("expo.modules.updates.EXPO_UPDATE_URL")?.let { Uri.parse(it) },
65 sdkVersion = overrideMap?.readValueCheckingType<String>(UPDATES_CONFIGURATION_SDK_VERSION_KEY) ?: context?.getMetadataValue("expo.modules.updates.EXPO_SDK_VERSION"),
66 runtimeVersion = overrideMap?.readValueCheckingType<String>(UPDATES_CONFIGURATION_RUNTIME_VERSION_KEY) ?: context?.getMetadataValue<Any>("expo.modules.updates.EXPO_RUNTIME_VERSION")?.toString()?.replaceFirst("^string:".toRegex(), ""),
67 releaseChannel = overrideMap?.readValueCheckingType<String>(UPDATES_CONFIGURATION_RELEASE_CHANNEL_KEY) ?: context?.getMetadataValue("expo.modules.updates.EXPO_RELEASE_CHANNEL") ?: UPDATES_CONFIGURATION_RELEASE_CHANNEL_DEFAULT_VALUE,
68 launchWaitMs = overrideMap?.readValueCheckingType<Int>(UPDATES_CONFIGURATION_LAUNCH_WAIT_MS_KEY) ?: context?.getMetadataValue("expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS") ?: UPDATES_CONFIGURATION_LAUNCH_WAIT_MS_DEFAULT_VALUE,
<lambda>null69 checkOnLaunch = overrideMap?.readValueCheckingType<String>(UPDATES_CONFIGURATION_CHECK_ON_LAUNCH_KEY)?.let {
70 try {
71 CheckAutomaticallyConfiguration.valueOf(it)
72 } catch (e: IllegalArgumentException) {
73 throw AssertionError("UpdatesConfiguration failed to initialize: invalid value $it provided for checkOnLaunch")
74 }
75 } ?: (context?.getMetadataValue<String>("expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH") ?: "ALWAYS").let {
76 try {
77 CheckAutomaticallyConfiguration.valueOf(it)
78 } catch (e: IllegalArgumentException) {
79 Log.e(
80 TAG,
81 "Invalid value $it for expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH in AndroidManifest; defaulting to ALWAYS"
82 )
83 CheckAutomaticallyConfiguration.ALWAYS
84 }
85 },
86 hasEmbeddedUpdate = overrideMap?.readValueCheckingType<Boolean>(UPDATES_CONFIGURATION_HAS_EMBEDDED_UPDATE_KEY) ?: context?.getMetadataValue("expo.modules.updates.HAS_EMBEDDED_UPDATE") ?: true,
<lambda>null87 requestHeaders = overrideMap?.readValueCheckingType<Map<String, String>>(UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY) ?: (context?.getMetadataValue<String>("expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY") ?: "{}").let {
88 UpdatesUtils.getMapFromJSONString(it)
89 },
90 codeSigningCertificate = overrideMap?.readValueCheckingType<String>(UPDATES_CONFIGURATION_CODE_SIGNING_CERTIFICATE) ?: context?.getMetadataValue("expo.modules.updates.CODE_SIGNING_CERTIFICATE"),
<lambda>null91 codeSigningMetadata = overrideMap?.readValueCheckingType<Map<String, String>>(UPDATES_CONFIGURATION_CODE_SIGNING_METADATA) ?: (context?.getMetadataValue<String>("expo.modules.updates.CODE_SIGNING_METADATA") ?: "{}").let {
92 UpdatesUtils.getMapFromJSONString(it)
93 },
94 codeSigningIncludeManifestResponseCertificateChain = overrideMap?.readValueCheckingType<Boolean>(
95 UPDATES_CONFIGURATION_CODE_SIGNING_INCLUDE_MANIFEST_RESPONSE_CERTIFICATE_CHAIN
96 ) ?: context?.getMetadataValue("expo.modules.updates.CODE_SIGNING_INCLUDE_MANIFEST_RESPONSE_CERTIFICATE_CHAIN") ?: false,
97 codeSigningAllowUnsignedManifests = overrideMap?.readValueCheckingType<Boolean>(
98 UPDATES_CONFIGURATION_CODE_SIGNING_ALLOW_UNSIGNED_MANIFESTS
99 ) ?: context?.getMetadataValue("expo.modules.updates.CODE_SIGNING_ALLOW_UNSIGNED_MANIFESTS") ?: false,
100 enableExpoUpdatesProtocolV0CompatibilityMode = overrideMap?.readValueCheckingType<Boolean>(UPDATES_CONFIGURATION_ENABLE_EXPO_UPDATES_PROTOCOL_V0_COMPATIBILITY_MODE) ?: context?.getMetadataValue("expo.modules.updates.ENABLE_EXPO_UPDATES_PROTOCOL_V0_COMPATIBILITY_MODE") ?: false,
101 )
102
103 val isMissingRuntimeVersion: Boolean
104 get() = (runtimeVersion == null || runtimeVersion.isEmpty()) &&
105 (sdkVersion == null || sdkVersion.isEmpty())
106
<lambda>null107 val codeSigningConfiguration: CodeSigningConfiguration? by lazy {
108 codeSigningCertificate?.let {
109 CodeSigningConfiguration(it, codeSigningMetadata, codeSigningIncludeManifestResponseCertificateChain, codeSigningAllowUnsignedManifests)
110 }
111 }
112
113 companion object {
114 private val TAG = UpdatesConfiguration::class.java.simpleName
115
116 const val UPDATES_CONFIGURATION_ENABLED_KEY = "enabled"
117 const val UPDATES_CONFIGURATION_SCOPE_KEY_KEY = "scopeKey"
118 const val UPDATES_CONFIGURATION_UPDATE_URL_KEY = "updateUrl"
119 const val UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY = "requestHeaders"
120 const val UPDATES_CONFIGURATION_RELEASE_CHANNEL_KEY = "releaseChannel"
121 const val UPDATES_CONFIGURATION_SDK_VERSION_KEY = "sdkVersion"
122 const val UPDATES_CONFIGURATION_RUNTIME_VERSION_KEY = "runtimeVersion"
123 const val UPDATES_CONFIGURATION_CHECK_ON_LAUNCH_KEY = "checkOnLaunch"
124 const val UPDATES_CONFIGURATION_LAUNCH_WAIT_MS_KEY = "launchWaitMs"
125 const val UPDATES_CONFIGURATION_HAS_EMBEDDED_UPDATE_KEY = "hasEmbeddedUpdate"
126 const val UPDATES_CONFIGURATION_EXPECTS_EXPO_SIGNED_MANIFEST = "expectsSignedManifest"
127 const val UPDATES_CONFIGURATION_ENABLE_EXPO_UPDATES_PROTOCOL_V0_COMPATIBILITY_MODE = "enableExpoUpdatesProtocolCompatibilityMode"
128
129 const val UPDATES_CONFIGURATION_CODE_SIGNING_CERTIFICATE = "codeSigningCertificate"
130 const val UPDATES_CONFIGURATION_CODE_SIGNING_METADATA = "codeSigningMetadata"
131 const val UPDATES_CONFIGURATION_CODE_SIGNING_INCLUDE_MANIFEST_RESPONSE_CERTIFICATE_CHAIN = "codeSigningIncludeManifestResponseCertificateChain"
132 const val UPDATES_CONFIGURATION_CODE_SIGNING_ALLOW_UNSIGNED_MANIFESTS = "codeSigningAllowUnsignedManifests"
133
134 private const val UPDATES_CONFIGURATION_RELEASE_CHANNEL_DEFAULT_VALUE = "default"
135 private const val UPDATES_CONFIGURATION_LAUNCH_WAIT_MS_DEFAULT_VALUE = 0
136 }
137 }
138
getMetadataValuenull139 private inline fun <reified T : Any> Context.getMetadataValue(key: String): T? {
140 val ai = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA).metaData
141 if (!ai.containsKey(key)) {
142 return null
143 }
144 return when (T::class) {
145 String::class -> ai.getString(key) as T?
146 Boolean::class -> ai.getBoolean(key) as T?
147 Int::class -> ai.getInt(key) as T?
148 else -> ai[key] as T?
149 }
150 }
151
readValueCheckingTypenull152 private inline fun <reified T : Any> Map<String, Any>.readValueCheckingType(key: String): T? {
153 if (!containsKey(key)) {
154 return null
155 }
156 val value = this[key]
157 return if (value is T) {
158 value
159 } else {
160 throw AssertionError("UpdatesConfiguration failed to initialize: bad value of type " + value!!.javaClass.simpleName + " provided for key " + key)
161 }
162 }
163
getDefaultPortForSchemenull164 private fun getDefaultPortForScheme(scheme: String?): Int {
165 if ("http" == scheme || "ws" == scheme) {
166 return 80
167 } else if ("https" == scheme || "wss" == scheme) {
168 return 443
169 } else if ("ftp" == scheme) {
170 return 21
171 }
172 return -1
173 }
174
getNormalizedUrlOriginnull175 internal fun getNormalizedUrlOrigin(url: Uri): String {
176 val scheme = url.scheme
177 var port = url.port
178 if (port == getDefaultPortForScheme(scheme)) {
179 port = -1
180 }
181 return if (port > -1) "$scheme://${url.host}:$port" else "$scheme://${url.host}"
182 }
183
maybeGetDefaultScopeKeynull184 private fun maybeGetDefaultScopeKey(scopeKey: String?, updateUrl: Uri?): String? {
185 // set updateUrl as the default value if none is provided
186 if (scopeKey == null) {
187 if (updateUrl != null) {
188 return getNormalizedUrlOrigin(updateUrl)
189 }
190 }
191 return scopeKey
192 }
193