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