@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.
Files changed (302) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +491 -388
  2. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +5 -11
  3. package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
  4. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +290 -12
  5. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +35 -19
  6. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
  7. package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
  8. package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
  9. package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
  10. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +110 -52
  11. package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
  12. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
  13. package/android/src/newarch/HotUpdaterModule.kt +74 -69
  14. package/android/src/oldarch/HotUpdaterModule.kt +63 -59
  15. package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
  16. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +1 -1
  17. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +446 -61
  18. package/android/src/test/java/com/hotupdater/HotUpdaterExceptionTest.kt +220 -0
  19. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +19 -27
  20. package/android/src/test/java/com/hotupdater/InstallIdentityServiceTest.kt +71 -0
  21. package/android/src/test/java/com/hotupdater/KeyValueStorageServiceTest.kt +106 -0
  22. package/android/src/test/java/com/hotupdater/NoBackupContext.kt +11 -0
  23. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +309 -23
  24. package/android/src/test/java/com/hotupdater/PreviousProcessExitTest.kt +57 -0
  25. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +238 -157
  26. package/ios/HotUpdater/Internal/BundleMetadata.swift +3 -53
  27. package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
  28. package/ios/HotUpdater/Internal/HotUpdater.mm +8 -18
  29. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +48 -29
  30. package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
  31. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +108 -1
  32. package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
  33. package/lib/commonjs/appReady.js +111 -0
  34. package/lib/commonjs/appReady.js.map +1 -0
  35. package/lib/commonjs/appReady.spec.js +360 -0
  36. package/lib/commonjs/appReady.spec.js.map +1 -0
  37. package/lib/commonjs/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
  38. package/lib/commonjs/appReady.test-utils.js.map +1 -0
  39. package/lib/commonjs/baseURL.js +16 -0
  40. package/lib/commonjs/baseURL.js.map +1 -0
  41. package/lib/commonjs/checkForUpdate.js +104 -56
  42. package/lib/commonjs/checkForUpdate.js.map +1 -1
  43. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +337 -74
  44. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  45. package/lib/commonjs/clientPlugin.js +49 -0
  46. package/lib/commonjs/clientPlugin.js.map +1 -0
  47. package/lib/commonjs/entryImportGraph.spec.js +50 -0
  48. package/lib/commonjs/entryImportGraph.spec.js.map +1 -0
  49. package/lib/commonjs/fetchJSON.js +2 -6
  50. package/lib/commonjs/fetchJSON.js.map +1 -1
  51. package/lib/commonjs/httpClient.js +15 -67
  52. package/lib/commonjs/httpClient.js.map +1 -1
  53. package/lib/commonjs/httpClient.spec.js +26 -9
  54. package/lib/commonjs/httpClient.spec.js.map +1 -1
  55. package/lib/commonjs/index.js +49 -25
  56. package/lib/commonjs/index.js.map +1 -1
  57. package/lib/commonjs/index.spec.js +126 -40
  58. package/lib/commonjs/index.spec.js.map +1 -1
  59. package/lib/commonjs/native.js +67 -48
  60. package/lib/commonjs/native.js.map +1 -1
  61. package/lib/commonjs/native.spec.js +80 -58
  62. package/lib/commonjs/native.spec.js.map +1 -1
  63. package/lib/commonjs/pluginHost.js +208 -0
  64. package/lib/commonjs/pluginHost.js.map +1 -0
  65. package/lib/commonjs/pluginHost.spec.js +292 -0
  66. package/lib/commonjs/pluginHost.spec.js.map +1 -0
  67. package/lib/commonjs/plugins/insights/eventId.js +129 -0
  68. package/lib/commonjs/plugins/insights/eventId.js.map +1 -0
  69. package/lib/commonjs/plugins/insights/eventId.spec.js +47 -0
  70. package/lib/commonjs/plugins/insights/eventId.spec.js.map +1 -0
  71. package/lib/commonjs/plugins/insights/index.js +272 -0
  72. package/lib/commonjs/plugins/insights/index.js.map +1 -0
  73. package/lib/commonjs/plugins/insights/insights.spec.js +640 -0
  74. package/lib/commonjs/plugins/insights/insights.spec.js.map +1 -0
  75. package/lib/commonjs/plugins/insights/sender.js +128 -0
  76. package/lib/commonjs/plugins/insights/sender.js.map +1 -0
  77. package/lib/commonjs/plugins/insights/sender.spec.js +265 -0
  78. package/lib/commonjs/plugins/insights/sender.spec.js.map +1 -0
  79. package/lib/commonjs/plugins/insights/state.js +145 -0
  80. package/lib/commonjs/plugins/insights/state.js.map +1 -0
  81. package/lib/commonjs/releaseCatalogCache.js +13 -2
  82. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  83. package/lib/commonjs/releaseCatalogCache.spec.js +18 -0
  84. package/lib/commonjs/releaseCatalogCache.spec.js.map +1 -1
  85. package/lib/commonjs/sdkVersion.js +1 -1
  86. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  87. package/lib/commonjs/updateError.js +132 -0
  88. package/lib/commonjs/updateError.js.map +1 -0
  89. package/lib/commonjs/updateError.spec.js +149 -0
  90. package/lib/commonjs/updateError.spec.js.map +1 -0
  91. package/lib/commonjs/wrap.js +43 -25
  92. package/lib/commonjs/wrap.js.map +1 -1
  93. package/lib/commonjs/wrap.progress.spec.js +195 -0
  94. package/lib/commonjs/wrap.progress.spec.js.map +1 -0
  95. package/lib/commonjs/wrap.spec.js +94 -230
  96. package/lib/commonjs/wrap.spec.js.map +1 -1
  97. package/lib/module/appReady.js +103 -0
  98. package/lib/module/appReady.js.map +1 -0
  99. package/lib/module/appReady.spec.js +359 -0
  100. package/lib/module/appReady.spec.js.map +1 -0
  101. package/lib/module/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
  102. package/lib/module/appReady.test-utils.js.map +1 -0
  103. package/lib/module/baseURL.js +11 -0
  104. package/lib/module/baseURL.js.map +1 -0
  105. package/lib/module/checkForUpdate.js +103 -57
  106. package/lib/module/checkForUpdate.js.map +1 -1
  107. package/lib/module/checkForUpdate.releaseCatalog.spec.js +337 -74
  108. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  109. package/lib/module/clientPlugin.js +44 -0
  110. package/lib/module/clientPlugin.js.map +1 -0
  111. package/lib/module/entryImportGraph.spec.js +50 -0
  112. package/lib/module/entryImportGraph.spec.js.map +1 -0
  113. package/lib/module/fetchJSON.js +2 -6
  114. package/lib/module/fetchJSON.js.map +1 -1
  115. package/lib/module/httpClient.js +14 -66
  116. package/lib/module/httpClient.js.map +1 -1
  117. package/lib/module/httpClient.spec.js +26 -9
  118. package/lib/module/httpClient.spec.js.map +1 -1
  119. package/lib/module/index.js +44 -27
  120. package/lib/module/index.js.map +1 -1
  121. package/lib/module/index.spec.js +127 -41
  122. package/lib/module/index.spec.js.map +1 -1
  123. package/lib/module/native.js +63 -44
  124. package/lib/module/native.js.map +1 -1
  125. package/lib/module/native.spec.js +80 -58
  126. package/lib/module/native.spec.js.map +1 -1
  127. package/lib/module/pluginHost.js +200 -0
  128. package/lib/module/pluginHost.js.map +1 -0
  129. package/lib/module/pluginHost.spec.js +291 -0
  130. package/lib/module/pluginHost.spec.js.map +1 -0
  131. package/lib/module/plugins/insights/eventId.js +122 -0
  132. package/lib/module/plugins/insights/eventId.js.map +1 -0
  133. package/lib/module/plugins/insights/eventId.spec.js +47 -0
  134. package/lib/module/plugins/insights/eventId.spec.js.map +1 -0
  135. package/lib/module/plugins/insights/index.js +268 -0
  136. package/lib/module/plugins/insights/index.js.map +1 -0
  137. package/lib/module/plugins/insights/insights.spec.js +639 -0
  138. package/lib/module/plugins/insights/insights.spec.js.map +1 -0
  139. package/lib/module/plugins/insights/sender.js +123 -0
  140. package/lib/module/plugins/insights/sender.js.map +1 -0
  141. package/lib/module/plugins/insights/sender.spec.js +265 -0
  142. package/lib/module/plugins/insights/sender.spec.js.map +1 -0
  143. package/lib/module/plugins/insights/state.js +139 -0
  144. package/lib/module/plugins/insights/state.js.map +1 -0
  145. package/lib/module/releaseCatalogCache.js +13 -2
  146. package/lib/module/releaseCatalogCache.js.map +1 -1
  147. package/lib/module/releaseCatalogCache.spec.js +18 -0
  148. package/lib/module/releaseCatalogCache.spec.js.map +1 -1
  149. package/lib/module/sdkVersion.js +1 -1
  150. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  151. package/lib/module/updateError.js +126 -0
  152. package/lib/module/updateError.js.map +1 -0
  153. package/lib/module/updateError.spec.js +149 -0
  154. package/lib/module/updateError.spec.js.map +1 -0
  155. package/lib/module/wrap.js +43 -25
  156. package/lib/module/wrap.js.map +1 -1
  157. package/lib/module/wrap.progress.spec.js +196 -0
  158. package/lib/module/wrap.progress.spec.js.map +1 -0
  159. package/lib/module/wrap.spec.js +93 -229
  160. package/lib/module/wrap.spec.js.map +1 -1
  161. package/lib/typescript/commonjs/appReady.d.ts +23 -0
  162. package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
  163. package/lib/typescript/commonjs/appReady.test-utils.d.ts +9 -0
  164. package/lib/typescript/commonjs/appReady.test-utils.d.ts.map +1 -0
  165. package/lib/typescript/commonjs/baseURL.d.ts +4 -0
  166. package/lib/typescript/commonjs/baseURL.d.ts.map +1 -0
  167. package/lib/typescript/commonjs/checkForUpdate.d.ts +15 -2
  168. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/clientPlugin.d.ts +185 -0
  170. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
  171. package/lib/typescript/commonjs/fetchJSON.d.ts +2 -3
  172. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/httpClient.d.ts +2 -30
  174. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  175. package/lib/typescript/commonjs/index.d.ts +3 -6
  176. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  177. package/lib/typescript/commonjs/native.d.ts +29 -15
  178. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  179. package/lib/typescript/commonjs/pluginHost.d.ts +29 -0
  180. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
  181. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts +18 -0
  182. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts.map +1 -0
  183. package/lib/typescript/commonjs/plugins/insights/index.d.ts +40 -0
  184. package/lib/typescript/commonjs/plugins/insights/index.d.ts.map +1 -0
  185. package/lib/typescript/commonjs/plugins/insights/sender.d.ts +74 -0
  186. package/lib/typescript/commonjs/plugins/insights/sender.d.ts.map +1 -0
  187. package/lib/typescript/commonjs/plugins/insights/state.d.ts +46 -0
  188. package/lib/typescript/commonjs/plugins/insights/state.d.ts.map +1 -0
  189. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +5 -1
  190. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  191. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  192. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +34 -12
  193. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  194. package/lib/typescript/commonjs/updateError.d.ts +26 -0
  195. package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
  196. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  197. package/lib/typescript/commonjs/wrap.types.d.ts +6 -5
  198. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  199. package/lib/typescript/module/appReady.d.ts +23 -0
  200. package/lib/typescript/module/appReady.d.ts.map +1 -0
  201. package/lib/typescript/module/appReady.test-utils.d.ts +9 -0
  202. package/lib/typescript/module/appReady.test-utils.d.ts.map +1 -0
  203. package/lib/typescript/module/baseURL.d.ts +4 -0
  204. package/lib/typescript/module/baseURL.d.ts.map +1 -0
  205. package/lib/typescript/module/checkForUpdate.d.ts +15 -2
  206. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  207. package/lib/typescript/module/clientPlugin.d.ts +185 -0
  208. package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
  209. package/lib/typescript/module/fetchJSON.d.ts +2 -3
  210. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  211. package/lib/typescript/module/httpClient.d.ts +2 -30
  212. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  213. package/lib/typescript/module/index.d.ts +3 -6
  214. package/lib/typescript/module/index.d.ts.map +1 -1
  215. package/lib/typescript/module/native.d.ts +29 -15
  216. package/lib/typescript/module/native.d.ts.map +1 -1
  217. package/lib/typescript/module/pluginHost.d.ts +29 -0
  218. package/lib/typescript/module/pluginHost.d.ts.map +1 -0
  219. package/lib/typescript/module/plugins/insights/eventId.d.ts +18 -0
  220. package/lib/typescript/module/plugins/insights/eventId.d.ts.map +1 -0
  221. package/lib/typescript/module/plugins/insights/index.d.ts +40 -0
  222. package/lib/typescript/module/plugins/insights/index.d.ts.map +1 -0
  223. package/lib/typescript/module/plugins/insights/sender.d.ts +74 -0
  224. package/lib/typescript/module/plugins/insights/sender.d.ts.map +1 -0
  225. package/lib/typescript/module/plugins/insights/state.d.ts +46 -0
  226. package/lib/typescript/module/plugins/insights/state.d.ts.map +1 -0
  227. package/lib/typescript/module/releaseCatalogCache.d.ts +5 -1
  228. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  229. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  230. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +34 -12
  231. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  232. package/lib/typescript/module/updateError.d.ts +26 -0
  233. package/lib/typescript/module/updateError.d.ts.map +1 -0
  234. package/lib/typescript/module/wrap.d.ts.map +1 -1
  235. package/lib/typescript/module/wrap.types.d.ts +6 -5
  236. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  237. package/package.json +14 -2
  238. package/plugins/insights/package.json +8 -0
  239. package/src/appReady.spec.ts +393 -0
  240. package/src/{notifyAppReadyInsights.test-utils.ts → appReady.test-utils.ts} +7 -7
  241. package/src/appReady.ts +149 -0
  242. package/src/baseURL.ts +15 -0
  243. package/src/checkForUpdate.releaseCatalog.spec.ts +370 -76
  244. package/src/checkForUpdate.ts +135 -80
  245. package/src/clientPlugin.ts +231 -0
  246. package/src/entryImportGraph.spec.ts +65 -0
  247. package/src/fetchJSON.ts +3 -10
  248. package/src/httpClient.spec.ts +28 -9
  249. package/src/httpClient.ts +16 -109
  250. package/src/index.spec.ts +129 -36
  251. package/src/index.ts +67 -37
  252. package/src/native.spec.ts +98 -72
  253. package/src/native.ts +93 -77
  254. package/src/pluginHost.spec.ts +297 -0
  255. package/src/pluginHost.ts +262 -0
  256. package/src/plugins/insights/eventId.spec.ts +60 -0
  257. package/src/plugins/insights/eventId.ts +171 -0
  258. package/src/plugins/insights/index.ts +367 -0
  259. package/src/plugins/insights/insights.spec.ts +732 -0
  260. package/src/plugins/insights/sender.spec.ts +287 -0
  261. package/src/plugins/insights/sender.ts +221 -0
  262. package/src/plugins/insights/state.ts +184 -0
  263. package/src/releaseCatalogCache.spec.ts +22 -0
  264. package/src/releaseCatalogCache.ts +19 -4
  265. package/src/sdkVersion.ts +1 -1
  266. package/src/specs/NativeHotUpdater.ts +34 -13
  267. package/src/updateError.spec.ts +144 -0
  268. package/src/updateError.ts +153 -0
  269. package/src/wrap.progress.spec.ts +232 -0
  270. package/src/wrap.spec.ts +100 -283
  271. package/src/wrap.tsx +57 -26
  272. package/src/wrap.types.ts +6 -5
  273. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -225
  274. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  275. package/lib/commonjs/notifyAppReadyInsights.js +0 -188
  276. package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
  277. package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -378
  278. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
  279. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
  280. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -124
  281. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
  282. package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -224
  283. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  284. package/lib/module/notifyAppReadyInsights.js +0 -181
  285. package/lib/module/notifyAppReadyInsights.js.map +0 -1
  286. package/lib/module/notifyAppReadyInsights.spec.js +0 -377
  287. package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
  288. package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
  289. package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -123
  290. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
  291. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -25
  292. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
  293. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
  294. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  295. package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -25
  296. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
  297. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
  298. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  299. package/src/notifyAppReadyInsights.boundary.spec.ts +0 -242
  300. package/src/notifyAppReadyInsights.spec.ts +0 -362
  301. package/src/notifyAppReadyInsights.transport.spec.ts +0 -138
  302. 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.SocketTimeoutException
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 : DownloadService {
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
- private const val MAX_RETRIES = 3
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 = 0
164
- var lastException: Exception? = null
165
-
166
- while (attempt < MAX_RETRIES) {
167
- try {
168
- return@withContext attemptDownload(
169
- fileUrl,
170
- destination,
171
- fileSizeCallback,
172
- progressCallback,
173
- )
174
- } catch (e: Exception) {
175
- lastException = e
176
- attempt++
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
- DownloadResult.Error(lastException ?: Exception("Download failed after $MAX_RETRIES attempts"))
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
- override suspend fun downloadFileOnce(
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 errorMsg = "HTTP error ${response.code}: ${response.message}"
235
- Log.d(TAG, errorMsg)
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(Exception(errorMsg))
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
- destination.outputStream().use { output ->
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) ->