@hot-updater/react-native 0.36.7 → 1.0.0-rc.1

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 (239) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +357 -6
  2. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +268 -34
  3. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +75 -9
  4. package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +2 -16
  5. package/android/src/main/java/com/hotupdater/ReleaseCatalogCacheService.kt +166 -0
  6. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +1 -2
  7. package/android/src/newarch/HotUpdater.kt +1 -1
  8. package/android/src/newarch/HotUpdaterModule.kt +116 -0
  9. package/android/src/oldarch/HotUpdater.kt +1 -1
  10. package/android/src/oldarch/HotUpdaterModule.kt +128 -0
  11. package/android/src/oldarch/HotUpdaterSpec.kt +38 -0
  12. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +280 -2
  13. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +57 -2
  14. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +118 -0
  15. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +491 -30
  16. package/ios/HotUpdater/Internal/BundleMetadata.swift +229 -6
  17. package/ios/HotUpdater/Internal/HotUpdater.mm +122 -0
  18. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +176 -7
  19. package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +213 -0
  20. package/lib/commonjs/catalogCacheNative.js +44 -0
  21. package/lib/commonjs/catalogCacheNative.js.map +1 -0
  22. package/lib/commonjs/catalogCacheNative.spec.js +31 -0
  23. package/lib/commonjs/catalogCacheNative.spec.js.map +1 -0
  24. package/lib/commonjs/checkForUpdate.js +231 -60
  25. package/lib/commonjs/checkForUpdate.js.map +1 -1
  26. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +604 -0
  27. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -0
  28. package/lib/commonjs/error.js +8 -1
  29. package/lib/commonjs/error.js.map +1 -1
  30. package/lib/commonjs/{fetchUpdateInfo.js → fetchJSON.js} +11 -14
  31. package/lib/commonjs/fetchJSON.js.map +1 -0
  32. package/lib/commonjs/httpClient.js +153 -0
  33. package/lib/commonjs/httpClient.js.map +1 -0
  34. package/lib/commonjs/httpClient.spec.js +102 -0
  35. package/lib/commonjs/httpClient.spec.js.map +1 -0
  36. package/lib/commonjs/index.js +47 -70
  37. package/lib/commonjs/index.js.map +1 -1
  38. package/lib/commonjs/index.spec.js +115 -159
  39. package/lib/commonjs/index.spec.js.map +1 -1
  40. package/lib/commonjs/iosInterop.spec.js +18 -0
  41. package/lib/commonjs/iosInterop.spec.js.map +1 -0
  42. package/lib/commonjs/native.js +222 -53
  43. package/lib/commonjs/native.js.map +1 -1
  44. package/lib/commonjs/native.spec.js +226 -12
  45. package/lib/commonjs/native.spec.js.map +1 -1
  46. package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js +218 -0
  47. package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js.map +1 -0
  48. package/lib/commonjs/notifyAppReadyAnalytics.js +133 -0
  49. package/lib/commonjs/notifyAppReadyAnalytics.js.map +1 -0
  50. package/lib/commonjs/notifyAppReadyAnalytics.spec.js +246 -0
  51. package/lib/commonjs/notifyAppReadyAnalytics.spec.js.map +1 -0
  52. package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js +24 -0
  53. package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js.map +1 -0
  54. package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js +117 -0
  55. package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js.map +1 -0
  56. package/lib/commonjs/releaseCatalogCache.js +179 -0
  57. package/lib/commonjs/releaseCatalogCache.js.map +1 -0
  58. package/lib/commonjs/releaseCatalogCache.spec.js +195 -0
  59. package/lib/commonjs/releaseCatalogCache.spec.js.map +1 -0
  60. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +64 -0
  61. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +1 -0
  62. package/lib/commonjs/sdkVersion.js +1 -1
  63. package/lib/commonjs/sdkVersion.js.map +1 -1
  64. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  65. package/lib/commonjs/types.js +0 -12
  66. package/lib/commonjs/types.js.map +1 -1
  67. package/lib/commonjs/wrap.js +7 -82
  68. package/lib/commonjs/wrap.js.map +1 -1
  69. package/lib/commonjs/wrap.spec.js +203 -56
  70. package/lib/commonjs/wrap.spec.js.map +1 -1
  71. package/lib/commonjs/wrap.types.js +6 -0
  72. package/lib/commonjs/wrap.types.js.map +1 -0
  73. package/lib/module/catalogCacheNative.js +36 -0
  74. package/lib/module/catalogCacheNative.js.map +1 -0
  75. package/lib/module/catalogCacheNative.spec.js +31 -0
  76. package/lib/module/catalogCacheNative.spec.js.map +1 -0
  77. package/lib/module/checkForUpdate.js +233 -62
  78. package/lib/module/checkForUpdate.js.map +1 -1
  79. package/lib/module/checkForUpdate.releaseCatalog.spec.js +603 -0
  80. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -0
  81. package/lib/module/error.js +6 -0
  82. package/lib/module/error.js.map +1 -1
  83. package/lib/module/{fetchUpdateInfo.js → fetchJSON.js} +9 -12
  84. package/lib/module/fetchJSON.js.map +1 -0
  85. package/lib/module/httpClient.js +148 -0
  86. package/lib/module/httpClient.js.map +1 -0
  87. package/lib/module/httpClient.spec.js +102 -0
  88. package/lib/module/httpClient.spec.js.map +1 -0
  89. package/lib/module/index.js +48 -65
  90. package/lib/module/index.js.map +1 -1
  91. package/lib/module/index.spec.js +115 -159
  92. package/lib/module/index.spec.js.map +1 -1
  93. package/lib/module/iosInterop.spec.js +18 -0
  94. package/lib/module/iosInterop.spec.js.map +1 -0
  95. package/lib/module/native.js +206 -49
  96. package/lib/module/native.js.map +1 -1
  97. package/lib/module/native.spec.js +226 -12
  98. package/lib/module/native.spec.js.map +1 -1
  99. package/lib/module/notifyAppReadyAnalytics.boundary.spec.js +217 -0
  100. package/lib/module/notifyAppReadyAnalytics.boundary.spec.js.map +1 -0
  101. package/lib/module/notifyAppReadyAnalytics.js +128 -0
  102. package/lib/module/notifyAppReadyAnalytics.js.map +1 -0
  103. package/lib/module/notifyAppReadyAnalytics.spec.js +245 -0
  104. package/lib/module/notifyAppReadyAnalytics.spec.js.map +1 -0
  105. package/lib/module/notifyAppReadyAnalytics.test-utils.js +18 -0
  106. package/lib/module/notifyAppReadyAnalytics.test-utils.js.map +1 -0
  107. package/lib/module/notifyAppReadyAnalytics.transport.spec.js +116 -0
  108. package/lib/module/notifyAppReadyAnalytics.transport.spec.js.map +1 -0
  109. package/lib/module/releaseCatalogCache.js +171 -0
  110. package/lib/module/releaseCatalogCache.js.map +1 -0
  111. package/lib/module/releaseCatalogCache.spec.js +195 -0
  112. package/lib/module/releaseCatalogCache.spec.js.map +1 -0
  113. package/lib/module/releaseCatalogCacheNativeContract.spec.js +64 -0
  114. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +1 -0
  115. package/lib/module/sdkVersion.js +1 -1
  116. package/lib/module/sdkVersion.js.map +1 -1
  117. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  118. package/lib/module/types.js +0 -12
  119. package/lib/module/types.js.map +1 -1
  120. package/lib/module/wrap.js +6 -81
  121. package/lib/module/wrap.js.map +1 -1
  122. package/lib/module/wrap.spec.js +203 -56
  123. package/lib/module/wrap.spec.js.map +1 -1
  124. package/lib/module/wrap.types.js +4 -0
  125. package/lib/module/wrap.types.js.map +1 -0
  126. package/lib/typescript/commonjs/catalogCacheNative.d.ts +4 -0
  127. package/lib/typescript/commonjs/catalogCacheNative.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/checkForUpdate.d.ts +14 -4
  129. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/error.d.ts +3 -0
  131. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/fetchJSON.d.ts +6 -0
  133. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/httpClient.d.ts +56 -0
  135. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/index.d.ts +31 -40
  137. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/native.d.ts +84 -17
  139. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts +13 -0
  141. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts.map +1 -0
  142. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts +8 -0
  143. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts.map +1 -0
  144. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +25 -0
  145. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -0
  146. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  147. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  148. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +40 -5
  149. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/types.d.ts +0 -118
  151. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/wrap.d.ts +2 -191
  153. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/wrap.types.d.ts +65 -0
  155. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -0
  156. package/lib/typescript/module/catalogCacheNative.d.ts +4 -0
  157. package/lib/typescript/module/catalogCacheNative.d.ts.map +1 -0
  158. package/lib/typescript/module/checkForUpdate.d.ts +14 -4
  159. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  160. package/lib/typescript/module/error.d.ts +3 -0
  161. package/lib/typescript/module/error.d.ts.map +1 -1
  162. package/lib/typescript/module/fetchJSON.d.ts +6 -0
  163. package/lib/typescript/module/fetchJSON.d.ts.map +1 -0
  164. package/lib/typescript/module/httpClient.d.ts +56 -0
  165. package/lib/typescript/module/httpClient.d.ts.map +1 -0
  166. package/lib/typescript/module/index.d.ts +31 -40
  167. package/lib/typescript/module/index.d.ts.map +1 -1
  168. package/lib/typescript/module/native.d.ts +84 -17
  169. package/lib/typescript/module/native.d.ts.map +1 -1
  170. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts +13 -0
  171. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts.map +1 -0
  172. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts +8 -0
  173. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts.map +1 -0
  174. package/lib/typescript/module/releaseCatalogCache.d.ts +25 -0
  175. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -0
  176. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  177. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  178. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +40 -5
  179. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  180. package/lib/typescript/module/types.d.ts +0 -118
  181. package/lib/typescript/module/types.d.ts.map +1 -1
  182. package/lib/typescript/module/wrap.d.ts +2 -191
  183. package/lib/typescript/module/wrap.d.ts.map +1 -1
  184. package/lib/typescript/module/wrap.types.d.ts +65 -0
  185. package/lib/typescript/module/wrap.types.d.ts.map +1 -0
  186. package/package.json +6 -24
  187. package/src/catalogCacheNative.spec.ts +63 -0
  188. package/src/catalogCacheNative.ts +74 -0
  189. package/src/checkForUpdate.releaseCatalog.spec.ts +596 -0
  190. package/src/checkForUpdate.ts +367 -96
  191. package/src/error.ts +7 -0
  192. package/src/{fetchUpdateInfo.ts → fetchJSON.ts} +8 -15
  193. package/src/httpClient.spec.ts +132 -0
  194. package/src/httpClient.ts +261 -0
  195. package/src/index.spec.ts +108 -201
  196. package/src/index.ts +68 -91
  197. package/src/iosInterop.spec.ts +30 -0
  198. package/src/native.spec.ts +226 -10
  199. package/src/native.ts +385 -81
  200. package/src/notifyAppReadyAnalytics.boundary.spec.ts +234 -0
  201. package/src/notifyAppReadyAnalytics.spec.ts +243 -0
  202. package/src/notifyAppReadyAnalytics.test-utils.ts +31 -0
  203. package/src/notifyAppReadyAnalytics.transport.spec.ts +130 -0
  204. package/src/notifyAppReadyAnalytics.ts +194 -0
  205. package/src/releaseCatalogCache.spec.ts +282 -0
  206. package/src/releaseCatalogCache.ts +341 -0
  207. package/src/releaseCatalogCacheNativeContract.spec.ts +80 -0
  208. package/src/sdkVersion.ts +1 -1
  209. package/src/specs/NativeHotUpdater.ts +51 -5
  210. package/src/types.ts +0 -140
  211. package/src/wrap.spec.ts +251 -56
  212. package/src/wrap.tsx +23 -318
  213. package/src/wrap.types.ts +78 -0
  214. package/android/src/main/java/com/hotupdater/StringResourceUtils.kt +0 -45
  215. package/app.plugin.js +0 -1
  216. package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +0 -7
  217. package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +0 -4
  218. package/lib/commonjs/DefaultResolver.js +0 -50
  219. package/lib/commonjs/DefaultResolver.js.map +0 -1
  220. package/lib/commonjs/DefaultResolver.spec.js +0 -144
  221. package/lib/commonjs/DefaultResolver.spec.js.map +0 -1
  222. package/lib/commonjs/fetchUpdateInfo.js.map +0 -1
  223. package/lib/module/DefaultResolver.js +0 -46
  224. package/lib/module/DefaultResolver.js.map +0 -1
  225. package/lib/module/DefaultResolver.spec.js +0 -144
  226. package/lib/module/DefaultResolver.spec.js.map +0 -1
  227. package/lib/module/fetchUpdateInfo.js.map +0 -1
  228. package/lib/typescript/commonjs/DefaultResolver.d.ts +0 -11
  229. package/lib/typescript/commonjs/DefaultResolver.d.ts.map +0 -1
  230. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +0 -7
  231. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +0 -1
  232. package/lib/typescript/module/DefaultResolver.d.ts +0 -11
  233. package/lib/typescript/module/DefaultResolver.d.ts.map +0 -1
  234. package/lib/typescript/module/fetchUpdateInfo.d.ts +0 -7
  235. package/lib/typescript/module/fetchUpdateInfo.d.ts.map +0 -1
  236. package/plugin/build/transformers.js +0 -314
  237. package/plugin/build/withHotUpdater.js +0 -228
  238. package/src/DefaultResolver.spec.ts +0 -193
  239. package/src/DefaultResolver.ts +0 -63
@@ -1,9 +1,12 @@
1
1
  package com.hotupdater
2
2
 
3
3
  import android.util.Log
4
+ import androidx.core.util.AtomicFile
4
5
  import org.json.JSONArray
5
6
  import org.json.JSONObject
6
7
  import java.io.File
8
+ import java.io.FileNotFoundException
9
+ import java.io.FileOutputStream
7
10
 
8
11
  /**
9
12
  * Bundle metadata for managing stable/staging bundles and verification state
@@ -13,38 +16,66 @@ data class BundleMetadata(
13
16
  val isolationKey: String? = null,
14
17
  val stableBundleId: String? = null,
15
18
  val stagingBundleId: String? = null,
19
+ val stableSelection: PersistedSelection? = null,
20
+ val stagingSelection: PersistedSelection? = null,
21
+ val pendingUpdateStrategy: String? = null,
22
+ val pendingTransition: PendingSelectionTransition? = null,
16
23
  val verificationPending: Boolean = false,
24
+ val highestSeenCatalogs: Map<String, CatalogHighWater> = emptyMap(),
25
+ val currentSelectionContexts: Map<String, String> = emptyMap(),
17
26
  val updatedAt: Long = System.currentTimeMillis(),
18
27
  ) {
19
28
  companion object {
20
29
  private const val TAG = "BundleMetadata"
21
- const val SCHEMA_VERSION = "metadata-v1"
30
+ const val SCHEMA_VERSION = "metadata-v2"
22
31
  const val METADATA_FILENAME = "metadata.json"
23
32
 
24
- fun fromJson(json: JSONObject): BundleMetadata =
25
- BundleMetadata(
26
- schema = json.optString("schema", SCHEMA_VERSION),
33
+ fun fromJson(json: JSONObject): BundleMetadata {
34
+ val stableBundleId = json.optNullableString("stableBundleId")
35
+ val stagingBundleId = json.optNullableString("stagingBundleId")
36
+ val stableSelection =
37
+ json.optJSONObject("stableSelection")?.let(PersistedSelection::fromJson)
38
+ ?: stableBundleId?.let(PersistedSelection::legacyBundle)
39
+ val stagingSelection =
40
+ json.optJSONObject("stagingSelection")?.let(PersistedSelection::fromJson)
41
+ ?: stagingBundleId?.let(PersistedSelection::legacyBundle)
42
+ val highestSeenCatalogs = linkedMapOf<String, CatalogHighWater>()
43
+ json.optJSONObject("highestSeenCatalogs")?.let { highWaters ->
44
+ highWaters.keys().forEach { key ->
45
+ highWaters.optJSONObject(key)?.let { value ->
46
+ highestSeenCatalogs[key] = CatalogHighWater.fromJson(value)
47
+ }
48
+ }
49
+ }
50
+ val currentSelectionContexts = linkedMapOf<String, String>()
51
+ json.optJSONObject("currentSelectionContexts")?.let { contexts ->
52
+ contexts.keys().forEach { key ->
53
+ contexts.optString(key).takeIf { it.isNotEmpty() }?.let { value ->
54
+ currentSelectionContexts[key] = value
55
+ }
56
+ }
57
+ }
58
+ return BundleMetadata(
59
+ schema = SCHEMA_VERSION,
27
60
  isolationKey =
28
61
  if (json.has("isolationKey") && !json.isNull("isolationKey")) {
29
62
  json.getString("isolationKey").takeIf { it.isNotEmpty() }
30
63
  } else {
31
64
  null
32
65
  },
33
- stableBundleId =
34
- if (json.has("stableBundleId") && !json.isNull("stableBundleId")) {
35
- json.getString("stableBundleId").takeIf { it.isNotEmpty() }
36
- } else {
37
- null
38
- },
39
- stagingBundleId =
40
- if (json.has("stagingBundleId") && !json.isNull("stagingBundleId")) {
41
- json.getString("stagingBundleId").takeIf { it.isNotEmpty() }
42
- } else {
43
- null
44
- },
66
+ stableBundleId = stableSelection?.bundleId ?: stableBundleId,
67
+ stagingBundleId = stagingSelection?.bundleId ?: stagingBundleId,
68
+ stableSelection = stableSelection,
69
+ stagingSelection = stagingSelection,
70
+ pendingUpdateStrategy = json.optNullableString("pendingUpdateStrategy"),
71
+ pendingTransition =
72
+ json.optJSONObject("pendingTransition")?.let(PendingSelectionTransition::fromJson),
45
73
  verificationPending = json.optBoolean("verificationPending", false),
74
+ highestSeenCatalogs = highestSeenCatalogs,
75
+ currentSelectionContexts = currentSelectionContexts,
46
76
  updatedAt = json.optLong("updatedAt", System.currentTimeMillis()),
47
77
  )
78
+ }
48
79
 
49
80
  fun loadFromFile(
50
81
  file: File,
@@ -85,19 +116,155 @@ data class BundleMetadata(
85
116
  put("isolationKey", isolationKey ?: JSONObject.NULL)
86
117
  put("stableBundleId", stableBundleId ?: JSONObject.NULL)
87
118
  put("stagingBundleId", stagingBundleId ?: JSONObject.NULL)
119
+ put("stableSelection", stableSelection?.toJson() ?: JSONObject.NULL)
120
+ put("stagingSelection", stagingSelection?.toJson() ?: JSONObject.NULL)
121
+ put("pendingUpdateStrategy", pendingUpdateStrategy ?: JSONObject.NULL)
122
+ put("pendingTransition", pendingTransition?.toJson() ?: JSONObject.NULL)
88
123
  put("verificationPending", verificationPending)
124
+ put(
125
+ "highestSeenCatalogs",
126
+ JSONObject().apply {
127
+ highestSeenCatalogs.toSortedMap().forEach { (key, value) ->
128
+ put(key, value.toJson())
129
+ }
130
+ },
131
+ )
132
+ put(
133
+ "currentSelectionContexts",
134
+ JSONObject().apply {
135
+ currentSelectionContexts.toSortedMap().forEach { (key, value) ->
136
+ put(key, value)
137
+ }
138
+ },
139
+ )
89
140
  put("updatedAt", updatedAt)
90
141
  }
91
142
 
92
143
  fun saveToFile(file: File): Boolean =
93
- try {
94
- file.parentFile?.mkdirs()
95
- file.writeText(toJson().toString(2))
96
- Log.d(TAG, "Saved metadata to file: ${file.absolutePath}")
97
- true
98
- } catch (e: Exception) {
99
- Log.e(TAG, "Failed to save metadata to file", e)
100
- false
144
+ AtomicFile(file).let { atomicFile ->
145
+ var output: FileOutputStream? = null
146
+ try {
147
+ file.parentFile?.mkdirs()
148
+ output = atomicFile.startWrite()
149
+ output.write(toJson().toString(2).toByteArray())
150
+ atomicFile.finishWrite(output)
151
+ Log.d(TAG, "Saved metadata to file: ${file.absolutePath}")
152
+ true
153
+ } catch (e: Exception) {
154
+ output?.let(atomicFile::failWrite)
155
+ Log.e(TAG, "Failed to save metadata to file", e)
156
+ false
157
+ }
158
+ }
159
+ }
160
+
161
+ data class CatalogHighWater(
162
+ val generation: Long,
163
+ val catalogHash: String,
164
+ ) {
165
+ companion object {
166
+ fun fromJson(json: JSONObject): CatalogHighWater =
167
+ CatalogHighWater(
168
+ generation = json.getLong("generation"),
169
+ catalogHash = json.getString("catalogHash"),
170
+ )
171
+ }
172
+
173
+ fun toJson(): JSONObject =
174
+ JSONObject().apply {
175
+ put("generation", generation)
176
+ put("catalogHash", catalogHash)
177
+ }
178
+ }
179
+
180
+ data class PersistedSelection(
181
+ val kind: String,
182
+ val releaseId: String?,
183
+ val bundleId: String,
184
+ val catalogId: String?,
185
+ val scopeKey: String?,
186
+ val generation: Long?,
187
+ val catalogHash: String?,
188
+ val channel: String,
189
+ val selectionContextHash: String?,
190
+ ) {
191
+ companion object {
192
+ fun legacyBundle(bundleId: String): PersistedSelection =
193
+ PersistedSelection(
194
+ kind = "BUNDLE",
195
+ releaseId = null,
196
+ bundleId = bundleId,
197
+ catalogId = null,
198
+ scopeKey = null,
199
+ generation = null,
200
+ catalogHash = null,
201
+ channel = "",
202
+ selectionContextHash = null,
203
+ )
204
+
205
+ fun fromJson(json: JSONObject): PersistedSelection =
206
+ PersistedSelection(
207
+ kind = json.getString("kind"),
208
+ releaseId = json.optNullableString("releaseId"),
209
+ bundleId = json.getString("bundleId"),
210
+ catalogId = json.optNullableString("catalogId"),
211
+ scopeKey = json.optNullableString("scopeKey"),
212
+ generation = if (json.has("generation") && !json.isNull("generation")) json.getLong("generation") else null,
213
+ catalogHash = json.optNullableString("catalogHash"),
214
+ channel = json.optString("channel", ""),
215
+ selectionContextHash = json.optNullableString("selectionContextHash"),
216
+ )
217
+ }
218
+
219
+ fun toJson(): JSONObject =
220
+ JSONObject().apply {
221
+ put("kind", kind)
222
+ put("releaseId", releaseId ?: JSONObject.NULL)
223
+ put("bundleId", bundleId)
224
+ put("catalogId", catalogId ?: JSONObject.NULL)
225
+ put("scopeKey", scopeKey ?: JSONObject.NULL)
226
+ put("generation", generation ?: JSONObject.NULL)
227
+ put("catalogHash", catalogHash ?: JSONObject.NULL)
228
+ put("channel", channel)
229
+ put("selectionContextHash", selectionContextHash ?: JSONObject.NULL)
230
+ }
231
+
232
+ fun toMap(): Map<String, Any?> =
233
+ mapOf(
234
+ "kind" to kind,
235
+ "releaseId" to releaseId,
236
+ "bundleId" to bundleId,
237
+ "catalogId" to catalogId,
238
+ "scopeKey" to scopeKey,
239
+ "generation" to generation?.toDouble(),
240
+ "catalogHash" to catalogHash,
241
+ "channel" to channel,
242
+ "selectionContextHash" to selectionContextHash,
243
+ )
244
+ }
245
+
246
+ data class PendingSelectionTransition(
247
+ val fromReleaseId: String?,
248
+ val fromBundleId: String,
249
+ val toReleaseId: String?,
250
+ val toBundleId: String,
251
+ ) {
252
+ companion object {
253
+ fun fromJson(json: JSONObject): PendingSelectionTransition =
254
+ PendingSelectionTransition(
255
+ fromReleaseId = json.optNullableString("fromReleaseId"),
256
+ fromBundleId = json.getString("fromBundleId"),
257
+ toReleaseId = json.optNullableString("toReleaseId"),
258
+ toBundleId = json.getString("toBundleId"),
259
+ )
260
+ }
261
+
262
+ fun toJson(): JSONObject =
263
+ JSONObject().apply {
264
+ put("fromReleaseId", fromReleaseId ?: JSONObject.NULL)
265
+ put("fromBundleId", fromBundleId)
266
+ put("toReleaseId", toReleaseId ?: JSONObject.NULL)
267
+ put("toBundleId", toBundleId)
101
268
  }
102
269
  }
103
270
 
@@ -247,8 +414,12 @@ data class LaunchSelection(
247
414
  )
248
415
 
249
416
  data class LaunchReport(
250
- val status: String = "STABLE",
251
- val crashedBundleId: String? = null,
417
+ val status: String = "UNCHANGED",
418
+ val fromReleaseId: String? = null,
419
+ val fromBundleId: String? = null,
420
+ val toReleaseId: String? = null,
421
+ val toBundleId: String? = null,
422
+ val updateStrategy: String? = null,
252
423
  ) {
253
424
  companion object {
254
425
  private const val TAG = "LaunchReport"
@@ -256,13 +427,12 @@ data class LaunchReport(
256
427
 
257
428
  fun fromJson(json: JSONObject): LaunchReport =
258
429
  LaunchReport(
259
- status = json.optString("status", "STABLE"),
260
- crashedBundleId =
261
- if (json.has("crashedBundleId") && !json.isNull("crashedBundleId")) {
262
- json.getString("crashedBundleId").takeIf { it.isNotEmpty() }
263
- } else {
264
- null
265
- },
430
+ status = json.optString("status", "UNCHANGED"),
431
+ fromReleaseId = json.optNullableString("fromReleaseId"),
432
+ fromBundleId = json.optNullableString("fromBundleId"),
433
+ toReleaseId = json.optNullableString("toReleaseId"),
434
+ toBundleId = json.optNullableString("toBundleId"),
435
+ updateStrategy = json.optNullableString("updateStrategy"),
266
436
  )
267
437
 
268
438
  fun loadFromFile(file: File): LaunchReport? =
@@ -281,7 +451,11 @@ data class LaunchReport(
281
451
  fun toJson(): JSONObject =
282
452
  JSONObject().apply {
283
453
  put("status", status)
284
- put("crashedBundleId", crashedBundleId ?: JSONObject.NULL)
454
+ put("fromReleaseId", fromReleaseId ?: JSONObject.NULL)
455
+ put("fromBundleId", fromBundleId ?: JSONObject.NULL)
456
+ put("toReleaseId", toReleaseId ?: JSONObject.NULL)
457
+ put("toBundleId", toBundleId ?: JSONObject.NULL)
458
+ put("updateStrategy", updateStrategy ?: JSONObject.NULL)
285
459
  }
286
460
 
287
461
  fun saveToFile(file: File): Boolean =
@@ -294,3 +468,63 @@ data class LaunchReport(
294
468
  false
295
469
  }
296
470
  }
471
+
472
+ data class InstallationIdentity(
473
+ val installId: String,
474
+ val userId: String? = null,
475
+ val username: String? = null,
476
+ ) {
477
+ companion object {
478
+ private const val TAG = "InstallationIdentity"
479
+ const val IDENTITY_FILENAME = "identity.json"
480
+
481
+ fun fromJson(json: JSONObject): InstallationIdentity =
482
+ InstallationIdentity(
483
+ installId = json.getString("installId"),
484
+ userId = json.optNullableString("userId"),
485
+ username = json.optNullableString("username"),
486
+ )
487
+
488
+ fun loadFromFile(file: File): InstallationIdentity? =
489
+ try {
490
+ AtomicFile(file).openRead().bufferedReader().use { reader ->
491
+ fromJson(JSONObject(reader.readText()))
492
+ }
493
+ } catch (_: FileNotFoundException) {
494
+ null
495
+ } catch (e: Exception) {
496
+ Log.e(TAG, "Failed to load installation identity", e)
497
+ null
498
+ }
499
+ }
500
+
501
+ fun toJson(): JSONObject =
502
+ JSONObject().apply {
503
+ put("installId", installId)
504
+ put("userId", userId ?: JSONObject.NULL)
505
+ put("username", username ?: JSONObject.NULL)
506
+ }
507
+
508
+ fun saveToFile(file: File): Boolean =
509
+ AtomicFile(file).let { atomicFile ->
510
+ var output: FileOutputStream? = null
511
+ try {
512
+ file.parentFile?.mkdirs()
513
+ output = atomicFile.startWrite()
514
+ output.write(toJson().toString(2).toByteArray())
515
+ atomicFile.finishWrite(output)
516
+ true
517
+ } catch (e: Exception) {
518
+ output?.let(atomicFile::failWrite)
519
+ Log.e(TAG, "Failed to save installation identity", e)
520
+ false
521
+ }
522
+ }
523
+ }
524
+
525
+ private fun JSONObject.optNullableString(key: String): String? =
526
+ if (has(key) && !isNull(key)) {
527
+ getString(key).takeIf { it.isNotEmpty() }
528
+ } else {
529
+ null
530
+ }
@@ -7,6 +7,7 @@ import android.os.Process
7
7
  import android.util.Log
8
8
  import kotlinx.coroutines.Dispatchers
9
9
  import kotlinx.coroutines.withContext
10
+ import java.io.File
10
11
 
11
12
  /**
12
13
  * Core implementation class for HotUpdater functionality
@@ -16,6 +17,7 @@ class HotUpdaterImpl {
16
17
  private val bundleStorage: BundleStorageService
17
18
  private val preferences: PreferencesService
18
19
  private val recoveryManager: HotUpdaterRecoveryManager
20
+ private val releaseCatalogCache: ReleaseCatalogCacheService
19
21
  private var currentLaunchSelection: LaunchSelection? = null
20
22
 
21
23
  /**
@@ -30,6 +32,10 @@ class HotUpdaterImpl {
30
32
  this.bundleStorage = bundleStorage
31
33
  this.preferences = preferences
32
34
  this.recoveryManager = HotUpdaterRecoveryManager(this.context)
35
+ this.releaseCatalogCache =
36
+ ReleaseCatalogCacheService(
37
+ File(this.context.noBackupFilesDir, "hot-updater-release-catalog-cache"),
38
+ )
33
39
  }
34
40
 
35
41
  /**
@@ -148,7 +154,6 @@ class HotUpdaterImpl {
148
154
  NativeConfigUtils.getString(
149
155
  context,
150
156
  NativeConfigUtils.CHANNEL_META_DATA_KEY,
151
- "hot_updater_channel",
152
157
  ) ?: DEFAULT_CHANNEL
153
158
 
154
159
  /**
@@ -220,7 +225,6 @@ class HotUpdaterImpl {
220
225
  NativeConfigUtils.getString(
221
226
  context,
222
227
  NativeConfigUtils.FINGERPRINT_HASH_META_DATA_KEY,
223
- "hot_updater_fingerprint_hash",
224
228
  )
225
229
  }
226
230
 
@@ -232,7 +236,6 @@ class HotUpdaterImpl {
232
236
  NativeConfigUtils.getString(
233
237
  context,
234
238
  NativeConfigUtils.FINGERPRINT_HASH_META_DATA_KEY,
235
- "hot_updater_fingerprint_hash",
236
239
  )
237
240
 
238
241
  /**
@@ -240,6 +243,11 @@ class HotUpdaterImpl {
240
243
  * @return The channel name or null if not set
241
244
  */
242
245
  fun getChannel(): String {
246
+ val activeSelection = bundleStorage.getActiveUpdateState()["activeSelection"] as? Map<*, *>
247
+ val receiptChannel = activeSelection?.get("channel") as? String
248
+ if (!receiptChannel.isNullOrEmpty()) {
249
+ return receiptChannel
250
+ }
243
251
  val overriddenChannel = preferences.getItem(CHANNEL_STORAGE_KEY)
244
252
  if (!overriddenChannel.isNullOrEmpty()) {
245
253
  return overriddenChannel
@@ -248,7 +256,6 @@ class HotUpdaterImpl {
248
256
  return NativeConfigUtils.getString(
249
257
  context,
250
258
  NativeConfigUtils.CHANNEL_META_DATA_KEY,
251
- "hot_updater_channel",
252
259
  ) ?: DEFAULT_CHANNEL
253
260
  }
254
261
 
@@ -276,8 +283,12 @@ class HotUpdaterImpl {
276
283
  manifestFileHash: String?,
277
284
  changedAssets: Map<String, ChangedAssetDescriptor>?,
278
285
  channel: String?,
286
+ selection: PersistedSelection? = null,
279
287
  progressCallback: (UpdateProgressPayload) -> Unit,
280
288
  ) {
289
+ if (selection != null && !bundleStorage.stageReleaseSelection(selection)) {
290
+ throw IllegalStateException("Release catalog selection is stale")
291
+ }
281
292
  bundleStorage.updateBundle(
282
293
  bundleId,
283
294
  fileUrl,
@@ -363,13 +374,68 @@ class HotUpdaterImpl {
363
374
  }
364
375
  }
365
376
 
366
- /**
367
- * Returns the launch report for the current process.
368
- * Startup success and rollback are finalized before JS reads it.
369
- * @return Map containing status and optional crashedBundleId
370
- */
371
377
  fun notifyAppReady(): Map<String, Any?> = bundleStorage.notifyAppReady()
372
378
 
379
+ fun acceptReleaseCatalog(
380
+ catalogId: String,
381
+ scopeKey: String,
382
+ generation: Long,
383
+ catalogHash: String,
384
+ channel: String,
385
+ selectionContextHash: String,
386
+ ): Boolean =
387
+ bundleStorage.acceptReleaseCatalog(
388
+ catalogId,
389
+ scopeKey,
390
+ generation,
391
+ catalogHash,
392
+ channel,
393
+ selectionContextHash,
394
+ )
395
+
396
+ fun getActiveUpdateState(): Map<String, Any?> = bundleStorage.getActiveUpdateState()
397
+
398
+ fun getReleaseCatalogCache(partition: String): String? = releaseCatalogCache.get(partition)
399
+
400
+ fun setReleaseCatalogCache(
401
+ partition: String,
402
+ payload: String,
403
+ ): Boolean = releaseCatalogCache.set(partition, payload)
404
+
405
+ fun removeReleaseCatalogCache(partition: String): Boolean = releaseCatalogCache.remove(partition)
406
+
407
+ fun isReleaseSelectionCurrent(
408
+ catalogId: String,
409
+ scopeKey: String,
410
+ generation: Long,
411
+ catalogHash: String,
412
+ channel: String,
413
+ selectionContextHash: String,
414
+ ): Boolean =
415
+ bundleStorage.isReleaseSelectionCurrent(
416
+ catalogId,
417
+ scopeKey,
418
+ generation,
419
+ catalogHash,
420
+ channel,
421
+ selectionContextHash,
422
+ )
423
+
424
+ suspend fun commitReleaseSelection(selection: PersistedSelection): Boolean = bundleStorage.commitReleaseSelection(selection)
425
+
426
+ fun getInstallId(): String = bundleStorage.getInstallId()
427
+
428
+ fun getUserId(): String? = bundleStorage.getUserId()
429
+
430
+ fun getUsername(): String? = bundleStorage.getUsername()
431
+
432
+ fun setUser(
433
+ userId: String?,
434
+ username: String?,
435
+ ) {
436
+ bundleStorage.setUser(userId, username)
437
+ }
438
+
373
439
  /**
374
440
  * Gets the crashed bundle history.
375
441
  * @return List of crashed bundle IDs
@@ -12,10 +12,9 @@ object NativeConfigUtils {
12
12
  fun getString(
13
13
  context: Context,
14
14
  metaDataKey: String,
15
- stringResourceName: String,
16
15
  ): String? {
17
- // Programmatic overrides take priority over manifest metadata and string
18
- // resources (brownfield/AAR setups, see HotUpdaterConfig).
16
+ // Programmatic overrides take priority over manifest metadata
17
+ // (brownfield/AAR setups, see HotUpdaterConfig).
19
18
  val override =
20
19
  when (metaDataKey) {
21
20
  FINGERPRINT_HASH_META_DATA_KEY -> HotUpdaterConfig.fingerprintHash
@@ -26,7 +25,6 @@ object NativeConfigUtils {
26
25
  override?.takeIf { it.isNotEmpty() }?.let { return it }
27
26
 
28
27
  return getManifestMetaDataString(context, metaDataKey)
29
- ?: getStringResource(context, stringResourceName)
30
28
  }
31
29
 
32
30
  private fun getManifestMetaDataString(
@@ -59,16 +57,4 @@ object NativeConfigUtils {
59
57
  value
60
58
  ?.toString()
61
59
  ?.takeIf { it.isNotEmpty() }
62
-
63
- private fun getStringResource(
64
- context: Context,
65
- name: String,
66
- ): String? {
67
- val id = StringResourceUtils.getIdentifier(context, name)
68
- return if (id != 0) {
69
- context.getString(id).takeIf { it.isNotEmpty() }
70
- } else {
71
- null
72
- }
73
- }
74
60
  }