@hot-updater/react-native 1.0.7 → 1.1.0

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 (144) hide show
  1. package/HotUpdater.podspec +14 -2
  2. package/Package.swift +24 -3
  3. package/android/build.gradle +2 -6
  4. package/android/hot-updater-fingerprint.gradle +135 -0
  5. package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
  6. package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
  7. package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
  8. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
  9. package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
  10. package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
  11. package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
  12. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
  13. package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
  14. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
  15. package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
  16. package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
  17. package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
  18. package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
  19. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +492 -0
  20. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
  21. package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
  22. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
  23. package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
  24. package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
  25. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
  26. package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
  27. package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
  28. package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
  29. package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
  30. package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
  31. package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
  32. package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
  33. package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
  34. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
  35. package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
  36. package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
  37. package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
  38. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
  39. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
  40. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
  41. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
  42. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
  43. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
  44. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
  45. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
  46. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
  47. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
  48. package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
  49. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
  50. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
  51. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
  52. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
  53. package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
  54. package/android/src/newarch/HotUpdater.kt +53 -15
  55. package/android/src/newarch/HotUpdaterModule.kt +42 -17
  56. package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
  57. package/android/src/oldarch/HotUpdater.kt +51 -12
  58. package/android/src/oldarch/HotUpdaterModule.kt +43 -17
  59. package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
  60. package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
  61. package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
  62. package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
  63. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
  64. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
  65. package/ios/HotUpdater/Internal/CohortService.swift +8 -0
  66. package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
  67. package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
  68. package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
  69. package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
  70. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
  71. package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
  72. package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
  73. package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
  74. package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
  75. package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
  76. package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
  77. package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
  78. package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
  79. package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
  80. package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
  81. package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
  82. package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
  83. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
  84. package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
  85. package/ios/HotUpdater/Package.swift +11 -0
  86. package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
  87. package/ios/HotUpdater/Public/HotUpdater.h +21 -2
  88. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
  89. package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
  90. package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
  91. package/lib/commonjs/checkForUpdate.js +3 -1
  92. package/lib/commonjs/checkForUpdate.js.map +1 -1
  93. package/lib/commonjs/index.js +9 -12
  94. package/lib/commonjs/index.js.map +1 -1
  95. package/lib/commonjs/native.js +24 -79
  96. package/lib/commonjs/native.js.map +1 -1
  97. package/lib/commonjs/nativeUpdateEvents.js +66 -0
  98. package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
  99. package/lib/commonjs/sdkVersion.js +1 -1
  100. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  101. package/lib/module/checkForUpdate.js +3 -1
  102. package/lib/module/checkForUpdate.js.map +1 -1
  103. package/lib/module/index.js +10 -13
  104. package/lib/module/index.js.map +1 -1
  105. package/lib/module/native.js +19 -73
  106. package/lib/module/native.js.map +1 -1
  107. package/lib/module/nativeUpdateEvents.js +61 -0
  108. package/lib/module/nativeUpdateEvents.js.map +1 -0
  109. package/lib/module/sdkVersion.js +1 -1
  110. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  111. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/index.d.ts +4 -14
  113. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/native.d.ts +9 -46
  115. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
  117. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  119. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
  120. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  121. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  122. package/lib/typescript/module/index.d.ts +4 -14
  123. package/lib/typescript/module/index.d.ts.map +1 -1
  124. package/lib/typescript/module/native.d.ts +9 -46
  125. package/lib/typescript/module/native.d.ts.map +1 -1
  126. package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
  127. package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
  128. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  129. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
  130. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  131. package/package.json +4 -4
  132. package/src/checkForUpdate.ts +7 -1
  133. package/src/index.ts +9 -17
  134. package/src/native.ts +19 -91
  135. package/src/nativeUpdateEvents.ts +77 -0
  136. package/src/sdkVersion.ts +1 -1
  137. package/src/specs/NativeHotUpdater.ts +20 -3
  138. package/android/libs/README.md +0 -50
  139. package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
  140. package/android/src/bridge/BridgeReactInstance.kt +0 -38
  141. package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
  142. package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
  143. package/android/src/newarch/ReactIntegrationManager.kt +0 -185
  144. package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
@@ -3,56 +3,140 @@ package com.hotupdater
3
3
  import android.app.Activity
4
4
  import android.app.Application
5
5
  import android.os.Bundle
6
+ import android.view.View
7
+ import android.view.ViewGroup
8
+ import android.view.ViewTreeObserver
6
9
  import androidx.lifecycle.Lifecycle
10
+ import androidx.lifecycle.LifecycleOwner
7
11
  import androidx.lifecycle.ProcessLifecycleOwner
12
+ import com.facebook.react.ReactRootView
8
13
 
9
14
  /**
10
- * Runs an action once this process has a started activity.
15
+ * Runs an action once a started activity of this process shows a React Native
16
+ * view.
11
17
  *
12
18
  * A headless JS task (HeadlessJsTaskService, which background push libraries
13
- * start) loads the bundle with no activity and never renders. Waiting for an
14
- * activity keeps that launch from counting as one that ended before its first
15
- * render.
19
+ * start) loads the bundle with no activity and never renders. A brownfield app
20
+ * loads the bundle with its first native screen, and the user may never open a
21
+ * React Native screen in that process. Waiting for a React Native view keeps
22
+ * both launches from counting as one that ended before its first render.
16
23
  *
17
24
  * Android does not replay lifecycle callbacks to a late registration. An
18
25
  * activity that started before this gate existed, as when a brownfield app adds
19
26
  * React Native to an activity already on screen, is read from
20
- * [processHasStartedActivity] instead.
27
+ * [processHasStartedActivity] instead, and counts at once. A gate that knows
28
+ * those activities ([activitiesStartedBefore]) checks them for a React Native
29
+ * view instead, as a named instance's gate must: a started activity says
30
+ * nothing about whether that instance's view is shown.
21
31
  */
22
32
  internal class ActivityStartGate(
23
33
  private val application: Application?,
24
34
  private val processHasStartedActivity: () -> Boolean = ::hasStartedActivity,
35
+ private val showsReactView: (Activity) -> Boolean = ::hasReactRootView,
36
+ private val watchLayout: (Activity, () -> Unit) -> () -> Unit = ::watchGlobalLayout,
37
+ private val activitiesStartedBefore: (() -> Collection<Activity>)? = null,
38
+ private val isStarted: (Activity) -> Boolean = { isActivityStarted(it) },
39
+ private val mainThread: HotUpdaterMainThread = HotUpdaterMainThread.android,
40
+ /**
41
+ * Runs on the main thread whenever this gate is about to look at an
42
+ * activity's views, before its action may run: a view now found there
43
+ * may be one whose first content appeared before it could be found, which
44
+ * verifies its launch instead of arming it.
45
+ */
46
+ private val beforeCheckingViews: () -> Unit = {},
25
47
  ) : Application.ActivityLifecycleCallbacks {
26
- private var startedActivities = 0
48
+ private val startedActivities = mutableSetOf<Activity>()
49
+ private val layoutWatches = mutableMapOf<Activity, () -> Unit>()
27
50
  private var pendingAction: (() -> Unit)? = null
28
51
 
52
+ // Counts the requests in the order they were made. A request made off the
53
+ // main thread runs later, and must not replace a newer one made there.
54
+ private var requests = 0L
55
+
29
56
  init {
30
57
  application?.registerActivityLifecycleCallbacks(this)
31
58
  }
32
59
 
33
60
  /**
34
- * Runs [action] now when an activity is started, otherwise at the next
35
- * activity start. It replaces an action still waiting; null cancels it.
36
- * Without an application to observe, it runs [action] now.
61
+ * Runs [action] now when a started activity shows a React Native view,
62
+ * otherwise when one does. It replaces an action still waiting; null
63
+ * cancels it. Without an application to observe, it runs [action] now.
37
64
  */
38
65
  fun runWhenActivityStarted(action: (() -> Unit)?) {
66
+ val request = synchronized(this) { ++requests }
67
+ // Views and their layout listeners belong to the main thread.
68
+ if (!mainThread.isCurrent()) {
69
+ mainThread.post { runWhenActivityStarted(request, action) }
70
+ return
71
+ }
72
+ runWhenActivityStarted(request, action)
73
+ }
74
+
75
+ private fun runWhenActivityStarted(
76
+ request: Long,
77
+ action: (() -> Unit)?,
78
+ ) {
79
+ beforeCheckingViews()
39
80
  val runNow =
40
81
  synchronized(this) {
82
+ if (request != requests) return
83
+ stopWatching()
84
+ pendingAction = null
85
+ if (application != null) activitiesStartedBefore?.let { startedActivities += it() }
41
86
  val waits =
42
87
  application != null &&
43
- startedActivities == 0 &&
44
- !processHasStartedActivity()
45
- pendingAction = action.takeIf { waits }
88
+ startedActivities.none(showsReactView) &&
89
+ (startedActivities.isNotEmpty() || activitiesStartedBefore != null || !processHasStartedActivity())
90
+ if (waits && action != null) {
91
+ pendingAction = action
92
+ startedActivities.forEach(::watch)
93
+ }
46
94
  action.takeUnless { waits }
47
95
  }
48
96
  runNow?.invoke()
49
97
  }
50
98
 
99
+ /**
100
+ * Counts [activity], which this gate may never have seen start, as
101
+ * started when it is, and while an action waits, checks it for a React
102
+ * Native view and watches its layout for one. A named instance's runtime
103
+ * hands over its activity when its module is created.
104
+ */
105
+ fun watchActivity(activity: Activity) {
106
+ if (!mainThread.isCurrent()) {
107
+ mainThread.post { watchActivity(activity) }
108
+ return
109
+ }
110
+ beforeCheckingViews()
111
+ val action =
112
+ synchronized(this) {
113
+ // A runtime keeps its activity after it stops, as when the app
114
+ // goes to the background while the bundle loads. One that
115
+ // isn't started now counts when this gate sees it start.
116
+ if (application == null || !isStarted(activity)) return
117
+ // Counted until it stops, as one this gate saw start.
118
+ startedActivities += activity
119
+ if (pendingAction == null) return
120
+ if (!showsReactView(activity)) {
121
+ watch(activity)
122
+ return
123
+ }
124
+ takePendingAction()
125
+ }
126
+ action?.invoke()
127
+ }
128
+
51
129
  override fun onActivityStarted(activity: Activity) {
130
+ beforeCheckingViews()
52
131
  val action =
53
132
  synchronized(this) {
54
- startedActivities += 1
55
- pendingAction.also { pendingAction = null }
133
+ startedActivities += activity
134
+ if (pendingAction == null) return
135
+ if (!showsReactView(activity)) {
136
+ watch(activity)
137
+ return
138
+ }
139
+ takePendingAction()
56
140
  }
57
141
  action?.invoke()
58
142
  }
@@ -60,10 +144,41 @@ internal class ActivityStartGate(
60
144
  override fun onActivityStopped(activity: Activity) {
61
145
  synchronized(this) {
62
146
  // An activity started before this gate existed was never counted.
63
- if (startedActivities > 0) startedActivities -= 1
147
+ startedActivities -= activity
148
+ layoutWatches.remove(activity)?.invoke()
64
149
  }
65
150
  }
66
151
 
152
+ private fun watch(activity: Activity) {
153
+ if (activity in layoutWatches) return
154
+ layoutWatches[activity] = watchLayout(activity) { onLayout(activity) }
155
+ }
156
+
157
+ private fun onLayout(activity: Activity) {
158
+ beforeCheckingViews()
159
+ val action =
160
+ synchronized(this) {
161
+ if (pendingAction == null ||
162
+ activity !in startedActivities ||
163
+ !showsReactView(activity)
164
+ ) {
165
+ return
166
+ }
167
+ takePendingAction()
168
+ }
169
+ action?.invoke()
170
+ }
171
+
172
+ private fun takePendingAction(): (() -> Unit)? {
173
+ stopWatching()
174
+ return pendingAction.also { pendingAction = null }
175
+ }
176
+
177
+ private fun stopWatching() {
178
+ layoutWatches.values.forEach { it() }
179
+ layoutWatches.clear()
180
+ }
181
+
67
182
  override fun onActivityCreated(
68
183
  activity: Activity,
69
184
  savedInstanceState: Bundle?,
@@ -92,3 +207,49 @@ private fun hasStartedActivity(): Boolean =
92
207
  .lifecycle.currentState
93
208
  .isAtLeast(Lifecycle.State.STARTED)
94
209
  }.getOrDefault(false)
210
+
211
+ /**
212
+ * Whether [activity] is started now. An activity that reports its lifecycle
213
+ * (ComponentActivity, which ReactActivity and AppCompatActivity are) says so.
214
+ * Any other, such as a plain Activity hosting a ReactRootView itself, is
215
+ * started while its window is shown: the framework hides a stopped
216
+ * activity's window.
217
+ */
218
+ internal fun isActivityStarted(
219
+ activity: Activity,
220
+ windowShown: (Activity) -> Boolean = ::isWindowShown,
221
+ ): Boolean {
222
+ if (activity.isFinishing || activity.isDestroyed) return false
223
+ val owner = activity as? LifecycleOwner ?: return windowShown(activity)
224
+ return runCatching { owner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) }.getOrDefault(false)
225
+ }
226
+
227
+ private fun isWindowShown(activity: Activity): Boolean {
228
+ val decorView = activity.window?.peekDecorView() ?: return false
229
+ return decorView.isAttachedToWindow && decorView.windowVisibility == View.VISIBLE
230
+ }
231
+
232
+ private fun hasReactRootView(activity: Activity): Boolean = activity.window?.peekDecorView()?.let(::containsReactRootView) ?: false
233
+
234
+ private fun containsReactRootView(view: View): Boolean =
235
+ view is ReactRootView ||
236
+ (view is ViewGroup && (0 until view.childCount).any { containsReactRootView(view.getChildAt(it)) })
237
+
238
+ // A React Native screen added later, such as a fragment of a native activity,
239
+ // changes the activity's layout.
240
+ // A dialog or presentation shows its views in a window of its own, whose
241
+ // layout this activity's never sees; the activity's window loses focus to it.
242
+ private fun watchGlobalLayout(
243
+ activity: Activity,
244
+ onLayout: () -> Unit,
245
+ ): () -> Unit {
246
+ val decorView = activity.window?.decorView ?: return {}
247
+ val listener = ViewTreeObserver.OnGlobalLayoutListener(onLayout)
248
+ val focusListener = ViewTreeObserver.OnWindowFocusChangeListener { onLayout() }
249
+ decorView.viewTreeObserver.addOnGlobalLayoutListener(listener)
250
+ decorView.viewTreeObserver.addOnWindowFocusChangeListener(focusListener)
251
+ return {
252
+ runCatching { decorView.viewTreeObserver.removeOnGlobalLayoutListener(listener) }
253
+ runCatching { decorView.viewTreeObserver.removeOnWindowFocusChangeListener(focusListener) }
254
+ }
255
+ }
@@ -32,12 +32,14 @@ interface ArtifactFileResolver {
32
32
 
33
33
  /**
34
34
  * POSTs `{ paths }` to the files URL the SDK composed, with the update check's
35
- * headers and request timeout.
35
+ * headers and request timeout. [onResponse] receives each response's status
36
+ * and body, or a null body when it could not be read.
36
37
  */
37
38
  class HttpArtifactFileResolver(
38
39
  private val filesUrl: String,
39
40
  private val headers: Map<String, String>,
40
41
  private val timeoutMs: Long = DEFAULT_TIMEOUT_MS,
42
+ private val onResponse: ((status: Int, body: String?) -> Unit)? = null,
41
43
  ) : ArtifactFileResolver {
42
44
  override suspend fun resolve(paths: List<String>): Map<String, ResolvedArtifactFile> =
43
45
  withContext(Dispatchers.IO) {
@@ -57,20 +59,30 @@ class HttpArtifactFileResolver(
57
59
  .newCall(request)
58
60
  .execute()
59
61
  .use { response ->
62
+ val body =
63
+ try {
64
+ response.body?.string()
65
+ } catch (error: java.io.IOException) {
66
+ onResponse?.invoke(response.code, null)
67
+ throw error
68
+ }
69
+ onResponse?.invoke(response.code, body)
60
70
  if (!response.isSuccessful) {
61
71
  throw HttpStatusException(response.code, response.message)
62
72
  }
73
+ // As JSON.parse reads it, which org.json's parser does not.
63
74
  val files =
64
- JSONObject(response.body?.string() ?: "").optJSONObject("files")
65
- ?: throw IllegalStateException("Artifact files response has no files")
75
+ (StrictJson.parse(body ?: "") as? JSONObject)?.opt("files") as? JSONObject
76
+ ?: throw InvalidUpdateResponseException("Artifact files response has no files")
66
77
  paths.associateWith { path ->
67
78
  val file =
68
- files.optJSONObject(path)
69
- ?: throw IllegalStateException("Artifact files response omits $path")
79
+ files.opt(path) as? JSONObject
80
+ ?: throw InvalidUpdateResponseException("Artifact files response omits $path")
70
81
  ResolvedArtifactFile(
71
- url = file.getString("url"),
72
- compression =
73
- if (file.has("compression") && !file.isNull("compression")) file.getString("compression") else null,
82
+ url =
83
+ file.opt("url") as? String
84
+ ?: throw InvalidUpdateResponseException("Artifact files response has no URL for $path"),
85
+ compression = file.opt("compression") as? String,
74
86
  )
75
87
  }
76
88
  }