@hot-updater/react-native 1.0.0-rc.3 → 1.0.0-rc.30

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 (300) hide show
  1. package/android/proguard-rules.pro +17 -2
  2. package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
  3. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +954 -837
  4. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +15 -11
  5. package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
  6. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +287 -22
  7. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +43 -31
  8. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
  9. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
  10. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
  11. package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
  12. package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
  13. package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
  14. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +127 -50
  15. package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
  16. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
  17. package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
  18. package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
  19. package/android/src/newarch/HotUpdater.kt +7 -9
  20. package/android/src/newarch/HotUpdaterModule.kt +109 -84
  21. package/android/src/oldarch/HotUpdater.kt +7 -9
  22. package/android/src/oldarch/HotUpdaterModule.kt +98 -74
  23. package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
  24. package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
  25. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
  26. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +950 -1112
  27. package/ios/HotUpdater/Internal/BundleMetadata.swift +8 -53
  28. package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
  29. package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
  30. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
  31. package/ios/HotUpdater/Internal/HotUpdater.mm +13 -45
  32. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +190 -73
  33. package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
  34. package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
  35. package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
  36. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +119 -71
  37. package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
  38. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
  39. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
  40. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
  41. package/lib/commonjs/appReady.js +111 -0
  42. package/lib/commonjs/appReady.js.map +1 -0
  43. package/lib/commonjs/checkForUpdate.js +124 -69
  44. package/lib/commonjs/checkForUpdate.js.map +1 -1
  45. package/lib/commonjs/clientPlugin.js +13 -0
  46. package/lib/commonjs/clientPlugin.js.map +1 -0
  47. package/lib/commonjs/error.js +4 -13
  48. package/lib/commonjs/error.js.map +1 -1
  49. package/lib/commonjs/fetchJSON.js +16 -27
  50. package/lib/commonjs/fetchJSON.js.map +1 -1
  51. package/lib/commonjs/fetchUpdateResponse.js +54 -0
  52. package/lib/commonjs/fetchUpdateResponse.js.map +1 -0
  53. package/lib/commonjs/httpClient.js +63 -94
  54. package/lib/commonjs/httpClient.js.map +1 -1
  55. package/lib/commonjs/index.js +71 -18
  56. package/lib/commonjs/index.js.map +1 -1
  57. package/lib/commonjs/native.js +78 -60
  58. package/lib/commonjs/native.js.map +1 -1
  59. package/lib/commonjs/pluginHost.js +44 -0
  60. package/lib/commonjs/pluginHost.js.map +1 -0
  61. package/lib/commonjs/releaseCatalogCache.js +36 -87
  62. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  63. package/lib/commonjs/sdkVersion.js +1 -1
  64. package/lib/commonjs/sdkVersion.js.map +1 -1
  65. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  66. package/lib/commonjs/store.js +3 -10
  67. package/lib/commonjs/store.js.map +1 -1
  68. package/lib/commonjs/updateError.js +132 -0
  69. package/lib/commonjs/updateError.js.map +1 -0
  70. package/lib/commonjs/wrap.js +43 -26
  71. package/lib/commonjs/wrap.js.map +1 -1
  72. package/lib/module/appReady.js +103 -0
  73. package/lib/module/appReady.js.map +1 -0
  74. package/lib/module/checkForUpdate.js +117 -64
  75. package/lib/module/checkForUpdate.js.map +1 -1
  76. package/lib/module/clientPlugin.js +4 -0
  77. package/lib/module/clientPlugin.js.map +1 -0
  78. package/lib/module/error.js +4 -13
  79. package/lib/module/error.js.map +1 -1
  80. package/lib/module/fetchJSON.js +14 -26
  81. package/lib/module/fetchJSON.js.map +1 -1
  82. package/lib/module/fetchUpdateResponse.js +49 -0
  83. package/lib/module/fetchUpdateResponse.js.map +1 -0
  84. package/lib/module/httpClient.js +62 -93
  85. package/lib/module/httpClient.js.map +1 -1
  86. package/lib/module/index.js +58 -20
  87. package/lib/module/index.js.map +1 -1
  88. package/lib/module/native.js +71 -53
  89. package/lib/module/native.js.map +1 -1
  90. package/lib/module/pluginHost.js +35 -0
  91. package/lib/module/pluginHost.js.map +1 -0
  92. package/lib/module/releaseCatalogCache.js +32 -82
  93. package/lib/module/releaseCatalogCache.js.map +1 -1
  94. package/lib/module/sdkVersion.js +1 -1
  95. package/lib/module/sdkVersion.js.map +1 -1
  96. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  97. package/lib/module/store.js +3 -10
  98. package/lib/module/store.js.map +1 -1
  99. package/lib/module/updateError.js +126 -0
  100. package/lib/module/updateError.js.map +1 -0
  101. package/lib/module/wrap.js +43 -26
  102. package/lib/module/wrap.js.map +1 -1
  103. package/lib/typescript/commonjs/appReady.d.ts +23 -0
  104. package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/checkForUpdate.d.ts +16 -4
  106. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/clientPlugin.d.ts +2 -0
  108. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/error.d.ts +4 -13
  110. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/fetchJSON.d.ts +5 -5
  112. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +14 -0
  114. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/httpClient.d.ts +4 -33
  116. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/index.d.ts +4 -8
  118. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/native.d.ts +31 -22
  120. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/pluginHost.d.ts +4 -0
  122. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +8 -14
  124. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  126. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +42 -47
  128. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/store.d.ts +1 -3
  130. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/types.d.ts +1 -1
  132. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/updateError.d.ts +26 -0
  134. package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/wrap.types.d.ts +6 -7
  137. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  138. package/lib/typescript/module/appReady.d.ts +23 -0
  139. package/lib/typescript/module/appReady.d.ts.map +1 -0
  140. package/lib/typescript/module/checkForUpdate.d.ts +16 -4
  141. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  142. package/lib/typescript/module/clientPlugin.d.ts +2 -0
  143. package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
  144. package/lib/typescript/module/error.d.ts +4 -13
  145. package/lib/typescript/module/error.d.ts.map +1 -1
  146. package/lib/typescript/module/fetchJSON.d.ts +5 -5
  147. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  148. package/lib/typescript/module/fetchUpdateResponse.d.ts +14 -0
  149. package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -0
  150. package/lib/typescript/module/httpClient.d.ts +4 -33
  151. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  152. package/lib/typescript/module/index.d.ts +4 -8
  153. package/lib/typescript/module/index.d.ts.map +1 -1
  154. package/lib/typescript/module/native.d.ts +31 -22
  155. package/lib/typescript/module/native.d.ts.map +1 -1
  156. package/lib/typescript/module/pluginHost.d.ts +4 -0
  157. package/lib/typescript/module/pluginHost.d.ts.map +1 -0
  158. package/lib/typescript/module/releaseCatalogCache.d.ts +8 -14
  159. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  160. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  161. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  162. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +42 -47
  163. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  164. package/lib/typescript/module/store.d.ts +1 -3
  165. package/lib/typescript/module/store.d.ts.map +1 -1
  166. package/lib/typescript/module/types.d.ts +1 -1
  167. package/lib/typescript/module/types.d.ts.map +1 -1
  168. package/lib/typescript/module/updateError.d.ts +26 -0
  169. package/lib/typescript/module/updateError.d.ts.map +1 -0
  170. package/lib/typescript/module/wrap.d.ts.map +1 -1
  171. package/lib/typescript/module/wrap.types.d.ts +6 -7
  172. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  173. package/package.json +7 -4
  174. package/src/appReady.ts +149 -0
  175. package/src/checkForUpdate.ts +151 -88
  176. package/src/clientPlugin.ts +18 -0
  177. package/src/error.ts +4 -14
  178. package/src/fetchJSON.ts +14 -30
  179. package/src/fetchUpdateResponse.ts +54 -0
  180. package/src/httpClient.ts +88 -159
  181. package/src/index.ts +90 -24
  182. package/src/native.ts +106 -102
  183. package/src/pluginHost.ts +45 -0
  184. package/src/releaseCatalogCache.ts +43 -172
  185. package/src/sdkVersion.ts +1 -1
  186. package/src/specs/NativeHotUpdater.ts +41 -48
  187. package/src/store.ts +2 -23
  188. package/src/types.ts +1 -1
  189. package/src/updateError.ts +153 -0
  190. package/src/wrap.tsx +57 -26
  191. package/src/wrap.types.ts +6 -7
  192. package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
  193. package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
  194. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
  195. package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
  196. package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
  197. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
  198. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -799
  199. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -273
  200. package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
  201. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -111
  202. package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
  203. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
  204. package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
  205. package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
  206. package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
  207. package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
  208. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
  209. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
  210. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
  211. package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
  212. package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
  213. package/lib/commonjs/catalogCacheNative.spec.js +0 -31
  214. package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
  215. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -615
  216. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  217. package/lib/commonjs/httpClient.spec.js +0 -102
  218. package/lib/commonjs/httpClient.spec.js.map +0 -1
  219. package/lib/commonjs/index.spec.js +0 -302
  220. package/lib/commonjs/index.spec.js.map +0 -1
  221. package/lib/commonjs/iosInterop.spec.js +0 -18
  222. package/lib/commonjs/iosInterop.spec.js.map +0 -1
  223. package/lib/commonjs/native.spec.js +0 -883
  224. package/lib/commonjs/native.spec.js.map +0 -1
  225. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -218
  226. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  227. package/lib/commonjs/notifyAppReadyInsights.js +0 -133
  228. package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
  229. package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -246
  230. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
  231. package/lib/commonjs/notifyAppReadyInsights.test-utils.js +0 -24
  232. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
  233. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -117
  234. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
  235. package/lib/commonjs/releaseCatalogCache.spec.js +0 -195
  236. package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
  237. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
  238. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  239. package/lib/commonjs/store.notification.spec.js +0 -76
  240. package/lib/commonjs/store.notification.spec.js.map +0 -1
  241. package/lib/commonjs/store.spec.js +0 -316
  242. package/lib/commonjs/store.spec.js.map +0 -1
  243. package/lib/commonjs/wrap.spec.js +0 -329
  244. package/lib/commonjs/wrap.spec.js.map +0 -1
  245. package/lib/module/catalogCacheNative.spec.js +0 -31
  246. package/lib/module/catalogCacheNative.spec.js.map +0 -1
  247. package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -614
  248. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  249. package/lib/module/httpClient.spec.js +0 -102
  250. package/lib/module/httpClient.spec.js.map +0 -1
  251. package/lib/module/index.spec.js +0 -301
  252. package/lib/module/index.spec.js.map +0 -1
  253. package/lib/module/iosInterop.spec.js +0 -18
  254. package/lib/module/iosInterop.spec.js.map +0 -1
  255. package/lib/module/native.spec.js +0 -882
  256. package/lib/module/native.spec.js.map +0 -1
  257. package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -217
  258. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  259. package/lib/module/notifyAppReadyInsights.js +0 -128
  260. package/lib/module/notifyAppReadyInsights.js.map +0 -1
  261. package/lib/module/notifyAppReadyInsights.spec.js +0 -245
  262. package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
  263. package/lib/module/notifyAppReadyInsights.test-utils.js +0 -18
  264. package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
  265. package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -116
  266. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
  267. package/lib/module/releaseCatalogCache.spec.js +0 -195
  268. package/lib/module/releaseCatalogCache.spec.js.map +0 -1
  269. package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
  270. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  271. package/lib/module/store.notification.spec.js +0 -77
  272. package/lib/module/store.notification.spec.js.map +0 -1
  273. package/lib/module/store.spec.js +0 -315
  274. package/lib/module/store.spec.js.map +0 -1
  275. package/lib/module/wrap.spec.js +0 -330
  276. package/lib/module/wrap.spec.js.map +0 -1
  277. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -13
  278. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
  279. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
  280. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  281. package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -13
  282. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
  283. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
  284. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  285. package/src/catalogCacheNative.spec.ts +0 -63
  286. package/src/checkForUpdate.releaseCatalog.spec.ts +0 -611
  287. package/src/httpClient.spec.ts +0 -132
  288. package/src/index.spec.ts +0 -337
  289. package/src/iosInterop.spec.ts +0 -30
  290. package/src/native.spec.ts +0 -945
  291. package/src/notifyAppReadyInsights.boundary.spec.ts +0 -234
  292. package/src/notifyAppReadyInsights.spec.ts +0 -243
  293. package/src/notifyAppReadyInsights.test-utils.ts +0 -31
  294. package/src/notifyAppReadyInsights.transport.spec.ts +0 -130
  295. package/src/notifyAppReadyInsights.ts +0 -194
  296. package/src/releaseCatalogCache.spec.ts +0 -282
  297. package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
  298. package/src/store.notification.spec.ts +0 -92
  299. package/src/store.spec.ts +0 -372
  300. package/src/wrap.spec.ts +0 -406
@@ -9,6 +9,7 @@ import android.os.Looper
9
9
  import android.os.Process
10
10
  import android.os.SystemClock
11
11
  import android.util.Log
12
+ import androidx.core.util.AtomicFile
12
13
  import com.facebook.react.ReactApplication
13
14
  import com.facebook.react.bridge.JSExceptionHandler
14
15
  import com.facebook.react.bridge.ReactContext
@@ -16,6 +17,7 @@ import com.facebook.react.bridge.ReactMarker
16
17
  import com.facebook.react.bridge.ReactMarkerConstants
17
18
  import org.json.JSONObject
18
19
  import java.io.File
20
+ import java.io.FileOutputStream
19
21
  import java.lang.reflect.Field
20
22
  import kotlin.system.exitProcess
21
23
 
@@ -28,10 +30,7 @@ internal class HotUpdaterRecoveryManager(
28
30
  private val crashMarkerFile = File(bundleStoreDir, CRASH_MARKER_FILENAME)
29
31
  private val watchdogStateFile = File(bundleStoreDir, WATCHDOG_STATE_FILENAME)
30
32
 
31
- private var currentBundleId: String? = null
32
- private var shouldRollbackOnCrash = false
33
- private var isMonitoring = false
34
- private var recoveryRequested = false
33
+ private val launchState = HotUpdaterRecoveryState()
35
34
  private var contentAppearedCallback: ((String?) -> Unit)? = null
36
35
  private var recoveryRestartCallback: (() -> Boolean)? = null
37
36
 
@@ -39,10 +38,7 @@ internal class HotUpdaterRecoveryManager(
39
38
  Runnable {
40
39
  cancelRecoveryWatchdog()
41
40
  Log.d(TAG, "Stopping crash monitoring for current launch")
42
- isMonitoring = false
43
- recoveryRequested = false
44
- shouldRollbackOnCrash = false
45
- currentBundleId = null
41
+ launchState.stop()
46
42
  contentAppearedCallback = null
47
43
  recoveryRestartCallback = null
48
44
  activeManager = null
@@ -52,7 +48,7 @@ internal class HotUpdaterRecoveryManager(
52
48
  private val installJsExceptionHooksRunnable =
53
49
  object : Runnable {
54
50
  override fun run() {
55
- if (!isMonitoring) {
51
+ if (launchState.snapshot() == null) {
56
52
  return
57
53
  }
58
54
 
@@ -74,7 +70,7 @@ internal class HotUpdaterRecoveryManager(
74
70
  }
75
71
 
76
72
  return try {
77
- val recovery = PendingCrashRecovery.fromJson(JSONObject(crashMarkerFile.readText()))
73
+ val recovery = PendingCrashRecovery.loadFromFile(crashMarkerFile) ?: return null
78
74
  Log.d(
79
75
  TAG,
80
76
  "Consumed pending crash marker bundleId=${recovery.launchedBundleId} shouldRollback=${recovery.shouldRollback}",
@@ -96,11 +92,8 @@ internal class HotUpdaterRecoveryManager(
96
92
  ) {
97
93
  crashMarkerFile.parentFile?.mkdirs()
98
94
  contentAppearedCallback = onContentAppeared
99
- currentBundleId = bundleId
100
- shouldRollbackOnCrash = shouldRollback
101
- isMonitoring = true
102
- recoveryRequested = false
103
95
  recoveryRestartCallback = onRecoveryRestartRequested
96
+ launchState.start(bundleId, shouldRollback)
104
97
  activeManager = this
105
98
 
106
99
  ensureExceptionHandlerInstalled()
@@ -122,17 +115,13 @@ internal class HotUpdaterRecoveryManager(
122
115
  }
123
116
 
124
117
  private fun handleContentAppeared() {
125
- if (!isMonitoring) {
126
- return
127
- }
128
-
129
- Log.d(TAG, "First content appeared for bundleId=$currentBundleId")
118
+ val launch = launchState.completeLaunch() ?: return
119
+ Log.d(TAG, "First content appeared for bundleId=${launch.bundleId}")
130
120
  ReactMarker.removeListener(contentAppearedListener)
131
121
  mainHandler.removeCallbacks(installJsExceptionHooksRunnable)
132
122
  mainHandler.removeCallbacks(stopMonitoringRunnable)
133
- contentAppearedCallback?.invoke(currentBundleId)
134
- shouldRollbackOnCrash = false
135
- updateNativeLaunchState(currentBundleId, false)
123
+ contentAppearedCallback?.invoke(launch.bundleId)
124
+ updateNativeLaunchState(launch.bundleId, false)
136
125
  cancelRecoveryWatchdog()
137
126
  mainHandler.postDelayed(stopMonitoringRunnable, MONITORING_GRACE_PERIOD_MS)
138
127
  }
@@ -145,11 +134,10 @@ internal class HotUpdaterRecoveryManager(
145
134
  previousExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
146
135
  Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
147
136
  val manager = activeManager
148
- manager?.writeCrashMarker()
149
-
150
137
  if (manager?.requestAutomaticRecovery() == true) {
151
- Process.killProcess(Process.myPid())
152
- exitProcess(10)
138
+ // The separate restart activity kills this process after it starts.
139
+ // A second fatal error must not kill it while that request is in flight.
140
+ return@setDefaultUncaughtExceptionHandler
153
141
  }
154
142
 
155
143
  previousExceptionHandler?.uncaughtException(thread, throwable)
@@ -161,20 +149,21 @@ internal class HotUpdaterRecoveryManager(
161
149
  exceptionHandlerInstalled = true
162
150
  }
163
151
 
164
- private fun writeCrashMarker() {
165
- if (!isMonitoring) {
166
- return
167
- }
168
-
152
+ private fun writeCrashMarker(launch: HotUpdaterRecoveryState.Launch) {
153
+ val atomicFile = AtomicFile(crashMarkerFile)
154
+ var output: FileOutputStream? = null
169
155
  try {
170
156
  val payload =
171
157
  JSONObject().apply {
172
- put("bundleId", currentBundleId ?: JSONObject.NULL)
173
- put("shouldRollback", shouldRollbackOnCrash)
158
+ put("bundleId", launch.bundleId ?: JSONObject.NULL)
159
+ put("shouldRollback", launch.shouldRollback)
174
160
  }
175
161
  crashMarkerFile.parentFile?.mkdirs()
176
- crashMarkerFile.writeText(payload.toString())
162
+ output = atomicFile.startWrite()
163
+ output.write(payload.toString().toByteArray(Charsets.UTF_8))
164
+ atomicFile.finishWrite(output)
177
165
  } catch (e: Exception) {
166
+ output?.let(atomicFile::failWrite)
178
167
  Log.e(TAG, "Failed to write crash marker", e)
179
168
  }
180
169
  }
@@ -194,35 +183,28 @@ internal class HotUpdaterRecoveryManager(
194
183
  cancelRecoveryWatchdogAlarm(appContext)
195
184
  }
196
185
 
197
- private fun requestAutomaticRecovery(): Boolean {
198
- if (!isMonitoring || !shouldRollbackOnCrash) {
199
- return false
186
+ private fun requestAutomaticRecovery(exception: Exception? = null): Boolean {
187
+ val request = launchState.requestRecovery()
188
+ if (exception != null) {
189
+ Log.e(TAG, "Caught React startup exception for bundleId=${launchState.snapshot()?.bundleId}", exception)
200
190
  }
201
-
202
- synchronized(this) {
203
- if (recoveryRequested) {
204
- return true
205
- }
206
- recoveryRequested = true
191
+ if (request == null) {
192
+ launchState.snapshot()?.let(::writeCrashMarker)
193
+ return false
207
194
  }
208
-
195
+ if (!request.shouldRestart) return true
196
+ writeCrashMarker(request.launch)
209
197
  val started = recoveryRestartCallback?.invoke() == true
198
+ launchState.finishRecoveryRequest(request, started)
210
199
  if (!started) {
211
- synchronized(this) {
212
- recoveryRequested = false
213
- }
214
200
  Log.w(TAG, "Failed to schedule automatic recovery restart")
215
201
  } else {
216
- Log.i(TAG, "Scheduled automatic recovery restart for bundleId=$currentBundleId")
202
+ Log.i(TAG, "Scheduled automatic recovery restart for bundleId=${request.launch.bundleId}")
217
203
  }
218
204
  return started
219
205
  }
220
206
 
221
- private fun handleJavaScriptException(exception: Exception): Boolean {
222
- Log.e(TAG, "Caught React startup exception for bundleId=$currentBundleId", exception)
223
- writeCrashMarker()
224
- return requestAutomaticRecovery()
225
- }
207
+ private fun handleJavaScriptException(exception: Exception): Boolean = requestAutomaticRecovery(exception)
226
208
 
227
209
  private fun ensureNativeSignalHandlerInstalled() {
228
210
  if (signalHandlerInstalled || !loadNativeLibrary()) {
@@ -467,7 +449,8 @@ internal class HotUpdaterRecoveryManager(
467
449
  }
468
450
 
469
451
  val crashMarkerFile = File(getBundleStoreDir(appContext), CRASH_MARKER_FILENAME)
470
- if (crashMarkerFile.exists()) {
452
+ val recovery = PendingCrashRecovery.loadFromFile(crashMarkerFile)
453
+ if (recovery?.shouldRollback == true && recovery.launchedBundleId != null) {
471
454
  Log.i(TAG, "Recovery watchdog detected crash marker, relaunching app")
472
455
  watchdogStateFile.delete()
473
456
  cancelRecoveryWatchdogAlarm(appContext)
@@ -0,0 +1,64 @@
1
+ package com.hotupdater
2
+
3
+ internal class HotUpdaterRecoveryState {
4
+ data class Launch(
5
+ val bundleId: String?,
6
+ val shouldRollback: Boolean,
7
+ )
8
+
9
+ data class RecoveryRequest(
10
+ val launch: Launch,
11
+ val shouldRestart: Boolean,
12
+ )
13
+
14
+ private enum class RestartState { NONE, REQUESTED, SCHEDULED, FAILED }
15
+
16
+ private var launch: Launch? = null
17
+ private var restartState = RestartState.NONE
18
+ private var contentAppeared = false
19
+
20
+ @Synchronized
21
+ fun start(
22
+ bundleId: String?,
23
+ shouldRollback: Boolean,
24
+ ) {
25
+ launch = Launch(bundleId, shouldRollback)
26
+ restartState = RestartState.NONE
27
+ contentAppeared = false
28
+ }
29
+
30
+ @Synchronized
31
+ fun snapshot(): Launch? = launch
32
+
33
+ @Synchronized
34
+ fun stop() {
35
+ launch = null
36
+ restartState = RestartState.NONE
37
+ }
38
+
39
+ @Synchronized
40
+ fun completeLaunch(): Launch? {
41
+ val current = launch ?: return null
42
+ if (contentAppeared || restartState != RestartState.NONE) return null
43
+ contentAppeared = true
44
+ launch = current.copy(shouldRollback = false)
45
+ return current
46
+ }
47
+
48
+ @Synchronized
49
+ fun requestRecovery(): RecoveryRequest? {
50
+ val current = launch?.takeIf { it.shouldRollback } ?: return null
51
+ val shouldRestart = restartState == RestartState.NONE || restartState == RestartState.FAILED
52
+ if (shouldRestart) restartState = RestartState.REQUESTED
53
+ return RecoveryRequest(current, shouldRestart)
54
+ }
55
+
56
+ @Synchronized
57
+ fun finishRecoveryRequest(
58
+ request: RecoveryRequest,
59
+ started: Boolean,
60
+ ) {
61
+ if (launch !== request.launch) return
62
+ restartState = if (started) RestartState.SCHEDULED else RestartState.FAILED
63
+ }
64
+ }
@@ -0,0 +1,37 @@
1
+ package com.hotupdater
2
+
3
+ import android.content.Context
4
+ import java.io.File
5
+ import java.util.UUID
6
+
7
+ /**
8
+ * Owns the install id, a random id created once per app installation.
9
+ *
10
+ * The id lives outside the bundle store and outside device backups, so a
11
+ * device restored from a backup gets an id of its own.
12
+ */
13
+ class InstallIdentityService(
14
+ private val identityFile: File,
15
+ ) {
16
+ companion object {
17
+ /** The install id kept in `noBackupFilesDir/hot-updater/install-identity.json`. */
18
+ fun create(context: Context): InstallIdentityService =
19
+ InstallIdentityService(NoBackupStorage.file(context, InstallationIdentity.IDENTITY_FILENAME))
20
+ }
21
+
22
+ private var identity: InstallationIdentity? = null
23
+
24
+ /**
25
+ * Returns the install id, creating and persisting a random one when none
26
+ * is stored. The id stays stable for this process even if persisting fails.
27
+ */
28
+ @Synchronized
29
+ fun getInstallId(): String {
30
+ identity?.let { return it.installId }
31
+ val current =
32
+ InstallationIdentity.loadFromFile(identityFile)
33
+ ?: InstallationIdentity(installId = UUID.randomUUID().toString()).also { it.saveToFile(identityFile) }
34
+ identity = current
35
+ return current.installId
36
+ }
37
+ }
@@ -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
  */
@@ -65,6 +108,13 @@ interface DownloadService {
65
108
  fileSizeCallback: ((Long) -> Unit)? = null,
66
109
  progressCallback: (DownloadProgress) -> Unit,
67
110
  ): DownloadResult
111
+
112
+ suspend fun downloadFileOnce(
113
+ fileUrl: URL,
114
+ destination: File,
115
+ fileSizeCallback: ((Long) -> Unit)? = null,
116
+ progressCallback: (DownloadProgress) -> Unit,
117
+ ): DownloadResult = downloadFile(fileUrl, destination, fileSizeCallback, progressCallback)
68
118
  }
69
119
 
70
120
  /**
@@ -127,13 +177,33 @@ private class ProgressResponseBody(
127
177
  }
128
178
  }
129
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
+
130
196
  /**
131
197
  * OkHttp-based implementation of DownloadService with resume support
132
198
  */
133
- class OkHttpDownloadService : DownloadService {
199
+ class OkHttpDownloadService internal constructor(
200
+ private val initialRetryDelayMs: Long,
201
+ ) : DownloadService {
202
+ constructor() : this(INITIAL_RETRY_DELAY_MS)
203
+
134
204
  companion object {
135
205
  private const val TAG = "OkHttpDownloadService"
136
- private const val MAX_RETRIES = 3
206
+ internal const val MAX_ATTEMPTS = 3
137
207
  private const val INITIAL_RETRY_DELAY_MS = 1000L
138
208
  private const val TIMEOUT_SECONDS = 30L
139
209
  }
@@ -146,6 +216,12 @@ class OkHttpDownloadService : DownloadService {
146
216
  .writeTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
147
217
  .build()
148
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
+ */
149
225
  override suspend fun downloadFile(
150
226
  fileUrl: URL,
151
227
  destination: File,
@@ -153,38 +229,51 @@ class OkHttpDownloadService : DownloadService {
153
229
  progressCallback: (DownloadProgress) -> Unit,
154
230
  ): DownloadResult =
155
231
  withContext(Dispatchers.IO) {
156
- var attempt = 0
157
- var lastException: Exception? = null
158
-
159
- while (attempt < MAX_RETRIES) {
160
- try {
161
- return@withContext attemptDownload(
162
- fileUrl,
163
- destination,
164
- fileSizeCallback,
165
- progressCallback,
166
- )
167
- } catch (e: Exception) {
168
- lastException = e
169
- attempt++
170
-
171
- if (attempt < MAX_RETRIES && isRetryableException(e)) {
172
- val delayMs = INITIAL_RETRY_DELAY_MS * (1 shl (attempt - 1))
173
- Log.d(
174
- TAG,
175
- "Download failed (attempt $attempt/$MAX_RETRIES): ${e.message}. Retrying in ${delayMs}ms...",
176
- )
177
- delay(delayMs)
178
- } else {
179
- Log.d(TAG, "Download failed: ${e.message}")
180
- break
181
- }
182
- }
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)
183
247
  }
248
+ if (result is DownloadResult.Error) {
249
+ Log.d(TAG, "Download failed (attempt $attempt/$MAX_ATTEMPTS): ${result.exception.message}")
250
+ }
251
+ result
252
+ }
184
253
 
185
- DownloadResult.Error(lastException ?: Exception("Download failed after $MAX_RETRIES attempts"))
254
+ /** One attempt; an exception a callback threw, such as the disk space check's, is its error. */
255
+ private suspend fun attemptOrError(
256
+ fileUrl: URL,
257
+ destination: File,
258
+ fileSizeCallback: ((Long) -> Unit)?,
259
+ progressCallback: (DownloadProgress) -> Unit,
260
+ ): DownloadResult =
261
+ try {
262
+ attemptDownload(fileUrl, destination, fileSizeCallback, progressCallback)
263
+ } catch (error: CancellationException) {
264
+ throw error
265
+ } catch (error: Exception) {
266
+ DownloadResult.Error(error)
186
267
  }
187
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
+
188
277
  private suspend fun attemptDownload(
189
278
  fileUrl: URL,
190
279
  destination: File,
@@ -212,10 +301,10 @@ class OkHttpDownloadService : DownloadService {
212
301
  }
213
302
 
214
303
  if (!response.isSuccessful) {
215
- val errorMsg = "HTTP error ${response.code}: ${response.message}"
216
- 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}")
217
306
  response.close()
218
- return@withContext DownloadResult.Error(Exception(errorMsg))
307
+ return@withContext DownloadResult.Error(error)
219
308
  }
220
309
 
221
310
  val body = response.body
@@ -242,9 +331,10 @@ class OkHttpDownloadService : DownloadService {
242
331
  progressCallback.invoke(progress)
243
332
  }
244
333
 
245
- // Write to file
334
+ // Write to file. A local write failure surfaces as LocalStorageException,
335
+ // so it is not mistaken for a network failure.
246
336
  progressBody.source().use { source ->
247
- destination.outputStream().use { output ->
337
+ LocalStorageOutputStream.open(destination).use { output ->
248
338
  val buffer = ByteArray(8 * 1024)
249
339
  var bytesRead: Int
250
340
 
@@ -291,17 +381,4 @@ class OkHttpDownloadService : DownloadService {
291
381
  DownloadResult.Error(e)
292
382
  }
293
383
  }
294
-
295
- /**
296
- * Check if exception is retryable
297
- */
298
- private fun isRetryableException(e: Exception): Boolean =
299
- when (e) {
300
- is SocketTimeoutException,
301
- is UnknownHostException,
302
- is IOException,
303
- -> true
304
-
305
- else -> false
306
- }
307
384
  }