@hot-updater/react-native 1.0.0-rc.16 → 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 (316) 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 +18 -165
  52. package/lib/commonjs/httpClient.js.map +1 -1
  53. package/lib/commonjs/httpClient.spec.js +26 -199
  54. package/lib/commonjs/httpClient.spec.js.map +1 -1
  55. package/lib/commonjs/index.js +49 -36
  56. package/lib/commonjs/index.js.map +1 -1
  57. package/lib/commonjs/index.spec.js +108 -123
  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 +18 -165
  116. package/lib/module/httpClient.js.map +1 -1
  117. package/lib/module/httpClient.spec.js +27 -200
  118. package/lib/module/httpClient.spec.js.map +1 -1
  119. package/lib/module/index.js +44 -38
  120. package/lib/module/index.js.map +1 -1
  121. package/lib/module/index.spec.js +108 -123
  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 -36
  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 -10
  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 -36
  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 -10
  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 +30 -227
  249. package/src/httpClient.ts +20 -256
  250. package/src/index.spec.ts +117 -93
  251. package/src/index.ts +67 -52
  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 -12
  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 -171
  281. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
  282. package/lib/commonjs/uuidv7.js +0 -41
  283. package/lib/commonjs/uuidv7.js.map +0 -1
  284. package/lib/commonjs/uuidv7.spec.js +0 -30
  285. package/lib/commonjs/uuidv7.spec.js.map +0 -1
  286. package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -224
  287. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  288. package/lib/module/notifyAppReadyInsights.js +0 -181
  289. package/lib/module/notifyAppReadyInsights.js.map +0 -1
  290. package/lib/module/notifyAppReadyInsights.spec.js +0 -377
  291. package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
  292. package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
  293. package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -170
  294. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
  295. package/lib/module/uuidv7.js +0 -36
  296. package/lib/module/uuidv7.js.map +0 -1
  297. package/lib/module/uuidv7.spec.js +0 -30
  298. package/lib/module/uuidv7.spec.js.map +0 -1
  299. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -25
  300. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
  301. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
  302. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  303. package/lib/typescript/commonjs/uuidv7.d.ts +0 -10
  304. package/lib/typescript/commonjs/uuidv7.d.ts.map +0 -1
  305. package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -25
  306. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
  307. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
  308. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  309. package/lib/typescript/module/uuidv7.d.ts +0 -10
  310. package/lib/typescript/module/uuidv7.d.ts.map +0 -1
  311. package/src/notifyAppReadyInsights.boundary.spec.ts +0 -242
  312. package/src/notifyAppReadyInsights.spec.ts +0 -362
  313. package/src/notifyAppReadyInsights.transport.spec.ts +0 -196
  314. package/src/notifyAppReadyInsights.ts +0 -293
  315. package/src/uuidv7.spec.ts +0 -25
  316. package/src/uuidv7.ts +0 -38
@@ -14,9 +14,8 @@ import kotlinx.coroutines.withContext
14
14
  import org.json.JSONObject
15
15
  import java.io.File
16
16
  import java.io.FileInputStream
17
- import java.io.FileOutputStream
18
17
  import java.net.URL
19
- import java.util.UUID
18
+ import kotlin.coroutines.cancellation.CancellationException
20
19
 
21
20
  data class ChangedAssetDescriptor(
22
21
  val fileUrl: String?,
@@ -56,6 +55,30 @@ data class DiffProgressDetails(
56
55
  val files: List<DiffProgressFileSnapshot> = emptyList(),
57
56
  )
58
57
 
58
+ /**
59
+ * How a staged bundle arrived, the `delivery` of the `updateBundle` result:
60
+ * [PATCH] when a bsdiff patch produced at least one file, [MANIFEST] when only
61
+ * the changed files were downloaded (possibly none), and [ARCHIVE] when the
62
+ * full tar.br archive was downloaded.
63
+ */
64
+ enum class BundleDelivery(
65
+ val value: String,
66
+ ) {
67
+ PATCH("patch"),
68
+ MANIFEST("manifest"),
69
+ ARCHIVE("archive"),
70
+ }
71
+
72
+ /**
73
+ * The `updateBundle` result. [patchFallback] is true when a patch for the
74
+ * running bundle could not produce its file, so the whole file was downloaded
75
+ * instead.
76
+ */
77
+ data class UpdateBundleResult(
78
+ val delivery: BundleDelivery,
79
+ val patchFallback: Boolean,
80
+ )
81
+
59
82
  /**
60
83
  * Interface for bundle storage operations
61
84
  */
@@ -92,6 +115,7 @@ interface BundleStorageService {
92
115
  * @param manifestUrl URL of the target bundle manifest
93
116
  * @param manifestFileHash Manifest hash or signature
94
117
  * @param progressCallback Callback for download progress updates
118
+ * @return how the staged bundle arrived
95
119
  * @throws HotUpdaterException if the update fails
96
120
  */
97
121
  suspend fun updateBundle(
@@ -101,7 +125,7 @@ interface BundleStorageService {
101
125
  assets: Map<String, ChangedAssetDescriptor>,
102
126
  archiveUrl: String? = null,
103
127
  progressCallback: (UpdateProgressPayload) -> Unit,
104
- )
128
+ ): UpdateBundleResult
105
129
 
106
130
  fun stageReleaseSelection(selection: PersistedSelection): Boolean = false
107
131
 
@@ -137,30 +161,6 @@ interface BundleStorageService {
137
161
  */
138
162
  fun notifyAppReady(): Map<String, Any?>
139
163
 
140
- /**
141
- * Returns the stable install ID for this app installation.
142
- */
143
- fun getInstallId(): String
144
-
145
- /**
146
- * Returns the persisted nullable user id for this app installation.
147
- */
148
- fun getUserId(): String?
149
-
150
- /**
151
- * Returns the persisted nullable username for this app installation.
152
- */
153
- fun getUsername(): String?
154
-
155
- /**
156
- * Persists the optional user envelope for insights.
157
- * Passing null clears the stored user identity.
158
- */
159
- fun setUser(
160
- userId: String?,
161
- username: String?,
162
- )
163
-
164
164
  /**
165
165
  * Gets the crashed bundle history
166
166
  * @return CrashedHistory containing crashed bundles
@@ -231,6 +231,10 @@ class BundleFileStorageService(
231
231
  private const val TAG = "BundleStorage"
232
232
  private const val MAX_SAFE_INTEGER = 9_007_199_254_740_991L
233
233
  private val SHA256_PATTERN = Regex("^[0-9a-f]{64}$")
234
+
235
+ /** A patch produced a file that does not match the target, or none at all. */
236
+ private val PATCH_FAILURE =
237
+ UpdateFailure(UpdateFailureStage.INSTALL, UpdateFailureReason.PATCH, resource = UpdateFailureResource.PATCH)
234
238
  }
235
239
 
236
240
  private val releaseStateLock = Any()
@@ -395,6 +399,10 @@ class BundleFileStorageService(
395
399
  return File(compressedDir, "$safeName.$compression")
396
400
  }
397
401
 
402
+ /**
403
+ * Decompresses a downloaded `.br` asset. A decoding failure is reported as
404
+ * an extract failure and a failure writing the output as a storage failure.
405
+ */
398
406
  private fun decompressBrotliFile(
399
407
  sourceFile: File,
400
408
  targetFile: File,
@@ -409,7 +417,7 @@ class BundleFileStorageService(
409
417
  try {
410
418
  FileInputStream(sourceFile).use { fileInputStream ->
411
419
  BrotliInputStream(fileInputStream).use { brotliInputStream ->
412
- FileOutputStream(tempOutputFile).use { outputStream ->
420
+ LocalStorageOutputStream.open(tempOutputFile).use { outputStream ->
413
421
  brotliInputStream.copyTo(outputStream)
414
422
  }
415
423
  }
@@ -419,15 +427,30 @@ class BundleFileStorageService(
419
427
  targetFile.delete()
420
428
  }
421
429
  if (!tempOutputFile.renameTo(targetFile)) {
422
- tempOutputFile.copyTo(targetFile, overwrite = true)
430
+ storageOperation { tempOutputFile.copyTo(targetFile, overwrite = true) }
423
431
  tempOutputFile.delete()
424
432
  }
433
+ } catch (error: Exception) {
434
+ tempOutputFile.delete()
435
+ throw HotUpdaterException.downloadFailed(
436
+ error,
437
+ if (error is LocalStorageException) {
438
+ UpdateFailure.install(UpdateFailureReason.STORAGE)
439
+ } else {
440
+ UpdateFailure.install(UpdateFailureReason.EXTRACT)
441
+ },
442
+ )
425
443
  } catch (error: Throwable) {
426
444
  tempOutputFile.delete()
427
445
  throw error
428
446
  }
429
447
  }
430
448
 
449
+ /**
450
+ * Tries to produce [targetFile] from a bsdiff patch against the running
451
+ * bundle. A patch for another base bundle or algorithm is not attempted.
452
+ * Once attempted, a failure leaves the file to be downloaded instead.
453
+ */
431
454
  private suspend fun applyPatchAssetIfPossible(
432
455
  assetPath: String,
433
456
  changedAsset: ChangedAssetDescriptor,
@@ -438,81 +461,97 @@ class BundleFileStorageService(
438
461
  tempDir: File,
439
462
  diffFiles: MutableList<DiffProgressFileSnapshot>,
440
463
  progressCallback: (UpdateProgressPayload) -> Unit,
441
- ): Boolean {
442
- val patch = changedAsset.patch ?: return false
464
+ ): PatchOutcome {
465
+ val patch = changedAsset.patch ?: return PatchOutcome.NOT_ATTEMPTED
443
466
  if (patch.algorithm != "bsdiff" || currentBundleId != patch.baseBundleId) {
444
- return false
467
+ return PatchOutcome.NOT_ATTEMPTED
445
468
  }
446
469
 
447
- val sourceDir = activeBundleDir ?: return false
448
- val sourceFile = RelativePathResolver.resolveInside(sourceDir, assetPath) ?: return false
470
+ // The patch targets the running bundle, so from here a miss is a fallback.
471
+ val sourceDir = activeBundleDir ?: return PatchOutcome.FAILED
472
+ val sourceFile = RelativePathResolver.resolveInside(sourceDir, assetPath) ?: return PatchOutcome.FAILED
449
473
  if (!sourceFile.exists() || !HashUtils.verifyHash(sourceFile, patch.baseFileHash)) {
450
- return false
474
+ return PatchOutcome.FAILED
451
475
  }
452
476
 
453
477
  val patchFile = patchTempFile(tempDir, assetPath)
454
478
 
455
- return try {
456
- when (
457
- val patchDownloadResult =
458
- downloadService.downloadFile(
459
- URL(patch.patchUrl),
460
- patchFile,
461
- ) { downloadProgress ->
462
- updateDiffProgressFile(
463
- files = diffFiles,
464
- assetPath = assetPath,
465
- status = "downloading",
466
- progress = downloadProgress.progress,
467
- downloadPath = patchDownloadPath(assetPath),
468
- downloadedBytes = downloadProgress.downloadedBytes,
469
- totalBytes = downloadProgress.totalBytes,
470
- )
471
- emitDiffProgress(
472
- progressCallback = progressCallback,
473
- phase = "downloading",
474
- files = diffFiles,
475
- )
479
+ val patched =
480
+ try {
481
+ when (
482
+ val patchDownloadResult =
483
+ downloadService.downloadFile(
484
+ URL(patch.patchUrl),
485
+ patchFile,
486
+ ) { downloadProgress ->
487
+ updateDiffProgressFile(
488
+ files = diffFiles,
489
+ assetPath = assetPath,
490
+ status = "downloading",
491
+ progress = downloadProgress.progress,
492
+ downloadPath = patchDownloadPath(assetPath),
493
+ downloadedBytes = downloadProgress.downloadedBytes,
494
+ totalBytes = downloadProgress.totalBytes,
495
+ )
496
+ emitDiffProgress(
497
+ progressCallback = progressCallback,
498
+ phase = "downloading",
499
+ files = diffFiles,
500
+ )
501
+ }
502
+ ) {
503
+ is DownloadResult.Error -> {
504
+ false
476
505
  }
477
- ) {
478
- is DownloadResult.Error -> {
479
- false
480
- }
481
506
 
482
- is DownloadResult.Success -> {
483
- if (!HashUtils.verifyHash(patchDownloadResult.file, patch.patchFileHash)) {
484
- false
485
- } else {
486
- withContext(Dispatchers.IO) {
487
- BsdiffPatch.apply(sourceFile, patchDownloadResult.file, targetFile)
488
- }
489
- HashUtils.verifyHash(targetFile, expectedHash).also { patched ->
490
- if (patched) {
491
- Log.d(
492
- TAG,
493
- "HotUpdaterBsdiffPatchApplied asset=$assetPath baseBundleId=${patch.baseBundleId}",
494
- )
507
+ is DownloadResult.Success -> {
508
+ if (!HashUtils.verifyHash(patchDownloadResult.file, patch.patchFileHash)) {
509
+ false
510
+ } else {
511
+ withContext(Dispatchers.IO) {
512
+ BsdiffPatch.apply(sourceFile, patchDownloadResult.file, targetFile)
513
+ }
514
+ HashUtils.verifyHash(targetFile, expectedHash).also { matches ->
515
+ if (matches) {
516
+ Log.d(
517
+ TAG,
518
+ "HotUpdaterBsdiffPatchApplied asset=$assetPath baseBundleId=${patch.baseBundleId}",
519
+ )
520
+ }
495
521
  }
496
522
  }
497
523
  }
498
524
  }
525
+ } catch (_: Exception) {
526
+ false
527
+ } finally {
528
+ patchFile.delete()
529
+ if (!targetFile.exists() || !HashUtils.verifyHash(targetFile, expectedHash)) {
530
+ targetFile.delete()
531
+ }
499
532
  }
500
- } catch (_: Exception) {
501
- false
502
- } finally {
503
- patchFile.delete()
504
- if (!targetFile.exists() || !HashUtils.verifyHash(targetFile, expectedHash)) {
505
- targetFile.delete()
506
- }
507
- }.also { patched ->
508
- if (!patched) {
509
- resetDiffProgressFile(
510
- files = diffFiles,
511
- assetPath = assetPath,
512
- progressCallback = progressCallback,
513
- )
514
- }
533
+ if (!patched) {
534
+ resetDiffProgressFile(
535
+ files = diffFiles,
536
+ assetPath = assetPath,
537
+ progressCallback = progressCallback,
538
+ )
539
+ return PatchOutcome.FAILED
515
540
  }
541
+ return PatchOutcome.APPLIED
542
+ }
543
+
544
+ private enum class PatchOutcome {
545
+ NOT_ATTEMPTED,
546
+ APPLIED,
547
+ FAILED,
548
+ }
549
+
550
+ /** How one file arrived under the per-file plan. */
551
+ private enum class FileDelivery {
552
+ PATCHED,
553
+ DOWNLOADED,
554
+ DOWNLOADED_AFTER_PATCH_FAILURE,
516
555
  }
517
556
 
518
557
  private data class ParsedBundleManifest(
@@ -553,10 +592,6 @@ class BundleFileStorageService(
553
592
  private var hasPreparedLaunch = false
554
593
  private var currentLaunchReport: LaunchReport? = null
555
594
 
556
- @Volatile
557
- private var currentInstallationIdentity: InstallationIdentity? = null
558
- private val installationIdentityLock = Any()
559
-
560
595
  @Volatile
561
596
  private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot? = null
562
597
  private val activeBundleMetadataLock = Any()
@@ -574,8 +609,6 @@ class BundleFileStorageService(
574
609
 
575
610
  private fun getLaunchReportFile(): File = File(getBundleStoreDir(), LaunchReport.LAUNCH_REPORT_FILENAME)
576
611
 
577
- private fun getInstallationIdentityFile(): File = File(getBundleStoreDir(), InstallationIdentity.IDENTITY_FILENAME)
578
-
579
612
  // MARK: - Metadata Operations
580
613
 
581
614
  private fun loadMetadataOrNull(): BundleMetadata? = BundleMetadata.loadFromFile(getMetadataFile(), isolationKey)
@@ -613,25 +646,6 @@ class BundleFileStorageService(
613
646
  "appVersion"
614
647
  }
615
648
 
616
- private fun loadInstallationIdentity(): InstallationIdentity? =
617
- currentInstallationIdentity ?: InstallationIdentity.loadFromFile(getInstallationIdentityFile())?.also {
618
- currentInstallationIdentity = it
619
- }
620
-
621
- private fun saveInstallationIdentity(identity: InstallationIdentity): Boolean {
622
- currentInstallationIdentity = identity
623
- return identity.saveToFile(getInstallationIdentityFile())
624
- }
625
-
626
- private fun getOrCreateInstallationIdentity(): InstallationIdentity =
627
- synchronized(installationIdentityLock) {
628
- loadInstallationIdentity()?.let { return@synchronized it }
629
-
630
- InstallationIdentity(installId = UUID.randomUUID().toString()).also {
631
- saveInstallationIdentity(it)
632
- }
633
- }
634
-
635
649
  private fun getKnownLaunchBundleId(metadata: BundleMetadata): String =
636
650
  getCurrentVerifiedBundleId(metadata) ?: HotUpdaterImpl.getMinBundleId()
637
651
 
@@ -949,14 +963,27 @@ class BundleFileStorageService(
949
963
  SignatureVerifier.verifyHashSignature(context, asset.fileHash, signature)
950
964
  }
951
965
 
966
+ /**
967
+ * @param hashMismatch how a hash mismatch is reported: a downloaded file
968
+ * that does not match is a hash mismatch, while a patched or extracted file
969
+ * that does not match is a patch or extract failure.
970
+ */
952
971
  private fun verifyManifestAssetFileOrThrow(
953
972
  file: File,
954
973
  asset: ParsedManifestAsset,
974
+ hashMismatch: UpdateFailure = UpdateFailure.download(UpdateFailureReason.HASH_MISMATCH),
955
975
  ) {
956
976
  try {
957
977
  verifyManifestAssetFile(file, asset)
958
978
  } catch (e: SignatureVerificationException) {
959
- throw HotUpdaterException.signatureVerificationFailed(e)
979
+ throw HotUpdaterException.signatureVerificationFailed(
980
+ e,
981
+ if (e is SignatureVerificationException.FileHashMismatch) {
982
+ hashMismatch
983
+ } else {
984
+ UpdateFailure.ofVerificationError(e)
985
+ },
986
+ )
960
987
  }
961
988
  }
962
989
 
@@ -1033,7 +1060,7 @@ class BundleFileStorageService(
1033
1060
  getCurrentVerifiedBundleId(metadata)?.takeIf { it != bundleId }
1034
1061
  val incomingSelection = pendingInstallSelections.remove(bundleId)
1035
1062
  if (incomingSelection != null && !isSelectionCurrent(incomingSelection)) {
1036
- throw IllegalStateException("Release catalog selection is stale")
1063
+ throw StaleReleaseSelectionException()
1037
1064
  }
1038
1065
  val currentVerifiedSelection =
1039
1066
  when {
@@ -1406,25 +1433,6 @@ class BundleFileStorageService(
1406
1433
  }
1407
1434
  }
1408
1435
 
1409
- override fun getInstallId(): String = getOrCreateInstallationIdentity().installId
1410
-
1411
- override fun getUserId(): String? = getOrCreateInstallationIdentity().userId
1412
-
1413
- override fun getUsername(): String? = getOrCreateInstallationIdentity().username
1414
-
1415
- override fun setUser(
1416
- userId: String?,
1417
- username: String?,
1418
- ) = synchronized(installationIdentityLock) {
1419
- saveInstallationIdentity(
1420
- getOrCreateInstallationIdentity().copy(
1421
- userId = userId?.trim()?.takeIf { it.isNotEmpty() },
1422
- username = username?.trim()?.takeIf { it.isNotEmpty() },
1423
- ),
1424
- )
1425
- Unit
1426
- }
1427
-
1428
1436
  // MARK: - Bundle URL Operations
1429
1437
 
1430
1438
  override fun setBundleURL(localPath: String?): Boolean {
@@ -1500,16 +1508,17 @@ class BundleFileStorageService(
1500
1508
  assets: Map<String, ChangedAssetDescriptor>,
1501
1509
  archiveUrl: String?,
1502
1510
  progressCallback: (UpdateProgressPayload) -> Unit,
1503
- ) = updateMutex.withLock {
1504
- updateBundleSerialized(
1505
- bundleId = bundleId,
1506
- manifestUrl = manifestUrl,
1507
- manifestFileHash = manifestFileHash,
1508
- changedAssets = assets,
1509
- archiveUrl = archiveUrl,
1510
- progressCallback = progressCallback,
1511
- )
1512
- }
1511
+ ): UpdateBundleResult =
1512
+ updateMutex.withLock {
1513
+ updateBundleSerialized(
1514
+ bundleId = bundleId,
1515
+ manifestUrl = manifestUrl,
1516
+ manifestFileHash = manifestFileHash,
1517
+ changedAssets = assets,
1518
+ archiveUrl = archiveUrl,
1519
+ progressCallback = progressCallback,
1520
+ )
1521
+ }
1513
1522
 
1514
1523
  private suspend fun updateBundleSerialized(
1515
1524
  bundleId: String,
@@ -1518,7 +1527,7 @@ class BundleFileStorageService(
1518
1527
  changedAssets: Map<String, ChangedAssetDescriptor>,
1519
1528
  archiveUrl: String?,
1520
1529
  progressCallback: (UpdateProgressPayload) -> Unit,
1521
- ) {
1530
+ ): UpdateBundleResult {
1522
1531
  Log.d(
1523
1532
  TAG,
1524
1533
  "updateBundle bundleId $bundleId manifestUrl $manifestUrl",
@@ -1531,15 +1540,17 @@ class BundleFileStorageService(
1531
1540
  }
1532
1541
 
1533
1542
  val bundleStoreDir = getBundleStoreDir()
1534
- if (!bundleStoreDir.exists()) {
1535
- bundleStoreDir.mkdirs()
1536
- }
1537
- if (!recoverInterruptedPromotions()) {
1538
- throw HotUpdaterException.moveOperationFailed()
1543
+ updateStep(UpdateFailureStage.INSTALL, resource = null) {
1544
+ if (!bundleStoreDir.exists()) {
1545
+ bundleStoreDir.mkdirs()
1546
+ }
1547
+ if (!recoverInterruptedPromotions()) {
1548
+ throw HotUpdaterException.moveOperationFailed()
1549
+ }
1539
1550
  }
1540
1551
 
1541
1552
  val finalBundleDir = File(bundleStoreDir, bundleId)
1542
- withContext(Dispatchers.IO) {
1553
+ return withContext(Dispatchers.IO) {
1543
1554
  updateBundleFromManifest(
1544
1555
  bundleId = bundleId,
1545
1556
  manifestUrl = manifestUrl,
@@ -1553,6 +1564,60 @@ class BundleFileStorageService(
1553
1564
  }
1554
1565
  }
1555
1566
 
1567
+ /**
1568
+ * Runs one step of the update pipeline and gives its failure a
1569
+ * classification for JS: [resource] is what the step fetches or applies,
1570
+ * and an error nothing anticipated is reported as unknown in [stage].
1571
+ */
1572
+ private inline fun <T> updateStep(
1573
+ stage: UpdateFailureStage,
1574
+ resource: UpdateFailureResource?,
1575
+ block: () -> T,
1576
+ ): T =
1577
+ try {
1578
+ block()
1579
+ } catch (e: Exception) {
1580
+ throw classifyUpdateFailure(e, stage, resource)
1581
+ }
1582
+
1583
+ /**
1584
+ * Gives a failure of the update pipeline its classification for JS.
1585
+ * Failures that are not update failures pass through unchanged, and an
1586
+ * already classified failure only gains a [resource] it did not name.
1587
+ */
1588
+ private fun classifyUpdateFailure(
1589
+ error: Exception,
1590
+ stage: UpdateFailureStage,
1591
+ resource: UpdateFailureResource?,
1592
+ ): Exception =
1593
+ when (error) {
1594
+ is StaleReleaseSelectionException, is CancellationException -> {
1595
+ error
1596
+ }
1597
+
1598
+ is HotUpdaterException -> {
1599
+ error.withResource(resource)
1600
+ }
1601
+
1602
+ else -> {
1603
+ HotUpdaterException.unexpected(
1604
+ error,
1605
+ UpdateFailure.ofUnexpectedError(error, stage).copy(resource = resource),
1606
+ )
1607
+ }
1608
+ }
1609
+
1610
+ /** The rejection for a download that returned [error]. */
1611
+ private fun downloadError(error: Exception): HotUpdaterException =
1612
+ if (error is IncompleteDownloadException) {
1613
+ HotUpdaterException.incompleteDownload(error.expectedSize, error.actualSize)
1614
+ } else {
1615
+ HotUpdaterException.downloadFailed(
1616
+ error,
1617
+ UpdateFailure.ofDownloadError(error) { NetworkState.hasNoActiveNetwork(context) },
1618
+ )
1619
+ }
1620
+
1556
1621
  private suspend fun downloadManifestAsset(
1557
1622
  assetPath: String,
1558
1623
  expectedAsset: ParsedManifestAsset,
@@ -1563,10 +1628,10 @@ class BundleFileStorageService(
1563
1628
  tempDir: File,
1564
1629
  progressFile: DiffProgressFileSnapshot,
1565
1630
  progressCallback: (UpdateProgressPayload) -> Unit,
1566
- ) {
1631
+ ): FileDelivery {
1567
1632
  val expectedHash = expectedAsset.fileHash
1568
1633
  val diffFiles = mutableListOf(progressFile)
1569
- val patched =
1634
+ val patchOutcome =
1570
1635
  applyPatchAssetIfPossible(
1571
1636
  assetPath = assetPath,
1572
1637
  changedAsset = changedAsset,
@@ -1578,8 +1643,12 @@ class BundleFileStorageService(
1578
1643
  diffFiles = diffFiles,
1579
1644
  progressCallback = progressCallback,
1580
1645
  )
1581
- if (patched) {
1582
- verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
1646
+ if (patchOutcome == PatchOutcome.APPLIED) {
1647
+ verifyManifestAssetFileOrThrow(
1648
+ targetFile,
1649
+ expectedAsset,
1650
+ hashMismatch = PATCH_FAILURE,
1651
+ )
1583
1652
  updateDiffProgressFile(
1584
1653
  files = diffFiles,
1585
1654
  assetPath = assetPath,
@@ -1596,7 +1665,7 @@ class BundleFileStorageService(
1596
1665
  },
1597
1666
  files = diffFiles,
1598
1667
  )
1599
- return
1668
+ return FileDelivery.PATCHED
1600
1669
  }
1601
1670
 
1602
1671
  val changedAssetFileUrl =
@@ -1615,6 +1684,7 @@ class BundleFileStorageService(
1615
1684
  )
1616
1685
  throw HotUpdaterException.downloadFailed(
1617
1686
  IllegalStateException("Changed asset fileUrl missing and patch could not be applied: $assetPath"),
1687
+ PATCH_FAILURE,
1618
1688
  )
1619
1689
  }
1620
1690
 
@@ -1663,15 +1733,7 @@ class BundleFileStorageService(
1663
1733
  phase = "downloading",
1664
1734
  files = diffFiles,
1665
1735
  )
1666
- if (assetDownloadResult.exception is IncompleteDownloadException) {
1667
- val incompleteEx =
1668
- assetDownloadResult.exception as IncompleteDownloadException
1669
- throw HotUpdaterException.incompleteDownload(
1670
- incompleteEx.expectedSize,
1671
- incompleteEx.actualSize,
1672
- )
1673
- }
1674
- throw HotUpdaterException.downloadFailed(assetDownloadResult.exception)
1736
+ throw downloadError(assetDownloadResult.exception)
1675
1737
  }
1676
1738
 
1677
1739
  is DownloadResult.Success -> {
@@ -1701,7 +1763,11 @@ class BundleFileStorageService(
1701
1763
  if (e is HotUpdaterException) {
1702
1764
  throw e
1703
1765
  }
1704
- throw HotUpdaterException.downloadFailed(e)
1766
+ // The bytes arrived; what failed is reading them back.
1767
+ throw HotUpdaterException.downloadFailed(
1768
+ e,
1769
+ UpdateFailure.ofUnexpectedError(e, UpdateFailureStage.DOWNLOAD),
1770
+ )
1705
1771
  } finally {
1706
1772
  if (changedAsset.fileCompression == "br") {
1707
1773
  assetDownloadResult.file.delete()
@@ -1720,6 +1786,11 @@ class BundleFileStorageService(
1720
1786
  )
1721
1787
  }
1722
1788
  }
1789
+ return if (patchOutcome == PatchOutcome.FAILED) {
1790
+ FileDelivery.DOWNLOADED_AFTER_PATCH_FAILURE
1791
+ } else {
1792
+ FileDelivery.DOWNLOADED
1793
+ }
1723
1794
  }
1724
1795
 
1725
1796
  private fun pruneStagingDirectory(
@@ -1730,11 +1801,15 @@ class BundleFileStorageService(
1730
1801
  directory.listFiles()?.forEach { file ->
1731
1802
  val relativePath = prefix + file.name
1732
1803
  if (file.canonicalFile != File(directory.canonicalFile, file.name)) {
1733
- check(file.delete()) { "Cannot remove staging link: $relativePath" }
1804
+ if (!file.delete()) {
1805
+ throw HotUpdaterException.storageFailed("Cannot remove staging link: $relativePath")
1806
+ }
1734
1807
  } else if (file.isDirectory && assets.any { it.startsWith("$relativePath/") }) {
1735
1808
  pruneStagingDirectory(file, assets, "$relativePath/")
1736
1809
  } else if (!file.isFile || relativePath !in assets) {
1737
- check(file.deleteRecursively()) { "Cannot remove stale staging file: $relativePath" }
1810
+ if (!file.deleteRecursively()) {
1811
+ throw HotUpdaterException.storageFailed("Cannot remove stale staging file: $relativePath")
1812
+ }
1738
1813
  }
1739
1814
  }
1740
1815
  }
@@ -1846,7 +1921,11 @@ class BundleFileStorageService(
1846
1921
  val extractedFile =
1847
1922
  RelativePathResolver.resolveInside(extractedDir, assetPath)
1848
1923
  ?: throw IllegalStateException("Archive output path is invalid")
1849
- verifyManifestAssetFileOrThrow(extractedFile, expectedAsset)
1924
+ verifyManifestAssetFileOrThrow(
1925
+ extractedFile,
1926
+ expectedAsset,
1927
+ hashMismatch = UpdateFailure.install(UpdateFailureReason.EXTRACT),
1928
+ )
1850
1929
  }
1851
1930
 
1852
1931
  if (!tmpDir.renameTo(localBackupDir)) {
@@ -1874,7 +1953,8 @@ class BundleFileStorageService(
1874
1953
  tmpDir.exists() ||
1875
1954
  (localBackupDir.exists() && localBackupDir.renameTo(tmpDir))
1876
1955
  if (!canUseIndividualPlan) {
1877
- throw error
1956
+ // Only the staging directory swap can leave tmpDir missing.
1957
+ throw HotUpdaterException.unexpected(error, UpdateFailure.install(UpdateFailureReason.STORAGE))
1878
1958
  }
1879
1959
  extractedDir.deleteRecursively()
1880
1960
  null
@@ -1894,7 +1974,7 @@ class BundleFileStorageService(
1894
1974
  bundleStoreDir: File,
1895
1975
  finalBundleDir: File,
1896
1976
  progressCallback: (UpdateProgressPayload) -> Unit,
1897
- ) {
1977
+ ): UpdateBundleResult {
1898
1978
  val activeBundleDir = getActiveBundleDir()
1899
1979
  val currentBundleId = getBundleId()
1900
1980
  val baseDir =
@@ -1911,257 +1991,280 @@ class BundleFileStorageService(
1911
1991
 
1912
1992
  try {
1913
1993
  val manifestFile = File(tempDir, "manifest.json")
1914
- emitDiffProgress(
1915
- progressCallback = progressCallback,
1916
- phase = "manifest",
1917
- files = diffFiles,
1918
- manifestProgress = 0.0,
1919
- )
1920
- when (
1921
- val manifestDownloadResult =
1922
- downloadService.downloadFile(
1923
- URL(manifestUrl),
1924
- manifestFile,
1925
- ) { downloadProgress ->
1926
- emitDiffProgress(
1927
- progressCallback = progressCallback,
1928
- phase = "manifest",
1929
- files = diffFiles,
1930
- manifestProgress = downloadProgress.progress,
1931
- )
1932
- }
1933
- ) {
1934
- is DownloadResult.Error -> {
1935
- if (manifestDownloadResult.exception is IncompleteDownloadException) {
1936
- val incompleteEx =
1937
- manifestDownloadResult.exception as IncompleteDownloadException
1938
- throw HotUpdaterException.incompleteDownload(
1939
- incompleteEx.expectedSize,
1940
- incompleteEx.actualSize,
1941
- )
1942
- }
1943
- throw HotUpdaterException.downloadFailed(manifestDownloadResult.exception)
1944
- }
1994
+ val targetManifest =
1995
+ updateStep(UpdateFailureStage.DOWNLOAD, UpdateFailureResource.MANIFEST) {
1996
+ emitDiffProgress(
1997
+ progressCallback = progressCallback,
1998
+ phase = "manifest",
1999
+ files = diffFiles,
2000
+ manifestProgress = 0.0,
2001
+ )
2002
+ when (
2003
+ val manifestDownloadResult =
2004
+ downloadService.downloadFile(
2005
+ URL(manifestUrl),
2006
+ manifestFile,
2007
+ ) { downloadProgress ->
2008
+ emitDiffProgress(
2009
+ progressCallback = progressCallback,
2010
+ phase = "manifest",
2011
+ files = diffFiles,
2012
+ manifestProgress = downloadProgress.progress,
2013
+ )
2014
+ }
2015
+ ) {
2016
+ is DownloadResult.Error -> {
2017
+ throw downloadError(manifestDownloadResult.exception)
2018
+ }
1945
2019
 
1946
- is DownloadResult.Success -> {
1947
- Unit
1948
- }
1949
- }
2020
+ is DownloadResult.Success -> {
2021
+ Unit
2022
+ }
2023
+ }
1950
2024
 
1951
- try {
1952
- SignatureVerifier.verifyBundle(context, manifestFile, manifestFileHash)
1953
- } catch (e: SignatureVerificationException) {
1954
- throw HotUpdaterException.signatureVerificationFailed(e)
1955
- }
2025
+ try {
2026
+ SignatureVerifier.verifyBundle(context, manifestFile, manifestFileHash)
2027
+ } catch (e: SignatureVerificationException) {
2028
+ throw HotUpdaterException.signatureVerificationFailed(e)
2029
+ }
1956
2030
 
1957
- val targetManifest = parseBundleManifestFromFile(manifestFile) ?: throw HotUpdaterException.invalidBundle()
1958
- if (targetManifest.bundleId != bundleId) {
1959
- throw HotUpdaterException.invalidBundle()
1960
- }
1961
- // Reuse never bypasses the complete artifact contract.
1962
- if (targetManifest.assets.keys != changedAssets.keys) throw HotUpdaterException.invalidBundle()
1963
- targetManifest.assets.forEach { (path, asset) ->
1964
- val descriptor = changedAssets.getValue(path)
1965
- if (RelativePathResolver.normalizeRelativePath(path) != path ||
1966
- path == "manifest.json" ||
1967
- descriptor.fileUrl.isNullOrBlank() ||
1968
- !descriptor.fileHash.equals(asset.fileHash, ignoreCase = true) ||
1969
- RelativePathResolver.resolveInside(tmpDir, path) == null
1970
- ) {
1971
- throw HotUpdaterException.invalidBundle()
2031
+ val manifest = parseBundleManifestFromFile(manifestFile) ?: throw HotUpdaterException.invalidBundle()
2032
+ if (manifest.bundleId != bundleId) {
2033
+ throw HotUpdaterException.invalidBundle()
2034
+ }
2035
+ // Reuse never bypasses the complete artifact contract.
2036
+ if (manifest.assets.keys != changedAssets.keys) throw HotUpdaterException.invalidBundle()
2037
+ manifest.assets.forEach { (path, asset) ->
2038
+ val descriptor = changedAssets.getValue(path)
2039
+ if (RelativePathResolver.normalizeRelativePath(path) != path ||
2040
+ path == "manifest.json" ||
2041
+ descriptor.fileUrl.isNullOrBlank() ||
2042
+ !descriptor.fileHash.equals(asset.fileHash, ignoreCase = true) ||
2043
+ RelativePathResolver.resolveInside(tmpDir, path) == null
2044
+ ) {
2045
+ throw HotUpdaterException.invalidBundle()
2046
+ }
2047
+ }
2048
+ manifest
1972
2049
  }
2050
+ updateStep(UpdateFailureStage.INSTALL, resource = null) {
2051
+ if (!tmpDir.isDirectory && !tmpDir.mkdirs()) throw HotUpdaterException.directoryCreationFailed()
2052
+ pruneStagingDirectory(tmpDir, targetManifest.assets.keys)
1973
2053
  }
1974
- if (!tmpDir.isDirectory && !tmpDir.mkdirs()) throw HotUpdaterException.directoryCreationFailed()
1975
- pruneStagingDirectory(tmpDir, targetManifest.assets.keys)
1976
- val targetEntries = targetManifest.assets.entries.sortedBy { it.key }
1977
- val localDirectories = listOfNotNull(activeBundleDir, finalBundleDir).distinct().filter { it.isDirectory }
1978
- val downloads = mutableMapOf<String, ChangedAssetDescriptor>()
1979
- targetEntries.forEachIndexed { index, (assetPath, expectedAsset) ->
1980
- progressCallback(
1981
- UpdateProgressPayload(
1982
- progress = 0.15 + 0.05 * index / targetEntries.size.coerceAtLeast(1),
1983
- artifactType = "diff",
1984
- details = DiffProgressDetails(totalFilesCount = 0, completedFilesCount = 0),
1985
- ),
1986
- )
1987
- val targetFile = RelativePathResolver.resolveInside(tmpDir, assetPath) ?: throw HotUpdaterException.invalidBundle()
1988
- targetFile.parentFile?.mkdirs()
1989
- if (targetFile.isFile && runCatching { verifyManifestAssetFileOrThrow(targetFile, expectedAsset) }.isSuccess) {
1990
- return@forEachIndexed
1991
- }
1992
- if (targetFile.exists()) targetFile.delete()
1993
- var reused = false
1994
- for (directory in localDirectories) {
1995
- val sourceFile = RelativePathResolver.resolveInside(directory, assetPath) ?: continue
1996
- if (!sourceFile.isFile || runCatching { verifyManifestAssetFileOrThrow(sourceFile, expectedAsset) }.isFailure) continue
1997
- if (runCatching {
1998
- copyBundleFile(sourceFile, targetFile)
2054
+ val downloads =
2055
+ updateStep(UpdateFailureStage.INSTALL, UpdateFailureResource.FILE) {
2056
+ val targetEntries = targetManifest.assets.entries.sortedBy { it.key }
2057
+ val localDirectories = listOfNotNull(activeBundleDir, finalBundleDir).distinct().filter { it.isDirectory }
2058
+ val downloads = mutableMapOf<String, ChangedAssetDescriptor>()
2059
+ targetEntries.forEachIndexed { index, (assetPath, expectedAsset) ->
2060
+ progressCallback(
2061
+ UpdateProgressPayload(
2062
+ progress = 0.15 + 0.05 * index / targetEntries.size.coerceAtLeast(1),
2063
+ artifactType = "diff",
2064
+ details = DiffProgressDetails(totalFilesCount = 0, completedFilesCount = 0),
2065
+ ),
2066
+ )
2067
+ val targetFile = RelativePathResolver.resolveInside(tmpDir, assetPath) ?: throw HotUpdaterException.invalidBundle()
2068
+ targetFile.parentFile?.mkdirs()
2069
+ if (targetFile.isFile && runCatching { verifyManifestAssetFileOrThrow(targetFile, expectedAsset) }.isSuccess) {
2070
+ return@forEachIndexed
2071
+ }
2072
+ if (targetFile.exists()) targetFile.delete()
2073
+ var reused = false
2074
+ for (directory in localDirectories) {
2075
+ val sourceFile = RelativePathResolver.resolveInside(directory, assetPath) ?: continue
2076
+ val sourceMatches =
2077
+ sourceFile.isFile && runCatching { verifyManifestAssetFileOrThrow(sourceFile, expectedAsset) }.isSuccess
2078
+ if (!sourceMatches) continue
2079
+ if (runCatching {
2080
+ copyBundleFile(sourceFile, targetFile)
2081
+ verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
2082
+ }.isSuccess
2083
+ ) {
2084
+ reused = true
2085
+ break
2086
+ }
2087
+ targetFile.delete()
2088
+ }
2089
+ if (reused) return@forEachIndexed
2090
+ if (builtInAssetResolver.copyIfMatches(assetPath, expectedAsset.fileHash, targetFile)) {
1999
2091
  verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
2000
- }.isSuccess
2001
- ) {
2002
- reused = true
2003
- break
2092
+ return@forEachIndexed
2093
+ }
2094
+ downloads[assetPath] = changedAssets.getValue(assetPath)
2004
2095
  }
2005
- targetFile.delete()
2096
+ downloads
2006
2097
  }
2007
- if (reused) return@forEachIndexed
2008
- if (builtInAssetResolver.copyIfMatches(assetPath, expectedAsset.fileHash, targetFile)) {
2009
- verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
2010
- return@forEachIndexed
2011
- }
2012
- downloads[assetPath] = changedAssets.getValue(assetPath)
2013
- }
2014
2098
  val selectedArchive = selectArchive(targetManifest, downloads, archiveUrl)
2015
2099
  val archiveProgress =
2016
2100
  if (selectedArchive != null) {
2017
- tryInstallArchive(
2018
- archiveUrl = archiveUrl!!,
2019
- archive = selectedArchive,
2020
- manifest = targetManifest,
2021
- tempDir = tempDir,
2022
- bundleStoreDir = bundleStoreDir,
2023
- tmpDir = tmpDir,
2024
- bundleId = bundleId,
2025
- progressCallback = progressCallback,
2026
- )
2101
+ updateStep(UpdateFailureStage.DOWNLOAD, UpdateFailureResource.ARCHIVE) {
2102
+ tryInstallArchive(
2103
+ archiveUrl = archiveUrl!!,
2104
+ archive = selectedArchive,
2105
+ manifest = targetManifest,
2106
+ tempDir = tempDir,
2107
+ bundleStoreDir = bundleStoreDir,
2108
+ tmpDir = tmpDir,
2109
+ bundleId = bundleId,
2110
+ progressCallback = progressCallback,
2111
+ )
2112
+ }
2027
2113
  } else {
2028
2114
  null
2029
2115
  }
2030
2116
 
2031
- if (archiveProgress != null) {
2032
- diffFiles = archiveProgress
2033
- } else {
2034
- diffFiles = createDiffProgressFiles(downloads)
2035
- emitDiffProgress(progressCallback, "downloading", diffFiles)
2036
- val progressLock = Any()
2037
- coroutineScope {
2038
- val permits = Semaphore(4)
2039
- diffFiles
2040
- .toList()
2041
- .map { progressFile ->
2042
- async {
2043
- permits.withPermit {
2044
- val path = progressFile.path
2045
- val workerTemp =
2046
- File(tempDir, progressFile.order.toString()).apply { mkdirs() }
2047
- downloadManifestAsset(
2048
- assetPath = path,
2049
- expectedAsset = targetManifest.assets.getValue(path),
2050
- changedAsset = downloads.getValue(path),
2051
- currentBundleId = currentBundleId,
2052
- activeBundleDir = activeBundleDir,
2053
- targetFile =
2054
- RelativePathResolver.resolveInside(tmpDir, path)
2055
- ?: throw HotUpdaterException.invalidBundle(),
2056
- tempDir = workerTemp,
2057
- progressFile = progressFile,
2058
- progressCallback = { payload ->
2059
- synchronized(progressLock) {
2060
- payload.details?.files?.firstOrNull()?.let { snapshot ->
2061
- val index = diffFiles.indexOfFirst { it.path == path }
2062
- diffFiles[index] = snapshot
2063
- }
2064
- emitDiffProgress(progressCallback, "downloading", diffFiles)
2065
- }
2066
- },
2067
- )
2117
+ val fileDeliveries =
2118
+ if (archiveProgress != null) {
2119
+ diffFiles = archiveProgress
2120
+ emptyList()
2121
+ } else {
2122
+ diffFiles = createDiffProgressFiles(downloads)
2123
+ emitDiffProgress(progressCallback, "downloading", diffFiles)
2124
+ val progressLock = Any()
2125
+ coroutineScope {
2126
+ val permits = Semaphore(4)
2127
+ diffFiles
2128
+ .toList()
2129
+ .map { progressFile ->
2130
+ async {
2131
+ permits.withPermit {
2132
+ updateStep(UpdateFailureStage.DOWNLOAD, UpdateFailureResource.FILE) {
2133
+ val path = progressFile.path
2134
+ val workerTemp =
2135
+ File(tempDir, progressFile.order.toString()).apply { mkdirs() }
2136
+ downloadManifestAsset(
2137
+ assetPath = path,
2138
+ expectedAsset = targetManifest.assets.getValue(path),
2139
+ changedAsset = downloads.getValue(path),
2140
+ currentBundleId = currentBundleId,
2141
+ activeBundleDir = activeBundleDir,
2142
+ targetFile =
2143
+ RelativePathResolver.resolveInside(tmpDir, path)
2144
+ ?: throw HotUpdaterException.invalidBundle(),
2145
+ tempDir = workerTemp,
2146
+ progressFile = progressFile,
2147
+ progressCallback = { payload ->
2148
+ synchronized(progressLock) {
2149
+ payload.details?.files?.firstOrNull()?.let { snapshot ->
2150
+ val index = diffFiles.indexOfFirst { it.path == path }
2151
+ diffFiles[index] = snapshot
2152
+ }
2153
+ emitDiffProgress(progressCallback, "downloading", diffFiles)
2154
+ }
2155
+ },
2156
+ )
2157
+ }
2158
+ }
2068
2159
  }
2069
- }
2070
- }.awaitAll()
2160
+ }.awaitAll()
2161
+ }
2071
2162
  }
2072
- }
2073
-
2074
- emitDiffProgress(
2075
- progressCallback = progressCallback,
2076
- phase = "finalizing",
2077
- files = diffFiles,
2078
- )
2079
2163
 
2080
- manifestFile.copyTo(File(tmpDir, "manifest.json"), overwrite = true)
2164
+ updateStep(UpdateFailureStage.INSTALL, resource = null) {
2165
+ emitDiffProgress(
2166
+ progressCallback = progressCallback,
2167
+ phase = "finalizing",
2168
+ files = diffFiles,
2169
+ )
2081
2170
 
2082
- val extractedIndex = tmpDir.walk().find { it.name == "index.android.bundle" }
2083
- if (extractedIndex == null) {
2084
- throw HotUpdaterException.invalidBundle()
2085
- }
2171
+ manifestFile.copyTo(File(tmpDir, "manifest.json"), overwrite = true)
2086
2172
 
2087
- val backupDir = installBackupDir(bundleId)
2088
- val updatedMetadata =
2089
- synchronized(releaseStateLock) {
2090
- if (backupDir.exists()) {
2091
- throw HotUpdaterException.moveOperationFailed()
2092
- }
2173
+ val extractedIndex = tmpDir.walk().find { it.name == "index.android.bundle" }
2174
+ if (extractedIndex == null) {
2175
+ // The manifest lists no platform bundle.
2176
+ throw HotUpdaterException.invalidBundle().withResource(UpdateFailureResource.MANIFEST)
2177
+ }
2093
2178
 
2094
- val hadExistingFinal = finalBundleDir.exists()
2095
- var promoted = false
2096
- val previousBundleUrl = preferences.getItem("HotUpdaterBundleURL")
2097
- try {
2098
- if (hadExistingFinal && !directoryRenamer(finalBundleDir, backupDir)) {
2179
+ val backupDir = installBackupDir(bundleId)
2180
+ val updatedMetadata =
2181
+ synchronized(releaseStateLock) {
2182
+ if (backupDir.exists()) {
2099
2183
  throw HotUpdaterException.moveOperationFailed()
2100
2184
  }
2101
- if (!directoryRenamer(tmpDir, finalBundleDir)) {
2102
- if (hadExistingFinal) {
2103
- directoryRenamer(backupDir, finalBundleDir)
2185
+
2186
+ val hadExistingFinal = finalBundleDir.exists()
2187
+ var promoted = false
2188
+ val previousBundleUrl = preferences.getItem("HotUpdaterBundleURL")
2189
+ try {
2190
+ if (hadExistingFinal && !directoryRenamer(finalBundleDir, backupDir)) {
2191
+ throw HotUpdaterException.moveOperationFailed()
2104
2192
  }
2105
- throw HotUpdaterException.moveOperationFailed()
2106
- }
2107
- promoted = true
2193
+ if (!directoryRenamer(tmpDir, finalBundleDir)) {
2194
+ if (hadExistingFinal) {
2195
+ directoryRenamer(backupDir, finalBundleDir)
2196
+ }
2197
+ throw HotUpdaterException.moveOperationFailed()
2198
+ }
2199
+ promoted = true
2108
2200
 
2109
- val finalIndexFile =
2110
- resolveBundleFile(finalBundleDir, bundleId)
2111
- ?: throw HotUpdaterException.invalidBundle()
2112
- if (!isCompleteBundleDirectory(finalBundleDir, bundleId)) {
2113
- throw HotUpdaterException.invalidBundle()
2114
- }
2115
- finalBundleDir.setLastModified(System.currentTimeMillis())
2201
+ val finalIndexFile =
2202
+ resolveBundleFile(finalBundleDir, bundleId)
2203
+ ?: throw HotUpdaterException.invalidBundle()
2204
+ if (!isCompleteBundleDirectory(finalBundleDir, bundleId)) {
2205
+ throw HotUpdaterException.invalidBundle()
2206
+ }
2207
+ finalBundleDir.setLastModified(System.currentTimeMillis())
2116
2208
 
2117
- val currentMetadata = loadMetadataOrNull() ?: createInitialMetadata()
2118
- val nextMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId)
2119
- if (!setBundleURL(finalIndexFile.absolutePath)) {
2120
- throw IllegalStateException("Failed to persist bundle URL")
2121
- }
2122
- if (!saveMetadata(nextMetadata)) {
2123
- throw IllegalStateException("Failed to persist bundle metadata")
2124
- }
2209
+ val currentMetadata = loadMetadataOrNull() ?: createInitialMetadata()
2210
+ val nextMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId)
2211
+ if (!setBundleURL(finalIndexFile.absolutePath)) {
2212
+ throw HotUpdaterException.storageFailed("Failed to persist bundle URL")
2213
+ }
2214
+ if (!saveMetadata(nextMetadata)) {
2215
+ throw HotUpdaterException.storageFailed("Failed to persist bundle metadata")
2216
+ }
2125
2217
 
2126
- if (backupDir.exists() && !backupDir.deleteRecursively()) {
2127
- Log.w(TAG, "Failed to remove committed install backup: ${backupDir.absolutePath}")
2128
- }
2129
- nextMetadata
2130
- } catch (e: Exception) {
2131
- runCatching { preferences.setItem("HotUpdaterBundleURL", previousBundleUrl) }
2132
- clearActiveBundleMetadataSnapshot()
2133
- if (promoted && finalBundleDir.exists() && !finalBundleDir.deleteRecursively()) {
2134
- Log.e(TAG, "Failed to remove uncommitted bundle: ${finalBundleDir.absolutePath}")
2135
- }
2136
- if (hadExistingFinal &&
2137
- !finalBundleDir.exists() &&
2138
- backupDir.exists() &&
2139
- !directoryRenamer(backupDir, finalBundleDir)
2140
- ) {
2141
- Log.e(TAG, "Failed to restore install backup: ${backupDir.absolutePath}")
2218
+ if (backupDir.exists() && !backupDir.deleteRecursively()) {
2219
+ Log.w(TAG, "Failed to remove committed install backup: ${backupDir.absolutePath}")
2220
+ }
2221
+ nextMetadata
2222
+ } catch (e: Exception) {
2223
+ runCatching { preferences.setItem("HotUpdaterBundleURL", previousBundleUrl) }
2224
+ clearActiveBundleMetadataSnapshot()
2225
+ if (promoted && finalBundleDir.exists() && !finalBundleDir.deleteRecursively()) {
2226
+ Log.e(TAG, "Failed to remove uncommitted bundle: ${finalBundleDir.absolutePath}")
2227
+ }
2228
+ if (hadExistingFinal &&
2229
+ !finalBundleDir.exists() &&
2230
+ backupDir.exists() &&
2231
+ !directoryRenamer(backupDir, finalBundleDir)
2232
+ ) {
2233
+ Log.e(TAG, "Failed to restore install backup: ${backupDir.absolutePath}")
2234
+ }
2235
+ throw e
2142
2236
  }
2143
- throw e
2144
2237
  }
2145
- }
2146
2238
 
2147
- tempDir.deleteRecursively()
2148
- cleanupOldBundles(bundleStoreDir, updatedMetadata.stableBundleId, bundleId)
2149
- progressCallback(
2150
- UpdateProgressPayload(
2151
- progress = 1.0,
2152
- artifactType = "diff",
2153
- details =
2154
- DiffProgressDetails(
2155
- totalFilesCount = diffFiles.size,
2156
- completedFilesCount = diffFiles.count { it.status == "downloaded" },
2157
- files = diffFiles.toList(),
2158
- ),
2159
- ),
2239
+ tempDir.deleteRecursively()
2240
+ cleanupOldBundles(bundleStoreDir, updatedMetadata.stableBundleId, bundleId)
2241
+ progressCallback(
2242
+ UpdateProgressPayload(
2243
+ progress = 1.0,
2244
+ artifactType = "diff",
2245
+ details =
2246
+ DiffProgressDetails(
2247
+ totalFilesCount = diffFiles.size,
2248
+ completedFilesCount = diffFiles.count { it.status == "downloaded" },
2249
+ files = diffFiles.toList(),
2250
+ ),
2251
+ ),
2252
+ )
2253
+ }
2254
+
2255
+ return UpdateBundleResult(
2256
+ delivery =
2257
+ when {
2258
+ archiveProgress != null -> BundleDelivery.ARCHIVE
2259
+ FileDelivery.PATCHED in fileDeliveries -> BundleDelivery.PATCH
2260
+ else -> BundleDelivery.MANIFEST
2261
+ },
2262
+ patchFallback = FileDelivery.DOWNLOADED_AFTER_PATCH_FAILURE in fileDeliveries,
2160
2263
  )
2161
2264
  } catch (e: Exception) {
2162
2265
  tempDir.deleteRecursively()
2163
2266
  // A retry rehashes completed files and rejects partial bytes.
2164
- throw e
2267
+ throw classifyUpdateFailure(e, UpdateFailureStage.DOWNLOAD, resource = null)
2165
2268
  }
2166
2269
  }
2167
2270