@hot-updater/react-native 1.0.0-rc.15 → 1.0.0-rc.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +491 -388
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +5 -11
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +290 -12
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +35 -19
- package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
- package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
- package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
- package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +110 -52
- package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
- package/android/src/newarch/HotUpdaterModule.kt +74 -69
- package/android/src/oldarch/HotUpdaterModule.kt +63 -59
- package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +1 -1
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +446 -61
- package/android/src/test/java/com/hotupdater/HotUpdaterExceptionTest.kt +220 -0
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +19 -27
- package/android/src/test/java/com/hotupdater/InstallIdentityServiceTest.kt +71 -0
- package/android/src/test/java/com/hotupdater/KeyValueStorageServiceTest.kt +106 -0
- package/android/src/test/java/com/hotupdater/NoBackupContext.kt +11 -0
- package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +309 -23
- package/android/src/test/java/com/hotupdater/PreviousProcessExitTest.kt +57 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +238 -157
- package/ios/HotUpdater/Internal/BundleMetadata.swift +3 -53
- package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +8 -18
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +48 -29
- package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +108 -1
- package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
- package/lib/commonjs/appReady.js +111 -0
- package/lib/commonjs/appReady.js.map +1 -0
- package/lib/commonjs/appReady.spec.js +360 -0
- package/lib/commonjs/appReady.spec.js.map +1 -0
- package/lib/commonjs/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
- package/lib/commonjs/appReady.test-utils.js.map +1 -0
- package/lib/commonjs/baseURL.js +16 -0
- package/lib/commonjs/baseURL.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +104 -56
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +337 -74
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/commonjs/clientPlugin.js +49 -0
- package/lib/commonjs/clientPlugin.js.map +1 -0
- package/lib/commonjs/entryImportGraph.spec.js +50 -0
- package/lib/commonjs/entryImportGraph.spec.js.map +1 -0
- package/lib/commonjs/fetchJSON.js +2 -6
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/httpClient.js +15 -67
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/httpClient.spec.js +26 -9
- package/lib/commonjs/httpClient.spec.js.map +1 -1
- package/lib/commonjs/index.js +49 -25
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +126 -40
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +67 -48
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +80 -58
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/pluginHost.js +208 -0
- package/lib/commonjs/pluginHost.js.map +1 -0
- package/lib/commonjs/pluginHost.spec.js +292 -0
- package/lib/commonjs/pluginHost.spec.js.map +1 -0
- package/lib/commonjs/plugins/insights/eventId.js +129 -0
- package/lib/commonjs/plugins/insights/eventId.js.map +1 -0
- package/lib/commonjs/plugins/insights/eventId.spec.js +47 -0
- package/lib/commonjs/plugins/insights/eventId.spec.js.map +1 -0
- package/lib/commonjs/plugins/insights/index.js +272 -0
- package/lib/commonjs/plugins/insights/index.js.map +1 -0
- package/lib/commonjs/plugins/insights/insights.spec.js +640 -0
- package/lib/commonjs/plugins/insights/insights.spec.js.map +1 -0
- package/lib/commonjs/plugins/insights/sender.js +128 -0
- package/lib/commonjs/plugins/insights/sender.js.map +1 -0
- package/lib/commonjs/plugins/insights/sender.spec.js +265 -0
- package/lib/commonjs/plugins/insights/sender.spec.js.map +1 -0
- package/lib/commonjs/plugins/insights/state.js +145 -0
- package/lib/commonjs/plugins/insights/state.js.map +1 -0
- package/lib/commonjs/releaseCatalogCache.js +13 -2
- package/lib/commonjs/releaseCatalogCache.js.map +1 -1
- package/lib/commonjs/releaseCatalogCache.spec.js +18 -0
- package/lib/commonjs/releaseCatalogCache.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/updateError.js +132 -0
- package/lib/commonjs/updateError.js.map +1 -0
- package/lib/commonjs/updateError.spec.js +149 -0
- package/lib/commonjs/updateError.spec.js.map +1 -0
- package/lib/commonjs/wrap.js +43 -25
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.progress.spec.js +195 -0
- package/lib/commonjs/wrap.progress.spec.js.map +1 -0
- package/lib/commonjs/wrap.spec.js +94 -230
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/appReady.js +103 -0
- package/lib/module/appReady.js.map +1 -0
- package/lib/module/appReady.spec.js +359 -0
- package/lib/module/appReady.spec.js.map +1 -0
- package/lib/module/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
- package/lib/module/appReady.test-utils.js.map +1 -0
- package/lib/module/baseURL.js +11 -0
- package/lib/module/baseURL.js.map +1 -0
- package/lib/module/checkForUpdate.js +103 -57
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +337 -74
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/module/clientPlugin.js +44 -0
- package/lib/module/clientPlugin.js.map +1 -0
- package/lib/module/entryImportGraph.spec.js +50 -0
- package/lib/module/entryImportGraph.spec.js.map +1 -0
- package/lib/module/fetchJSON.js +2 -6
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/httpClient.js +14 -66
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/httpClient.spec.js +26 -9
- package/lib/module/httpClient.spec.js.map +1 -1
- package/lib/module/index.js +44 -27
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +127 -41
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +63 -44
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +80 -58
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/pluginHost.js +200 -0
- package/lib/module/pluginHost.js.map +1 -0
- package/lib/module/pluginHost.spec.js +291 -0
- package/lib/module/pluginHost.spec.js.map +1 -0
- package/lib/module/plugins/insights/eventId.js +122 -0
- package/lib/module/plugins/insights/eventId.js.map +1 -0
- package/lib/module/plugins/insights/eventId.spec.js +47 -0
- package/lib/module/plugins/insights/eventId.spec.js.map +1 -0
- package/lib/module/plugins/insights/index.js +268 -0
- package/lib/module/plugins/insights/index.js.map +1 -0
- package/lib/module/plugins/insights/insights.spec.js +639 -0
- package/lib/module/plugins/insights/insights.spec.js.map +1 -0
- package/lib/module/plugins/insights/sender.js +123 -0
- package/lib/module/plugins/insights/sender.js.map +1 -0
- package/lib/module/plugins/insights/sender.spec.js +265 -0
- package/lib/module/plugins/insights/sender.spec.js.map +1 -0
- package/lib/module/plugins/insights/state.js +139 -0
- package/lib/module/plugins/insights/state.js.map +1 -0
- package/lib/module/releaseCatalogCache.js +13 -2
- package/lib/module/releaseCatalogCache.js.map +1 -1
- package/lib/module/releaseCatalogCache.spec.js +18 -0
- package/lib/module/releaseCatalogCache.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/updateError.js +126 -0
- package/lib/module/updateError.js.map +1 -0
- package/lib/module/updateError.spec.js +149 -0
- package/lib/module/updateError.spec.js.map +1 -0
- package/lib/module/wrap.js +43 -25
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.progress.spec.js +196 -0
- package/lib/module/wrap.progress.spec.js.map +1 -0
- package/lib/module/wrap.spec.js +93 -229
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/appReady.d.ts +23 -0
- package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
- package/lib/typescript/commonjs/appReady.test-utils.d.ts +9 -0
- package/lib/typescript/commonjs/appReady.test-utils.d.ts.map +1 -0
- package/lib/typescript/commonjs/baseURL.d.ts +4 -0
- package/lib/typescript/commonjs/baseURL.d.ts.map +1 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts +15 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/clientPlugin.d.ts +185 -0
- package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
- package/lib/typescript/commonjs/fetchJSON.d.ts +2 -3
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +2 -30
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -6
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +29 -15
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/pluginHost.d.ts +29 -0
- package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/plugins/insights/eventId.d.ts +18 -0
- package/lib/typescript/commonjs/plugins/insights/eventId.d.ts.map +1 -0
- package/lib/typescript/commonjs/plugins/insights/index.d.ts +40 -0
- package/lib/typescript/commonjs/plugins/insights/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/plugins/insights/sender.d.ts +74 -0
- package/lib/typescript/commonjs/plugins/insights/sender.d.ts.map +1 -0
- package/lib/typescript/commonjs/plugins/insights/state.d.ts +46 -0
- package/lib/typescript/commonjs/plugins/insights/state.d.ts.map +1 -0
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts +5 -1
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +34 -12
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/updateError.d.ts +26 -0
- package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +6 -5
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/appReady.d.ts +23 -0
- package/lib/typescript/module/appReady.d.ts.map +1 -0
- package/lib/typescript/module/appReady.test-utils.d.ts +9 -0
- package/lib/typescript/module/appReady.test-utils.d.ts.map +1 -0
- package/lib/typescript/module/baseURL.d.ts +4 -0
- package/lib/typescript/module/baseURL.d.ts.map +1 -0
- package/lib/typescript/module/checkForUpdate.d.ts +15 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/clientPlugin.d.ts +185 -0
- package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
- package/lib/typescript/module/fetchJSON.d.ts +2 -3
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +2 -30
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -6
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +29 -15
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/pluginHost.d.ts +29 -0
- package/lib/typescript/module/pluginHost.d.ts.map +1 -0
- package/lib/typescript/module/plugins/insights/eventId.d.ts +18 -0
- package/lib/typescript/module/plugins/insights/eventId.d.ts.map +1 -0
- package/lib/typescript/module/plugins/insights/index.d.ts +40 -0
- package/lib/typescript/module/plugins/insights/index.d.ts.map +1 -0
- package/lib/typescript/module/plugins/insights/sender.d.ts +74 -0
- package/lib/typescript/module/plugins/insights/sender.d.ts.map +1 -0
- package/lib/typescript/module/plugins/insights/state.d.ts +46 -0
- package/lib/typescript/module/plugins/insights/state.d.ts.map +1 -0
- package/lib/typescript/module/releaseCatalogCache.d.ts +5 -1
- package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +34 -12
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/updateError.d.ts +26 -0
- package/lib/typescript/module/updateError.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +6 -5
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +14 -2
- package/plugins/insights/package.json +8 -0
- package/src/appReady.spec.ts +393 -0
- package/src/{notifyAppReadyInsights.test-utils.ts → appReady.test-utils.ts} +7 -7
- package/src/appReady.ts +149 -0
- package/src/baseURL.ts +15 -0
- package/src/checkForUpdate.releaseCatalog.spec.ts +370 -76
- package/src/checkForUpdate.ts +135 -80
- package/src/clientPlugin.ts +231 -0
- package/src/entryImportGraph.spec.ts +65 -0
- package/src/fetchJSON.ts +3 -10
- package/src/httpClient.spec.ts +28 -9
- package/src/httpClient.ts +16 -109
- package/src/index.spec.ts +129 -36
- package/src/index.ts +67 -37
- package/src/native.spec.ts +98 -72
- package/src/native.ts +93 -77
- package/src/pluginHost.spec.ts +297 -0
- package/src/pluginHost.ts +262 -0
- package/src/plugins/insights/eventId.spec.ts +60 -0
- package/src/plugins/insights/eventId.ts +171 -0
- package/src/plugins/insights/index.ts +367 -0
- package/src/plugins/insights/insights.spec.ts +732 -0
- package/src/plugins/insights/sender.spec.ts +287 -0
- package/src/plugins/insights/sender.ts +221 -0
- package/src/plugins/insights/state.ts +184 -0
- package/src/releaseCatalogCache.spec.ts +22 -0
- package/src/releaseCatalogCache.ts +19 -4
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +34 -13
- package/src/updateError.spec.ts +144 -0
- package/src/updateError.ts +153 -0
- package/src/wrap.progress.spec.ts +232 -0
- package/src/wrap.spec.ts +100 -283
- package/src/wrap.tsx +57 -26
- package/src/wrap.types.ts +6 -5
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -225
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.js +0 -188
- package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -378
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -124
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -224
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.js +0 -181
- package/lib/module/notifyAppReadyInsights.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.spec.js +0 -377
- package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -123
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -25
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -25
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
- package/src/notifyAppReadyInsights.boundary.spec.ts +0 -242
- package/src/notifyAppReadyInsights.spec.ts +0 -362
- package/src/notifyAppReadyInsights.transport.spec.ts +0 -138
- package/src/notifyAppReadyInsights.ts +0 -293
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import androidx.core.util.AtomicFile
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
import java.io.File
|
|
8
|
+
import java.io.FileNotFoundException
|
|
9
|
+
import java.io.FileOutputStream
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The persistent string key-value store behind `getStorageItem` and
|
|
13
|
+
* `setStorageItem`, which client plugins use through JS. Keys are opaque
|
|
14
|
+
* here; JS scopes them per plugin.
|
|
15
|
+
*
|
|
16
|
+
* Values live as one JSON object in a file outside device backups. The file
|
|
17
|
+
* is read once, on first use, and rewritten atomically on every set. A
|
|
18
|
+
* missing, unreadable, or corrupt file reads as empty and is replaced by the
|
|
19
|
+
* next set. A set that cannot be persisted still applies for this process.
|
|
20
|
+
*/
|
|
21
|
+
class KeyValueStorageService(
|
|
22
|
+
private val file: File,
|
|
23
|
+
) {
|
|
24
|
+
companion object {
|
|
25
|
+
private const val TAG = "KeyValueStorage"
|
|
26
|
+
const val FILENAME = "storage.json"
|
|
27
|
+
|
|
28
|
+
/** The store kept in `noBackupFilesDir/hot-updater/storage.json`. */
|
|
29
|
+
fun create(context: Context): KeyValueStorageService = KeyValueStorageService(NoBackupStorage.file(context, FILENAME))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private var values: MutableMap<String, String>? = null
|
|
33
|
+
|
|
34
|
+
@Synchronized
|
|
35
|
+
fun getItem(key: String): String? = loadValues()[key]
|
|
36
|
+
|
|
37
|
+
/** Stores [value] under [key], or removes [key] when [value] is null. */
|
|
38
|
+
@Synchronized
|
|
39
|
+
fun setItem(
|
|
40
|
+
key: String,
|
|
41
|
+
value: String?,
|
|
42
|
+
) {
|
|
43
|
+
val values = loadValues()
|
|
44
|
+
if (value == null) {
|
|
45
|
+
values.remove(key)
|
|
46
|
+
} else {
|
|
47
|
+
values[key] = value
|
|
48
|
+
}
|
|
49
|
+
write(values)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private fun loadValues(): MutableMap<String, String> = values ?: read().also { values = it }
|
|
53
|
+
|
|
54
|
+
private fun read(): MutableMap<String, String> =
|
|
55
|
+
try {
|
|
56
|
+
val json =
|
|
57
|
+
AtomicFile(file).openRead().use { input ->
|
|
58
|
+
JSONObject(String(input.readBytes(), Charsets.UTF_8))
|
|
59
|
+
}
|
|
60
|
+
val stored = linkedMapOf<String, String>()
|
|
61
|
+
json.keys().forEach { key ->
|
|
62
|
+
(json.opt(key) as? String)?.let { stored[key] = it }
|
|
63
|
+
}
|
|
64
|
+
stored
|
|
65
|
+
} catch (_: FileNotFoundException) {
|
|
66
|
+
linkedMapOf()
|
|
67
|
+
} catch (e: Exception) {
|
|
68
|
+
// The message of a parse error can quote stored values, so it is not logged.
|
|
69
|
+
Log.w(TAG, "Ignoring unreadable storage file (${e.javaClass.simpleName})")
|
|
70
|
+
linkedMapOf()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private fun write(values: Map<String, String>) {
|
|
74
|
+
val atomicFile = AtomicFile(file)
|
|
75
|
+
var output: FileOutputStream? = null
|
|
76
|
+
try {
|
|
77
|
+
file.parentFile?.mkdirs()
|
|
78
|
+
output = atomicFile.startWrite()
|
|
79
|
+
output.write(JSONObject(values).toString().toByteArray(Charsets.UTF_8))
|
|
80
|
+
atomicFile.finishWrite(output)
|
|
81
|
+
} catch (e: Exception) {
|
|
82
|
+
output?.let(atomicFile::failWrite)
|
|
83
|
+
Log.w(TAG, "Failed to persist storage", e)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.pm.PackageManager
|
|
6
|
+
import android.net.ConnectivityManager
|
|
7
|
+
import android.os.Build
|
|
8
|
+
|
|
9
|
+
internal object NetworkState {
|
|
10
|
+
/**
|
|
11
|
+
* True when the device reports no active network. False when it has one,
|
|
12
|
+
* or when that cannot be told, as when the app lacks
|
|
13
|
+
* `ACCESS_NETWORK_STATE`.
|
|
14
|
+
*/
|
|
15
|
+
fun hasNoActiveNetwork(context: Context): Boolean =
|
|
16
|
+
try {
|
|
17
|
+
val permission = context.checkCallingOrSelfPermission(Manifest.permission.ACCESS_NETWORK_STATE)
|
|
18
|
+
val connectivity = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
|
19
|
+
when {
|
|
20
|
+
permission != PackageManager.PERMISSION_GRANTED || connectivity == null -> false
|
|
21
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> connectivity.activeNetwork == null
|
|
22
|
+
else -> hasNoConnectedNetworkLegacy(connectivity)
|
|
23
|
+
}
|
|
24
|
+
} catch (_: Exception) {
|
|
25
|
+
false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Suppress("DEPRECATION")
|
|
29
|
+
private fun hasNoConnectedNetworkLegacy(connectivity: ConnectivityManager): Boolean =
|
|
30
|
+
connectivity.activeNetworkInfo?.isConnected != true
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import java.io.File
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SDK state that must never be restored onto another installation lives in
|
|
8
|
+
* `noBackupFilesDir/hot-updater`, which Android Auto Backup skips.
|
|
9
|
+
*/
|
|
10
|
+
internal object NoBackupStorage {
|
|
11
|
+
private const val DIRECTORY_NAME = "hot-updater"
|
|
12
|
+
|
|
13
|
+
fun file(
|
|
14
|
+
context: Context,
|
|
15
|
+
name: String,
|
|
16
|
+
): File = File(File(context.noBackupFilesDir, DIRECTORY_NAME), name)
|
|
17
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.hotupdater
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
|
+
import kotlinx.coroutines.CancellationException
|
|
4
5
|
import kotlinx.coroutines.Dispatchers
|
|
5
6
|
import kotlinx.coroutines.delay
|
|
6
7
|
import kotlinx.coroutines.withContext
|
|
@@ -15,10 +16,11 @@ import okio.Source
|
|
|
15
16
|
import okio.buffer
|
|
16
17
|
import java.io.File
|
|
17
18
|
import java.io.IOException
|
|
18
|
-
import java.net.
|
|
19
|
+
import java.net.MalformedURLException
|
|
19
20
|
import java.net.URL
|
|
20
|
-
import java.net.UnknownHostException
|
|
21
21
|
import java.util.concurrent.TimeUnit
|
|
22
|
+
import javax.net.ssl.SSLHandshakeException
|
|
23
|
+
import javax.net.ssl.SSLPeerUnverifiedException
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Exception for incomplete downloads with size information
|
|
@@ -28,6 +30,47 @@ class IncompleteDownloadException(
|
|
|
28
30
|
val actualSize: Long,
|
|
29
31
|
) : IOException("Download incomplete: received $actualSize bytes, expected $expectedSize bytes")
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The server answered with a non-2xx status. [originCode] is the storage
|
|
35
|
+
* origin's XML error `<Code>` (S3, R2, or GCS style) when the body names one.
|
|
36
|
+
*/
|
|
37
|
+
class HttpStatusException(
|
|
38
|
+
val statusCode: Int,
|
|
39
|
+
statusMessage: String,
|
|
40
|
+
val originCode: String? = null,
|
|
41
|
+
) : Exception("HTTP error $statusCode: $statusMessage")
|
|
42
|
+
|
|
43
|
+
/** How much of an error body is read to find a storage origin's error code. */
|
|
44
|
+
internal const val MAX_ORIGIN_ERROR_BODY_BYTES = 4 * 1024L
|
|
45
|
+
|
|
46
|
+
private val ORIGIN_ERROR_CODE = Regex("<Error(?:\\s[^>]*)?>(?:(?!</Error>).)*?<Code>([^<]*)</Code>", RegexOption.DOT_MATCHES_ALL)
|
|
47
|
+
private val ORIGIN_CODE_VALUE = Regex("[A-Za-z0-9._-]{1,64}")
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reads the `<Code>` of a storage origin's XML error, as in
|
|
51
|
+
* `<Error><Code>AccessDenied</Code>...`, from at most the first 4 KB of
|
|
52
|
+
* [body]. Only a 1 to 64 character code of letters, digits, `.`, `_`, and `-`
|
|
53
|
+
* is kept, so no key, resource, or message text is ever returned.
|
|
54
|
+
*/
|
|
55
|
+
internal fun readOriginErrorCode(body: ResponseBody?): String? =
|
|
56
|
+
try {
|
|
57
|
+
body?.source()?.let { source ->
|
|
58
|
+
source.request(MAX_ORIGIN_ERROR_BODY_BYTES)
|
|
59
|
+
val prefix = source.buffer.readByteArray(minOf(source.buffer.size, MAX_ORIGIN_ERROR_BODY_BYTES))
|
|
60
|
+
parseOriginErrorCode(String(prefix, Charsets.UTF_8))
|
|
61
|
+
}
|
|
62
|
+
} catch (_: Exception) {
|
|
63
|
+
null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
internal fun parseOriginErrorCode(body: String): String? =
|
|
67
|
+
ORIGIN_ERROR_CODE
|
|
68
|
+
.find(body)
|
|
69
|
+
?.groupValues
|
|
70
|
+
?.get(1)
|
|
71
|
+
?.trim()
|
|
72
|
+
?.takeIf { ORIGIN_CODE_VALUE.matches(it) }
|
|
73
|
+
|
|
31
74
|
/**
|
|
32
75
|
* Result wrapper for download operations
|
|
33
76
|
*/
|
|
@@ -134,13 +177,33 @@ private class ProgressResponseBody(
|
|
|
134
177
|
}
|
|
135
178
|
}
|
|
136
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Whether a failed download may succeed if tried again: a network failure or
|
|
182
|
+
* timeout, a body that ended early, or a 408, 429, or 5xx answer. Another 4xx,
|
|
183
|
+
* a TLS certificate failure, a canceled call, and a local storage failure
|
|
184
|
+
* would fail the same way again.
|
|
185
|
+
*/
|
|
186
|
+
internal fun isRetryableDownloadError(error: Exception): Boolean =
|
|
187
|
+
when (error) {
|
|
188
|
+
is HttpStatusException -> error.statusCode == 408 || error.statusCode == 429 || error.statusCode >= 500
|
|
189
|
+
is LocalStorageException -> false
|
|
190
|
+
is SSLHandshakeException, is SSLPeerUnverifiedException -> false
|
|
191
|
+
is MalformedURLException -> false
|
|
192
|
+
is IOException -> error.message != "Canceled"
|
|
193
|
+
else -> false
|
|
194
|
+
}
|
|
195
|
+
|
|
137
196
|
/**
|
|
138
197
|
* OkHttp-based implementation of DownloadService with resume support
|
|
139
198
|
*/
|
|
140
|
-
class OkHttpDownloadService
|
|
199
|
+
class OkHttpDownloadService internal constructor(
|
|
200
|
+
private val initialRetryDelayMs: Long,
|
|
201
|
+
) : DownloadService {
|
|
202
|
+
constructor() : this(INITIAL_RETRY_DELAY_MS)
|
|
203
|
+
|
|
141
204
|
companion object {
|
|
142
205
|
private const val TAG = "OkHttpDownloadService"
|
|
143
|
-
|
|
206
|
+
internal const val MAX_ATTEMPTS = 3
|
|
144
207
|
private const val INITIAL_RETRY_DELAY_MS = 1000L
|
|
145
208
|
private const val TIMEOUT_SECONDS = 30L
|
|
146
209
|
}
|
|
@@ -153,6 +216,12 @@ class OkHttpDownloadService : DownloadService {
|
|
|
153
216
|
.writeTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
|
154
217
|
.build()
|
|
155
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Downloads [fileUrl], trying again after a retryable failure (see
|
|
221
|
+
* [isRetryableDownloadError]) up to [MAX_ATTEMPTS] attempts in all, 1 and
|
|
222
|
+
* then 2 seconds apart. The last attempt's result is returned, so a
|
|
223
|
+
* failure keeps what classified it.
|
|
224
|
+
*/
|
|
156
225
|
override suspend fun downloadFile(
|
|
157
226
|
fileUrl: URL,
|
|
158
227
|
destination: File,
|
|
@@ -160,39 +229,30 @@ class OkHttpDownloadService : DownloadService {
|
|
|
160
229
|
progressCallback: (DownloadProgress) -> Unit,
|
|
161
230
|
): DownloadResult =
|
|
162
231
|
withContext(Dispatchers.IO) {
|
|
163
|
-
var attempt =
|
|
164
|
-
var
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (attempt < MAX_RETRIES && isRetryableException(e)) {
|
|
179
|
-
val delayMs = INITIAL_RETRY_DELAY_MS * (1 shl (attempt - 1))
|
|
180
|
-
Log.d(
|
|
181
|
-
TAG,
|
|
182
|
-
"Download failed (attempt $attempt/$MAX_RETRIES): ${e.message}. Retrying in ${delayMs}ms...",
|
|
183
|
-
)
|
|
184
|
-
delay(delayMs)
|
|
185
|
-
} else {
|
|
186
|
-
Log.d(TAG, "Download failed: ${e.message}")
|
|
187
|
-
break
|
|
188
|
-
}
|
|
189
|
-
}
|
|
232
|
+
var attempt = 1
|
|
233
|
+
var result = attemptOrError(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
234
|
+
while (
|
|
235
|
+
result is DownloadResult.Error &&
|
|
236
|
+
attempt < MAX_ATTEMPTS &&
|
|
237
|
+
isRetryableDownloadError(result.exception)
|
|
238
|
+
) {
|
|
239
|
+
val delayMs = initialRetryDelayMs * (1 shl (attempt - 1))
|
|
240
|
+
Log.d(
|
|
241
|
+
TAG,
|
|
242
|
+
"Download failed (attempt $attempt/$MAX_ATTEMPTS): ${result.exception.message}. Retrying in ${delayMs}ms...",
|
|
243
|
+
)
|
|
244
|
+
delay(delayMs)
|
|
245
|
+
attempt++
|
|
246
|
+
result = attemptOrError(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
190
247
|
}
|
|
191
|
-
|
|
192
|
-
|
|
248
|
+
if (result is DownloadResult.Error) {
|
|
249
|
+
Log.d(TAG, "Download failed (attempt $attempt/$MAX_ATTEMPTS): ${result.exception.message}")
|
|
250
|
+
}
|
|
251
|
+
result
|
|
193
252
|
}
|
|
194
253
|
|
|
195
|
-
|
|
254
|
+
/** One attempt; an exception a callback threw, such as the disk space check's, is its error. */
|
|
255
|
+
private suspend fun attemptOrError(
|
|
196
256
|
fileUrl: URL,
|
|
197
257
|
destination: File,
|
|
198
258
|
fileSizeCallback: ((Long) -> Unit)?,
|
|
@@ -200,10 +260,20 @@ class OkHttpDownloadService : DownloadService {
|
|
|
200
260
|
): DownloadResult =
|
|
201
261
|
try {
|
|
202
262
|
attemptDownload(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
263
|
+
} catch (error: CancellationException) {
|
|
264
|
+
throw error
|
|
203
265
|
} catch (error: Exception) {
|
|
204
266
|
DownloadResult.Error(error)
|
|
205
267
|
}
|
|
206
268
|
|
|
269
|
+
/** One attempt with no retry, for a download with a fallback, such as the archive's per-file downloads. */
|
|
270
|
+
override suspend fun downloadFileOnce(
|
|
271
|
+
fileUrl: URL,
|
|
272
|
+
destination: File,
|
|
273
|
+
fileSizeCallback: ((Long) -> Unit)?,
|
|
274
|
+
progressCallback: (DownloadProgress) -> Unit,
|
|
275
|
+
): DownloadResult = attemptOrError(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
276
|
+
|
|
207
277
|
private suspend fun attemptDownload(
|
|
208
278
|
fileUrl: URL,
|
|
209
279
|
destination: File,
|
|
@@ -231,10 +301,10 @@ class OkHttpDownloadService : DownloadService {
|
|
|
231
301
|
}
|
|
232
302
|
|
|
233
303
|
if (!response.isSuccessful) {
|
|
234
|
-
val
|
|
235
|
-
Log.d(TAG,
|
|
304
|
+
val error = HttpStatusException(response.code, response.message, readOriginErrorCode(response.body))
|
|
305
|
+
Log.d(TAG, "HTTP error ${response.code}: ${response.message}")
|
|
236
306
|
response.close()
|
|
237
|
-
return@withContext DownloadResult.Error(
|
|
307
|
+
return@withContext DownloadResult.Error(error)
|
|
238
308
|
}
|
|
239
309
|
|
|
240
310
|
val body = response.body
|
|
@@ -261,9 +331,10 @@ class OkHttpDownloadService : DownloadService {
|
|
|
261
331
|
progressCallback.invoke(progress)
|
|
262
332
|
}
|
|
263
333
|
|
|
264
|
-
// Write to file
|
|
334
|
+
// Write to file. A local write failure surfaces as LocalStorageException,
|
|
335
|
+
// so it is not mistaken for a network failure.
|
|
265
336
|
progressBody.source().use { source ->
|
|
266
|
-
|
|
337
|
+
LocalStorageOutputStream.open(destination).use { output ->
|
|
267
338
|
val buffer = ByteArray(8 * 1024)
|
|
268
339
|
var bytesRead: Int
|
|
269
340
|
|
|
@@ -310,17 +381,4 @@ class OkHttpDownloadService : DownloadService {
|
|
|
310
381
|
DownloadResult.Error(e)
|
|
311
382
|
}
|
|
312
383
|
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Check if exception is retryable
|
|
316
|
-
*/
|
|
317
|
-
private fun isRetryableException(e: Exception): Boolean =
|
|
318
|
-
when (e) {
|
|
319
|
-
is SocketTimeoutException,
|
|
320
|
-
is UnknownHostException,
|
|
321
|
-
is IOException,
|
|
322
|
-
-> true
|
|
323
|
-
|
|
324
|
-
else -> false
|
|
325
|
-
}
|
|
326
384
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.app.ActivityManager
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import android.util.Log
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Why the app's previous main process exited, from
|
|
10
|
+
* `ActivityManager.getHistoricalProcessExitReasons` (Android 11+).
|
|
11
|
+
*
|
|
12
|
+
* HotUpdater calls [initialize] when it starts. The reason is read once, on
|
|
13
|
+
* first use, and kept for the life of the process: the running process never
|
|
14
|
+
* appears in its own exit history, so reading later returns the same entry.
|
|
15
|
+
*/
|
|
16
|
+
internal object PreviousProcessExit {
|
|
17
|
+
private const val TAG = "PreviousProcessExit"
|
|
18
|
+
private const val MAX_EXIT_RECORDS = 5
|
|
19
|
+
|
|
20
|
+
/** `ApplicationExitInfo.REASON_*` values, named without the prefix. */
|
|
21
|
+
private val REASON_NAMES =
|
|
22
|
+
mapOf(
|
|
23
|
+
0 to "UNKNOWN",
|
|
24
|
+
1 to "EXIT_SELF",
|
|
25
|
+
2 to "SIGNALED",
|
|
26
|
+
3 to "LOW_MEMORY",
|
|
27
|
+
4 to "CRASH",
|
|
28
|
+
5 to "CRASH_NATIVE",
|
|
29
|
+
6 to "ANR",
|
|
30
|
+
7 to "INITIALIZATION_FAILURE",
|
|
31
|
+
8 to "PERMISSION_CHANGE",
|
|
32
|
+
9 to "EXCESSIVE_RESOURCE_USAGE",
|
|
33
|
+
10 to "USER_REQUESTED",
|
|
34
|
+
11 to "USER_STOPPED",
|
|
35
|
+
12 to "DEPENDENCY_DIED",
|
|
36
|
+
13 to "OTHER",
|
|
37
|
+
14 to "FREEZER",
|
|
38
|
+
15 to "PACKAGE_STATE_CHANGE",
|
|
39
|
+
16 to "PACKAGE_UPDATED",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
data class ExitRecord(
|
|
43
|
+
val processName: String?,
|
|
44
|
+
val reason: Int,
|
|
45
|
+
val timestamp: Long,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// The application context, which lives as long as the process.
|
|
49
|
+
@Volatile
|
|
50
|
+
private var applicationContext: Context? = null
|
|
51
|
+
|
|
52
|
+
private val reason: String? by lazy { applicationContext?.let(::read) }
|
|
53
|
+
|
|
54
|
+
fun initialize(context: Context) {
|
|
55
|
+
if (applicationContext == null) {
|
|
56
|
+
applicationContext = context.applicationContext ?: context
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The previous main process's exit reason, or null when it is not known. */
|
|
61
|
+
fun get(): String? = if (applicationContext == null) null else reason
|
|
62
|
+
|
|
63
|
+
fun reasonName(reason: Int): String = REASON_NAMES[reason] ?: "UNKNOWN"
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The reason of [mainProcessName]'s most recent exit. Other processes of
|
|
67
|
+
* the app, such as `:hotupdater_restart`, are skipped.
|
|
68
|
+
*/
|
|
69
|
+
fun selectReason(
|
|
70
|
+
records: List<ExitRecord>,
|
|
71
|
+
mainProcessName: String,
|
|
72
|
+
): Int? = records.filter { it.processName == mainProcessName }.maxByOrNull { it.timestamp }?.reason
|
|
73
|
+
|
|
74
|
+
private fun read(context: Context): String? {
|
|
75
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null
|
|
76
|
+
return try {
|
|
77
|
+
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager ?: return null
|
|
78
|
+
val records =
|
|
79
|
+
activityManager
|
|
80
|
+
.getHistoricalProcessExitReasons(null, 0, MAX_EXIT_RECORDS)
|
|
81
|
+
.map { ExitRecord(it.processName, it.reason, it.timestamp) }
|
|
82
|
+
selectReason(records, context.applicationInfo.processName)?.let(::reasonName)
|
|
83
|
+
} catch (error: Throwable) {
|
|
84
|
+
// Never let this diagnostic break startup.
|
|
85
|
+
Log.w(TAG, "Could not read the previous process exit reason", error)
|
|
86
|
+
null
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
package com.hotupdater
|
|
2
2
|
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
3
5
|
import com.facebook.react.bridge.WritableArray
|
|
4
6
|
import com.facebook.react.bridge.WritableMap
|
|
5
7
|
import com.facebook.react.bridge.WritableNativeArray
|
|
6
8
|
import com.facebook.react.bridge.WritableNativeMap
|
|
7
9
|
|
|
10
|
+
/** The value an `updateBundle` promise resolves to: `{ delivery, patchFallback }`. */
|
|
11
|
+
internal fun UpdateBundleResult.toWritableMap(): WritableMap =
|
|
12
|
+
Arguments.createMap().apply {
|
|
13
|
+
putString("delivery", delivery.value)
|
|
14
|
+
putBoolean("patchFallback", patchFallback)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Rejects an `updateBundle` promise with the error's code and message. An
|
|
19
|
+
* update failure also carries its classification as the rejection's
|
|
20
|
+
* `userInfo`: `{ stage, reason, httpStatus? }`.
|
|
21
|
+
*/
|
|
22
|
+
internal fun Promise.rejectUpdateBundle(error: HotUpdaterException) {
|
|
23
|
+
val userInfo = error.failure?.toUserInfo()
|
|
24
|
+
if (userInfo == null) {
|
|
25
|
+
reject(error.code, error.message)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
reject(
|
|
29
|
+
error.code,
|
|
30
|
+
error.message,
|
|
31
|
+
Arguments.createMap().apply {
|
|
32
|
+
userInfo.forEach { (key, value) ->
|
|
33
|
+
when (value) {
|
|
34
|
+
is Int -> putInt(key, value)
|
|
35
|
+
else -> putString(key, value.toString())
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
8
42
|
internal fun Map<String, Any?>.toWritableNativeMap(): WritableNativeMap {
|
|
9
43
|
val result = WritableNativeMap()
|
|
10
44
|
forEach { (key, value) ->
|