@sigx/lynx-updates 0.8.0 → 0.8.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.
- package/LICENSE +21 -21
- package/README.md +199 -199
- package/android/com/sigx/updates/UpdateDownloader.kt +154 -154
- package/android/com/sigx/updates/UpdateStore.kt +367 -367
- package/android/com/sigx/updates/UpdatesActivityHook.kt +25 -25
- package/android/com/sigx/updates/UpdatesBundleResolver.kt +18 -18
- package/android/com/sigx/updates/UpdatesEventBus.kt +54 -54
- package/android/com/sigx/updates/UpdatesLifecyclePublisher.kt +42 -42
- package/android/com/sigx/updates/UpdatesModule.kt +235 -235
- package/ios/UpdateDownloader.swift +152 -152
- package/ios/UpdateStore.swift +286 -286
- package/ios/UpdatesBundleResolver.swift +15 -15
- package/ios/UpdatesEventBus.swift +59 -59
- package/ios/UpdatesLifecyclePublisher.swift +48 -48
- package/ios/UpdatesModule.swift +178 -178
- package/package.json +3 -3
- package/signalx-module.json +35 -35
|
@@ -1,235 +1,235 @@
|
|
|
1
|
-
package com.sigx.updates
|
|
2
|
-
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import android.os.Handler
|
|
5
|
-
import android.os.Looper
|
|
6
|
-
import android.util.Log
|
|
7
|
-
import com.lynx.jsbridge.LynxMethod
|
|
8
|
-
import com.lynx.jsbridge.LynxModule
|
|
9
|
-
import com.lynx.react.bridge.Callback
|
|
10
|
-
import com.lynx.react.bridge.JavaOnlyMap
|
|
11
|
-
import com.lynx.react.bridge.ReadableMap
|
|
12
|
-
import com.lynx.tasm.TemplateData
|
|
13
|
-
import org.json.JSONObject
|
|
14
|
-
import java.io.File
|
|
15
|
-
import java.util.concurrent.Executors
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* JS bridge for OTA updates.
|
|
19
|
-
* JS usage: NativeModules.Updates.downloadUpdate({...}, callback)
|
|
20
|
-
*
|
|
21
|
-
* Downloads run on a single-thread executor (single-flight is also enforced
|
|
22
|
-
* in [UpdateDownloader]); everything else is cheap file/state work.
|
|
23
|
-
*/
|
|
24
|
-
class UpdatesModule(context: Context) : LynxModule(context) {
|
|
25
|
-
|
|
26
|
-
companion object {
|
|
27
|
-
private const val TAG = "SigxUpdates"
|
|
28
|
-
private val executor = Executors.newSingleThreadExecutor { r ->
|
|
29
|
-
Thread(r, "sigx-updates").apply { isDaemon = true }
|
|
30
|
-
}
|
|
31
|
-
private val mainHandler = Handler(Looper.getMainLooper())
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
private fun error(message: String, code: String? = null): JavaOnlyMap {
|
|
35
|
-
val map = JavaOnlyMap()
|
|
36
|
-
map.putString("error", message)
|
|
37
|
-
if (code != null) map.putString("code", code)
|
|
38
|
-
return map
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
private fun ok(): JavaOnlyMap {
|
|
42
|
-
val map = JavaOnlyMap()
|
|
43
|
-
map.putBoolean("ok", true)
|
|
44
|
-
return map
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
@LynxMethod
|
|
48
|
-
fun getInstalledRuntimeVersion(): String {
|
|
49
|
-
return UpdateStore.installedRuntimeVersion(mContext)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
@LynxMethod
|
|
53
|
-
fun getPlatform(): String = "android"
|
|
54
|
-
|
|
55
|
-
@LynxMethod
|
|
56
|
-
fun getCurrentUpdate(callback: Callback?) {
|
|
57
|
-
try {
|
|
58
|
-
val map = JavaOnlyMap()
|
|
59
|
-
val updateId = UpdateStore.launchedUpdateId
|
|
60
|
-
if (updateId != null) {
|
|
61
|
-
map.putString("updateId", updateId)
|
|
62
|
-
map.putBoolean("isEmbedded", false)
|
|
63
|
-
val metaFile = UpdateStore.updateJsonFile(mContext, updateId)
|
|
64
|
-
if (metaFile.exists()) {
|
|
65
|
-
try {
|
|
66
|
-
val meta = JSONObject(metaFile.readText())
|
|
67
|
-
map.putString("version", meta.optString("version"))
|
|
68
|
-
} catch (_: Exception) { /* metadata is best-effort */ }
|
|
69
|
-
}
|
|
70
|
-
} else {
|
|
71
|
-
map.putBoolean("isEmbedded", true)
|
|
72
|
-
}
|
|
73
|
-
map.putString("runtimeVersion", UpdateStore.installedRuntimeVersion(mContext))
|
|
74
|
-
// Store-shipped app version — providers receive it as
|
|
75
|
-
// UpdateCheckContext.embeddedVersion.
|
|
76
|
-
val versionName = runCatching {
|
|
77
|
-
mContext.packageManager.getPackageInfo(mContext.packageName, 0).versionName
|
|
78
|
-
}.getOrNull()
|
|
79
|
-
map.putString("embeddedVersion", versionName ?: "")
|
|
80
|
-
map.putBoolean("isFirstLaunchAfterUpdate", UpdateStore.isFirstLaunchAfterUpdate)
|
|
81
|
-
map.putBoolean("didRollBack", UpdateStore.didRollBack)
|
|
82
|
-
UpdateStore.rolledBackUpdateId?.let { map.putString("rolledBackUpdateId", it) }
|
|
83
|
-
callback?.invoke(map)
|
|
84
|
-
} catch (e: Exception) {
|
|
85
|
-
callback?.invoke(error(e.message ?: "getCurrentUpdate failed"))
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
@LynxMethod
|
|
90
|
-
fun getState(callback: Callback?) {
|
|
91
|
-
try {
|
|
92
|
-
val state = UpdateStore.readState(mContext)
|
|
93
|
-
val map = JavaOnlyMap()
|
|
94
|
-
if (state != null) {
|
|
95
|
-
map.putString("currentUpdateId", state.currentUpdateId ?: "")
|
|
96
|
-
map.putString("previousUpdateId", state.previousUpdateId ?: "")
|
|
97
|
-
map.putString("pendingUpdateId", state.pendingUpdateId ?: "")
|
|
98
|
-
map.putInt("pendingLaunchAttempts", state.pendingLaunchAttempts)
|
|
99
|
-
map.putInt("maxLaunchAttempts", state.maxLaunchAttempts)
|
|
100
|
-
map.putString("lastRollbackUpdateId", state.lastRollbackUpdateId ?: "")
|
|
101
|
-
map.putString("lastRollbackReason", state.lastRollbackReason ?: "")
|
|
102
|
-
}
|
|
103
|
-
map.putString("runningUpdateId", UpdateStore.launchedUpdateId ?: "")
|
|
104
|
-
callback?.invoke(map)
|
|
105
|
-
} catch (e: Exception) {
|
|
106
|
-
callback?.invoke(error(e.message ?: "getState failed"))
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
@LynxMethod
|
|
111
|
-
fun downloadUpdate(params: ReadableMap?, callback: Callback?) {
|
|
112
|
-
val url = params?.getString("url")
|
|
113
|
-
val sha256 = params?.getString("sha256")
|
|
114
|
-
val updateId = params?.getString("updateId")
|
|
115
|
-
val runtimeVersion = params?.getString("runtimeVersion")
|
|
116
|
-
val manifestJson = try { params?.getString("manifestJson") } catch (_: Exception) { null }
|
|
117
|
-
if (url.isNullOrEmpty() || sha256.isNullOrEmpty() || updateId.isNullOrEmpty()) {
|
|
118
|
-
callback?.invoke(error("url, sha256 and updateId are required"))
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Refuse incompatible bundles at the door — defense in depth on top
|
|
123
|
-
// of the JS-side check.
|
|
124
|
-
val installed = UpdateStore.installedRuntimeVersion(mContext)
|
|
125
|
-
if (!runtimeVersion.isNullOrEmpty() && runtimeVersion != installed) {
|
|
126
|
-
callback?.invoke(error(
|
|
127
|
-
"Update requires runtime $runtimeVersion but binary is $installed",
|
|
128
|
-
"E_RUNTIME_MISMATCH",
|
|
129
|
-
))
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
val headers = mutableMapOf<String, String>()
|
|
134
|
-
try {
|
|
135
|
-
val headerMap = params.getMap("headers")
|
|
136
|
-
if (headerMap != null) {
|
|
137
|
-
val it = headerMap.keySetIterator()
|
|
138
|
-
while (it.hasNextKey()) {
|
|
139
|
-
val key = it.nextKey()
|
|
140
|
-
headerMap.getString(key)?.let { v -> headers[key] = v }
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
} catch (_: Exception) { /* headers optional */ }
|
|
144
|
-
|
|
145
|
-
executor.execute {
|
|
146
|
-
val result = UpdateDownloader.download(
|
|
147
|
-
mContext, url, sha256, updateId, headers, manifestJson ?: "{}")
|
|
148
|
-
if (result == null) {
|
|
149
|
-
callback?.invoke(ok())
|
|
150
|
-
} else {
|
|
151
|
-
val code = when {
|
|
152
|
-
result.startsWith("E_DOWNLOAD_IN_PROGRESS") -> "E_DOWNLOAD_IN_PROGRESS"
|
|
153
|
-
result.startsWith("E_HASH_MISMATCH") -> "hash-mismatch"
|
|
154
|
-
else -> null
|
|
155
|
-
}
|
|
156
|
-
callback?.invoke(error(result, code))
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
@LynxMethod
|
|
162
|
-
fun applyOnNextLaunch(updateId: String?, callback: Callback?) {
|
|
163
|
-
if (updateId.isNullOrEmpty()) {
|
|
164
|
-
callback?.invoke(error("updateId is required"))
|
|
165
|
-
return
|
|
166
|
-
}
|
|
167
|
-
val result = UpdateStore.stagePending(mContext, updateId)
|
|
168
|
-
callback?.invoke(if (result == null) ok() else error(result))
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
@LynxMethod
|
|
172
|
-
fun applyNow(updateId: String?, callback: Callback?) {
|
|
173
|
-
if (updateId.isNullOrEmpty()) {
|
|
174
|
-
callback?.invoke(error("updateId is required"))
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
val bundle = UpdateStore.bundleFile(mContext, updateId)
|
|
178
|
-
if (!bundle.exists()) {
|
|
179
|
-
callback?.invoke(error("Update $updateId is not on disk"))
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
val view = UpdateStore.currentView()
|
|
183
|
-
if (view == null) {
|
|
184
|
-
callback?.invoke(error("No LynxView attached — cannot reload in place", "E_NO_VIEW"))
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
|
-
// Stage first so a crash mid-reload still gets crash-guarded rollback
|
|
188
|
-
// (the reload bypasses the startup resolver).
|
|
189
|
-
UpdateStore.stagePending(mContext, updateId)
|
|
190
|
-
UpdateStore.recordReloadAttempt(mContext, updateId)
|
|
191
|
-
mainHandler.post {
|
|
192
|
-
try {
|
|
193
|
-
val bytes = File(bundle.absolutePath).readBytes()
|
|
194
|
-
view.reloadAndInit()
|
|
195
|
-
view.renderTemplateWithBaseUrl(bytes, TemplateData.empty(), "file://${bundle.absolutePath}")
|
|
196
|
-
// JS context is replaced — the callback never reaches the old
|
|
197
|
-
// context on success, which is the documented contract.
|
|
198
|
-
} catch (e: Exception) {
|
|
199
|
-
Log.e(TAG, "applyNow reload failed: ${e.message}")
|
|
200
|
-
callback?.invoke(error(e.message ?: "Reload failed", "apply-failed"))
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
@LynxMethod
|
|
206
|
-
fun markReady(callback: Callback?) {
|
|
207
|
-
try {
|
|
208
|
-
UpdateStore.markReady(mContext)
|
|
209
|
-
callback?.invoke(ok())
|
|
210
|
-
} catch (e: Exception) {
|
|
211
|
-
callback?.invoke(error(e.message ?: "markReady failed"))
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
@LynxMethod
|
|
216
|
-
fun setRollbackOptions(params: ReadableMap?, callback: Callback?) {
|
|
217
|
-
try {
|
|
218
|
-
val max = params?.getInt("maxFailedLaunches") ?: 2
|
|
219
|
-
UpdateStore.setMaxLaunchAttempts(mContext, max)
|
|
220
|
-
callback?.invoke(ok())
|
|
221
|
-
} catch (e: Exception) {
|
|
222
|
-
callback?.invoke(error(e.message ?: "setRollbackOptions failed"))
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
@LynxMethod
|
|
227
|
-
fun clearUpdates(callback: Callback?) {
|
|
228
|
-
try {
|
|
229
|
-
UpdateStore.clearAll(mContext)
|
|
230
|
-
callback?.invoke(ok())
|
|
231
|
-
} catch (e: Exception) {
|
|
232
|
-
callback?.invoke(error(e.message ?: "clearUpdates failed"))
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
1
|
+
package com.sigx.updates
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import com.lynx.jsbridge.LynxMethod
|
|
8
|
+
import com.lynx.jsbridge.LynxModule
|
|
9
|
+
import com.lynx.react.bridge.Callback
|
|
10
|
+
import com.lynx.react.bridge.JavaOnlyMap
|
|
11
|
+
import com.lynx.react.bridge.ReadableMap
|
|
12
|
+
import com.lynx.tasm.TemplateData
|
|
13
|
+
import org.json.JSONObject
|
|
14
|
+
import java.io.File
|
|
15
|
+
import java.util.concurrent.Executors
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* JS bridge for OTA updates.
|
|
19
|
+
* JS usage: NativeModules.Updates.downloadUpdate({...}, callback)
|
|
20
|
+
*
|
|
21
|
+
* Downloads run on a single-thread executor (single-flight is also enforced
|
|
22
|
+
* in [UpdateDownloader]); everything else is cheap file/state work.
|
|
23
|
+
*/
|
|
24
|
+
class UpdatesModule(context: Context) : LynxModule(context) {
|
|
25
|
+
|
|
26
|
+
companion object {
|
|
27
|
+
private const val TAG = "SigxUpdates"
|
|
28
|
+
private val executor = Executors.newSingleThreadExecutor { r ->
|
|
29
|
+
Thread(r, "sigx-updates").apply { isDaemon = true }
|
|
30
|
+
}
|
|
31
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private fun error(message: String, code: String? = null): JavaOnlyMap {
|
|
35
|
+
val map = JavaOnlyMap()
|
|
36
|
+
map.putString("error", message)
|
|
37
|
+
if (code != null) map.putString("code", code)
|
|
38
|
+
return map
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private fun ok(): JavaOnlyMap {
|
|
42
|
+
val map = JavaOnlyMap()
|
|
43
|
+
map.putBoolean("ok", true)
|
|
44
|
+
return map
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@LynxMethod
|
|
48
|
+
fun getInstalledRuntimeVersion(): String {
|
|
49
|
+
return UpdateStore.installedRuntimeVersion(mContext)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@LynxMethod
|
|
53
|
+
fun getPlatform(): String = "android"
|
|
54
|
+
|
|
55
|
+
@LynxMethod
|
|
56
|
+
fun getCurrentUpdate(callback: Callback?) {
|
|
57
|
+
try {
|
|
58
|
+
val map = JavaOnlyMap()
|
|
59
|
+
val updateId = UpdateStore.launchedUpdateId
|
|
60
|
+
if (updateId != null) {
|
|
61
|
+
map.putString("updateId", updateId)
|
|
62
|
+
map.putBoolean("isEmbedded", false)
|
|
63
|
+
val metaFile = UpdateStore.updateJsonFile(mContext, updateId)
|
|
64
|
+
if (metaFile.exists()) {
|
|
65
|
+
try {
|
|
66
|
+
val meta = JSONObject(metaFile.readText())
|
|
67
|
+
map.putString("version", meta.optString("version"))
|
|
68
|
+
} catch (_: Exception) { /* metadata is best-effort */ }
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
map.putBoolean("isEmbedded", true)
|
|
72
|
+
}
|
|
73
|
+
map.putString("runtimeVersion", UpdateStore.installedRuntimeVersion(mContext))
|
|
74
|
+
// Store-shipped app version — providers receive it as
|
|
75
|
+
// UpdateCheckContext.embeddedVersion.
|
|
76
|
+
val versionName = runCatching {
|
|
77
|
+
mContext.packageManager.getPackageInfo(mContext.packageName, 0).versionName
|
|
78
|
+
}.getOrNull()
|
|
79
|
+
map.putString("embeddedVersion", versionName ?: "")
|
|
80
|
+
map.putBoolean("isFirstLaunchAfterUpdate", UpdateStore.isFirstLaunchAfterUpdate)
|
|
81
|
+
map.putBoolean("didRollBack", UpdateStore.didRollBack)
|
|
82
|
+
UpdateStore.rolledBackUpdateId?.let { map.putString("rolledBackUpdateId", it) }
|
|
83
|
+
callback?.invoke(map)
|
|
84
|
+
} catch (e: Exception) {
|
|
85
|
+
callback?.invoke(error(e.message ?: "getCurrentUpdate failed"))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@LynxMethod
|
|
90
|
+
fun getState(callback: Callback?) {
|
|
91
|
+
try {
|
|
92
|
+
val state = UpdateStore.readState(mContext)
|
|
93
|
+
val map = JavaOnlyMap()
|
|
94
|
+
if (state != null) {
|
|
95
|
+
map.putString("currentUpdateId", state.currentUpdateId ?: "")
|
|
96
|
+
map.putString("previousUpdateId", state.previousUpdateId ?: "")
|
|
97
|
+
map.putString("pendingUpdateId", state.pendingUpdateId ?: "")
|
|
98
|
+
map.putInt("pendingLaunchAttempts", state.pendingLaunchAttempts)
|
|
99
|
+
map.putInt("maxLaunchAttempts", state.maxLaunchAttempts)
|
|
100
|
+
map.putString("lastRollbackUpdateId", state.lastRollbackUpdateId ?: "")
|
|
101
|
+
map.putString("lastRollbackReason", state.lastRollbackReason ?: "")
|
|
102
|
+
}
|
|
103
|
+
map.putString("runningUpdateId", UpdateStore.launchedUpdateId ?: "")
|
|
104
|
+
callback?.invoke(map)
|
|
105
|
+
} catch (e: Exception) {
|
|
106
|
+
callback?.invoke(error(e.message ?: "getState failed"))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@LynxMethod
|
|
111
|
+
fun downloadUpdate(params: ReadableMap?, callback: Callback?) {
|
|
112
|
+
val url = params?.getString("url")
|
|
113
|
+
val sha256 = params?.getString("sha256")
|
|
114
|
+
val updateId = params?.getString("updateId")
|
|
115
|
+
val runtimeVersion = params?.getString("runtimeVersion")
|
|
116
|
+
val manifestJson = try { params?.getString("manifestJson") } catch (_: Exception) { null }
|
|
117
|
+
if (url.isNullOrEmpty() || sha256.isNullOrEmpty() || updateId.isNullOrEmpty()) {
|
|
118
|
+
callback?.invoke(error("url, sha256 and updateId are required"))
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Refuse incompatible bundles at the door — defense in depth on top
|
|
123
|
+
// of the JS-side check.
|
|
124
|
+
val installed = UpdateStore.installedRuntimeVersion(mContext)
|
|
125
|
+
if (!runtimeVersion.isNullOrEmpty() && runtimeVersion != installed) {
|
|
126
|
+
callback?.invoke(error(
|
|
127
|
+
"Update requires runtime $runtimeVersion but binary is $installed",
|
|
128
|
+
"E_RUNTIME_MISMATCH",
|
|
129
|
+
))
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
val headers = mutableMapOf<String, String>()
|
|
134
|
+
try {
|
|
135
|
+
val headerMap = params.getMap("headers")
|
|
136
|
+
if (headerMap != null) {
|
|
137
|
+
val it = headerMap.keySetIterator()
|
|
138
|
+
while (it.hasNextKey()) {
|
|
139
|
+
val key = it.nextKey()
|
|
140
|
+
headerMap.getString(key)?.let { v -> headers[key] = v }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (_: Exception) { /* headers optional */ }
|
|
144
|
+
|
|
145
|
+
executor.execute {
|
|
146
|
+
val result = UpdateDownloader.download(
|
|
147
|
+
mContext, url, sha256, updateId, headers, manifestJson ?: "{}")
|
|
148
|
+
if (result == null) {
|
|
149
|
+
callback?.invoke(ok())
|
|
150
|
+
} else {
|
|
151
|
+
val code = when {
|
|
152
|
+
result.startsWith("E_DOWNLOAD_IN_PROGRESS") -> "E_DOWNLOAD_IN_PROGRESS"
|
|
153
|
+
result.startsWith("E_HASH_MISMATCH") -> "hash-mismatch"
|
|
154
|
+
else -> null
|
|
155
|
+
}
|
|
156
|
+
callback?.invoke(error(result, code))
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@LynxMethod
|
|
162
|
+
fun applyOnNextLaunch(updateId: String?, callback: Callback?) {
|
|
163
|
+
if (updateId.isNullOrEmpty()) {
|
|
164
|
+
callback?.invoke(error("updateId is required"))
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
val result = UpdateStore.stagePending(mContext, updateId)
|
|
168
|
+
callback?.invoke(if (result == null) ok() else error(result))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@LynxMethod
|
|
172
|
+
fun applyNow(updateId: String?, callback: Callback?) {
|
|
173
|
+
if (updateId.isNullOrEmpty()) {
|
|
174
|
+
callback?.invoke(error("updateId is required"))
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
val bundle = UpdateStore.bundleFile(mContext, updateId)
|
|
178
|
+
if (!bundle.exists()) {
|
|
179
|
+
callback?.invoke(error("Update $updateId is not on disk"))
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
val view = UpdateStore.currentView()
|
|
183
|
+
if (view == null) {
|
|
184
|
+
callback?.invoke(error("No LynxView attached — cannot reload in place", "E_NO_VIEW"))
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
// Stage first so a crash mid-reload still gets crash-guarded rollback
|
|
188
|
+
// (the reload bypasses the startup resolver).
|
|
189
|
+
UpdateStore.stagePending(mContext, updateId)
|
|
190
|
+
UpdateStore.recordReloadAttempt(mContext, updateId)
|
|
191
|
+
mainHandler.post {
|
|
192
|
+
try {
|
|
193
|
+
val bytes = File(bundle.absolutePath).readBytes()
|
|
194
|
+
view.reloadAndInit()
|
|
195
|
+
view.renderTemplateWithBaseUrl(bytes, TemplateData.empty(), "file://${bundle.absolutePath}")
|
|
196
|
+
// JS context is replaced — the callback never reaches the old
|
|
197
|
+
// context on success, which is the documented contract.
|
|
198
|
+
} catch (e: Exception) {
|
|
199
|
+
Log.e(TAG, "applyNow reload failed: ${e.message}")
|
|
200
|
+
callback?.invoke(error(e.message ?: "Reload failed", "apply-failed"))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@LynxMethod
|
|
206
|
+
fun markReady(callback: Callback?) {
|
|
207
|
+
try {
|
|
208
|
+
UpdateStore.markReady(mContext)
|
|
209
|
+
callback?.invoke(ok())
|
|
210
|
+
} catch (e: Exception) {
|
|
211
|
+
callback?.invoke(error(e.message ?: "markReady failed"))
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@LynxMethod
|
|
216
|
+
fun setRollbackOptions(params: ReadableMap?, callback: Callback?) {
|
|
217
|
+
try {
|
|
218
|
+
val max = params?.getInt("maxFailedLaunches") ?: 2
|
|
219
|
+
UpdateStore.setMaxLaunchAttempts(mContext, max)
|
|
220
|
+
callback?.invoke(ok())
|
|
221
|
+
} catch (e: Exception) {
|
|
222
|
+
callback?.invoke(error(e.message ?: "setRollbackOptions failed"))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@LynxMethod
|
|
227
|
+
fun clearUpdates(callback: Callback?) {
|
|
228
|
+
try {
|
|
229
|
+
UpdateStore.clearAll(mContext)
|
|
230
|
+
callback?.invoke(ok())
|
|
231
|
+
} catch (e: Exception) {
|
|
232
|
+
callback?.invoke(error(e.message ?: "clearUpdates failed"))
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|