<lambda>null1 package expo.modules.updates
2
3 import android.content.Context
4 import expo.modules.updates.UpdatesConfiguration.CheckAutomaticallyConfiguration
5 import expo.modules.updates.db.entity.AssetEntity
6 import android.os.AsyncTask
7 import android.net.ConnectivityManager
8 import android.os.Build
9 import android.util.Base64
10 import android.util.Log
11 import com.facebook.react.ReactNativeHost
12 import com.facebook.react.bridge.Arguments
13 import com.facebook.react.bridge.ReactContext
14 import com.facebook.react.bridge.WritableMap
15 import com.facebook.react.modules.core.DeviceEventManagerModule
16 import expo.modules.updates.logging.UpdatesErrorCode
17 import expo.modules.updates.logging.UpdatesLogger
18 import org.apache.commons.io.FileUtils
19 import org.json.JSONArray
20 import org.json.JSONObject
21 import java.io.*
22 import java.lang.ClassCastException
23 import java.lang.Exception
24 import java.lang.IllegalArgumentException
25 import java.lang.ref.WeakReference
26 import java.security.DigestInputStream
27 import java.security.MessageDigest
28 import java.security.NoSuchAlgorithmException
29 import java.text.DateFormat
30 import java.text.ParseException
31 import java.text.SimpleDateFormat
32 import java.util.*
33 import kotlin.experimental.and
34
35 /**
36 * Miscellaneous helper functions that are used by multiple classes in the library.
37 */
38 object UpdatesUtils {
39 private val TAG = UpdatesUtils::class.java.simpleName
40
41 private const val UPDATES_DIRECTORY_NAME = ".expo-internal"
42
43 @Throws(Exception::class)
44 fun getMapFromJSONString(stringifiedJSON: String): Map<String, String> {
45 val jsonObject = JSONObject(stringifiedJSON)
46 val keys = jsonObject.keys()
47 val newMap = mutableMapOf<String, String>()
48 while (keys.hasNext()) {
49 val key = keys.next()
50 newMap[key] = try {
51 jsonObject[key] as String
52 } catch (e: ClassCastException) {
53 throw Exception("The values in the JSON object must be strings")
54 }
55 }
56 return newMap
57 }
58
59 @Throws(Exception::class)
60 fun getStringListFromJSONString(stringifiedJSON: String): List<String> {
61 val jsonArray = JSONArray(stringifiedJSON)
62 return List(jsonArray.length()) { index -> jsonArray.getString(index) }
63 }
64
65 @Throws(Exception::class)
66 fun getOrCreateUpdatesDirectory(context: Context): File {
67 val updatesDirectory = File(context.filesDir, UPDATES_DIRECTORY_NAME)
68 val exists = updatesDirectory.exists()
69 if (exists) {
70 if (updatesDirectory.isFile) {
71 throw Exception("File already exists at the location of the Updates Directory: $updatesDirectory ; aborting")
72 }
73 } else {
74 if (!updatesDirectory.mkdir()) {
75 throw Exception("Failed to create Updates Directory: mkdir() returned false")
76 }
77 }
78 return updatesDirectory
79 }
80
81 @Throws(NoSuchAlgorithmException::class, UnsupportedEncodingException::class)
82 fun sha256(string: String): String {
83 return try {
84 val md = MessageDigest.getInstance("SHA-256")
85 val data = string.toByteArray(charset("UTF-8"))
86 md.update(data, 0, data.size)
87 val sha1hash = md.digest()
88 bytesToHex(sha1hash)
89 } catch (e: NoSuchAlgorithmException) {
90 Log.e(TAG, "Failed to checksum string via SHA-256", e)
91 throw e
92 } catch (e: UnsupportedEncodingException) {
93 Log.e(TAG, "Failed to checksum string via SHA-256", e)
94 throw e
95 }
96 }
97
98 @Throws(NoSuchAlgorithmException::class, IOException::class)
99 fun sha256(file: File): ByteArray {
100 try {
101 FileInputStream(file).use { inputStream ->
102 DigestInputStream(
103 inputStream,
104 MessageDigest.getInstance("SHA-256")
105 ).use { digestInputStream ->
106 val md = digestInputStream.messageDigest
107 return md.digest()
108 }
109 }
110 } catch (e: NoSuchAlgorithmException) {
111 Log.e(TAG, "Failed to checksum file via SHA-256: $file", e)
112 throw e
113 } catch (e: IOException) {
114 Log.e(TAG, "Failed to checksum file via SHA-256: $file", e)
115 throw e
116 }
117 }
118
119 @Throws(NoSuchAlgorithmException::class, IOException::class)
120 fun verifySHA256AndWriteToFile(inputStream: InputStream, destination: File, expectedBase64URLEncodedHash: String?): ByteArray {
121 DigestInputStream(inputStream, MessageDigest.getInstance("SHA-256")).use { digestInputStream ->
122 // write file atomically by writing it to a temporary path and then renaming
123 // this protects us against partially written files if the process is interrupted
124 val tmpFile = File(destination.absolutePath + ".tmp")
125 FileUtils.copyInputStreamToFile(digestInputStream, tmpFile)
126
127 // this message digest must be read after the input stream has been consumed in order to get the hash correctly
128 val md = digestInputStream.messageDigest
129 val hash = md.digest()
130 // base64url - https://datatracker.ietf.org/doc/html/rfc4648#section-5
131 val hashBase64String = Base64.encodeToString(hash, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
132 if (expectedBase64URLEncodedHash != null && expectedBase64URLEncodedHash != hashBase64String) {
133 throw IOException("File download was successful but base64url-encoded SHA-256 did not match expected; expected: $expectedBase64URLEncodedHash; actual: $hashBase64String")
134 }
135
136 // only rename after the hash has been verified
137 if (!tmpFile.renameTo(destination)) {
138 throw IOException("File download was successful, but failed to move from temporary to permanent location " + destination.absolutePath)
139 }
140
141 return hash
142 }
143 }
144
145 fun createFilenameForAsset(asset: AssetEntity): String {
146 var fileExtension: String? = ""
147 if (asset.type != null) {
148 fileExtension = if (asset.type!!.startsWith(".")) asset.type else "." + asset.type
149 }
150 return if (asset.key == null) {
151 // create a filename that's unlikely to collide with any other asset
152 "asset-" + Date().time + "-" + Random().nextInt() + fileExtension
153 } else asset.key + fileExtension
154 }
155
156 fun sendEventToReactNative(
157 reactNativeHost: WeakReference<ReactNativeHost>?,
158 logger: UpdatesLogger,
159 eventName: String,
160 eventType: String,
161 params: WritableMap?
162 ) {
163 val host = reactNativeHost?.get()
164 if (host != null) {
165 AsyncTask.execute {
166 try {
167 var reactContext: ReactContext? = null
168 // in case we're trying to send an event before the reactContext has been initialized
169 // continue to retry for 5000ms
170 for (i in 0..4) {
171 // Calling host.reactInstanceManager has a side effect of creating a new
172 // reactInstanceManager if there isn't already one. We want to avoid this so we check
173 // if it has an instance first.
174 if (host.hasInstance()) {
175 reactContext = host.reactInstanceManager.currentReactContext
176 if (reactContext != null) {
177 break
178 }
179 }
180 Thread.sleep(1000)
181 }
182 if (reactContext != null) {
183 val emitter = reactContext.getJSModule(
184 DeviceEventManagerModule.RCTDeviceEventEmitter::class.java
185 )
186 if (emitter != null) {
187 var eventParams = params
188 if (eventParams == null) {
189 eventParams = Arguments.createMap()
190 }
191 eventParams!!.putString("type", eventType)
192 logger.info("Emitted event: name = $eventName, type = $eventType")
193 emitter.emit(eventName, eventParams)
194 return@execute
195 }
196 }
197 logger.error("Could not emit $eventName $eventType event; no event emitter was found.", UpdatesErrorCode.JSRuntimeError)
198 } catch (e: Exception) {
199 logger.error("Could not emit $eventName $eventType event; no react context was found.", UpdatesErrorCode.JSRuntimeError)
200 }
201 }
202 } else {
203 logger.error(
204 "Could not emit $eventType event; UpdatesController was not initialized with an instance of ReactApplication.",
205 UpdatesErrorCode.Unknown
206 )
207 }
208 }
209
210 fun shouldCheckForUpdateOnLaunch(
211 updatesConfiguration: UpdatesConfiguration,
212 context: Context
213 ): Boolean {
214 if (updatesConfiguration.updateUrl == null) {
215 return false
216 }
217 return when (updatesConfiguration.checkOnLaunch) {
218 CheckAutomaticallyConfiguration.NEVER -> false
219 // check will happen later on if there's an error
220 CheckAutomaticallyConfiguration.ERROR_RECOVERY_ONLY -> false
221 CheckAutomaticallyConfiguration.WIFI_ONLY -> {
222 val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
223 if (cm == null) {
224 Log.e(
225 TAG,
226 "Could not determine active network connection is metered; not checking for updates"
227 )
228 return false
229 }
230 !cm.isActiveNetworkMetered
231 }
232 CheckAutomaticallyConfiguration.ALWAYS -> true
233 }
234 }
235
236 fun getRuntimeVersion(updatesConfiguration: UpdatesConfiguration): String {
237 val runtimeVersion = updatesConfiguration.runtimeVersion
238 val sdkVersion = updatesConfiguration.sdkVersion
239 return if (runtimeVersion != null && runtimeVersion.isNotEmpty()) {
240 runtimeVersion
241 } else if (sdkVersion != null && sdkVersion.isNotEmpty()) {
242 sdkVersion
243 } else {
244 // various places in the code assume that we have a nonnull runtimeVersion, so if the developer
245 // hasn't configured either runtimeVersion or sdkVersion, we'll use a dummy value of "1" but warn
246 // the developer in JS that they need to configure one of these values
247 "1"
248 }
249 }
250
251 // https://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to-a-hex-string-in-java
252 private val HEX_ARRAY = "0123456789ABCDEF".toCharArray()
253 fun bytesToHex(bytes: ByteArray): String {
254 val hexChars = CharArray(bytes.size * 2)
255 for (j in bytes.indices) {
256 val v = (bytes[j] and 0xFF.toByte()).toInt()
257 hexChars[j * 2] = HEX_ARRAY[v ushr 4]
258 hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F]
259 }
260 return String(hexChars)
261 }
262
263 @Throws(ParseException::class)
264 fun parseDateString(dateString: String): Date {
265 return try {
266 val formatter: DateFormat = when (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
267 true -> SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'X'", Locale.US)
268 false -> SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
269 timeZone = TimeZone.getTimeZone("GMT")
270 }
271 }
272 formatter.parse(dateString) as Date
273 } catch (e: ParseException) {
274 Log.e(TAG, "Failed to parse date string on first try: $dateString", e)
275 // some old Android versions don't support the 'X' character in SimpleDateFormat, so try without this
276 val formatter: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
277 formatter.timeZone = TimeZone.getTimeZone("GMT")
278 // throw if this fails too
279 formatter.parse(dateString) as Date
280 } catch (e: IllegalArgumentException) {
281 Log.e(TAG, "Failed to parse date string on first try: $dateString", e)
282 val formatter: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
283 formatter.timeZone = TimeZone.getTimeZone("GMT")
284 formatter.parse(dateString) as Date
285 }
286 }
287 }
288