react-native-update 10.56.1 → 10.57.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/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +102 -11
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateConfig.java +99 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateResult.java +51 -0
- package/android/src/main/java/cn/reactnative/modules/update/PushyNativeUpdate.java +127 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +35 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +1 -1
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +121 -4
- package/harmony/pushy/src/main/ets/NativeUpdateConfig.ts +102 -0
- package/harmony/pushy/src/main/ets/NativeUpdateResult.ts +32 -0
- package/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +15 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +1 -1
- package/harmony/pushy/src/main/ets/UpdateContext.ts +25 -0
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.h +24 -0
- package/ios/RCTPushy/RCTPushy.mm +191 -12
- package/ios/RCTPushy/RCTPushyNativeConfig.h +5 -0
- package/ios/RCTPushy/RCTPushyNativeConfig.mm +103 -0
- package/package.json +1 -1
- package/react-native-update.podspec +7 -1
- package/src/NativePushy.ts +3 -2
- package/src/client.ts +8 -0
- package/src/type.ts +7 -0
|
@@ -71,6 +71,52 @@ final class NativeCheckOrchestrator {
|
|
|
71
71
|
// (markJsCheckCompleted). Process-scoped by design: the next launch
|
|
72
72
|
// starts with no signal and the cold-start round runs again.
|
|
73
73
|
private static volatile String sJsCompletedConfig;
|
|
74
|
+
// Published after the launch rollback snapshot, before host calls are accepted.
|
|
75
|
+
private static volatile boolean nativeReady;
|
|
76
|
+
private static volatile NativeUpdateResult roundResult =
|
|
77
|
+
NativeUpdateResult.of(NativeUpdateResult.FAILED, "check_failed");
|
|
78
|
+
private static volatile long roundGeneration = -1;
|
|
79
|
+
private static volatile long roundConfigGeneration = -1;
|
|
80
|
+
private static volatile String roundConfigJson;
|
|
81
|
+
|
|
82
|
+
/** Blocking only on the host API's worker; never call on the UI thread. */
|
|
83
|
+
static NativeUpdateResult checkAndUpdate(UpdateContext context) throws InterruptedException {
|
|
84
|
+
if (UpdateContext.DEBUG) {
|
|
85
|
+
return NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "debug");
|
|
86
|
+
}
|
|
87
|
+
if (!nativeReady || sContext != context || !context.getIsUsingBundleUrl()) {
|
|
88
|
+
return NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "not_initialized");
|
|
89
|
+
}
|
|
90
|
+
String configJson = context.getKv(KEY_CONFIG);
|
|
91
|
+
if (configJson == null || configJson.isEmpty()) {
|
|
92
|
+
return NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "not_configured");
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
JSONObject config = new JSONObject(configJson);
|
|
96
|
+
if (config.optBoolean("disabled", false)) {
|
|
97
|
+
return NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "disabled");
|
|
98
|
+
}
|
|
99
|
+
if (config.optString("appKey", "").isEmpty()) {
|
|
100
|
+
return NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_config");
|
|
101
|
+
}
|
|
102
|
+
} catch (JSONException e) {
|
|
103
|
+
return NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_config");
|
|
104
|
+
}
|
|
105
|
+
startRound(0);
|
|
106
|
+
if (!roundStarted.get()) {
|
|
107
|
+
return NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "config_changed");
|
|
108
|
+
}
|
|
109
|
+
roundDone.await();
|
|
110
|
+
if (roundConfigGeneration != UpdateContext.getNativeConfigGeneration()
|
|
111
|
+
|| !configJson.equals(roundConfigJson) || !configJson.equals(context.getKv(KEY_CONFIG))) {
|
|
112
|
+
return NativeUpdateResult.of(NativeUpdateResult.CANCELLED, "config_changed");
|
|
113
|
+
}
|
|
114
|
+
if (roundGeneration != UpdateContext.getResetGeneration()) {
|
|
115
|
+
return NativeUpdateResult.of(NativeUpdateResult.CANCELLED, "reset");
|
|
116
|
+
}
|
|
117
|
+
return roundResult;
|
|
118
|
+
}
|
|
119
|
+
|
|
74
120
|
|
|
75
121
|
static void markJsCheckCompleted(String config) {
|
|
76
122
|
sJsCompletedConfig = config;
|
|
@@ -99,6 +145,7 @@ final class NativeCheckOrchestrator {
|
|
|
99
145
|
}
|
|
100
146
|
sContext = context;
|
|
101
147
|
sLaunchRolledBackVersion = launchRolledBackVersion;
|
|
148
|
+
nativeReady = true;
|
|
102
149
|
// The crash-hold rescue shares the orchestrator's rollout gate: no
|
|
103
150
|
// persisted config, no handler (§11.3).
|
|
104
151
|
if (context.getKv(KEY_CONFIG) != null) {
|
|
@@ -142,7 +189,36 @@ final class NativeCheckOrchestrator {
|
|
|
142
189
|
* started it yet. deadlineNanos > 0 (crash rescue) caps every HTTP call
|
|
143
190
|
* and download phase to the remaining budget.
|
|
144
191
|
*/
|
|
192
|
+
private static boolean hasRunnableConfig(UpdateContext context) {
|
|
193
|
+
if (context == null) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
String json = context.getKv(KEY_CONFIG);
|
|
198
|
+
if (json == null) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
JSONObject config = new JSONObject(json);
|
|
202
|
+
return !config.optBoolean("disabled", false)
|
|
203
|
+
&& config.opt("appKey") instanceof String
|
|
204
|
+
&& !config.getString("appKey").trim().isEmpty();
|
|
205
|
+
} catch (JSONException e) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static void onConfigured(UpdateContext context) {
|
|
211
|
+
if (nativeReady && sContext == context && hasRunnableConfig(context)) {
|
|
212
|
+
CrashRescue.install();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
145
216
|
private static void startRound(long deadlineNanos) {
|
|
217
|
+
// An automatic check before first-run provisioning must not consume
|
|
218
|
+
// the process's only round. Hosts may configure later in this launch.
|
|
219
|
+
if (!hasRunnableConfig(sContext)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
146
222
|
if (!roundStarted.compareAndSet(false, true)) {
|
|
147
223
|
return;
|
|
148
224
|
}
|
|
@@ -150,6 +226,7 @@ final class NativeCheckOrchestrator {
|
|
|
150
226
|
runOnce(sContext, sLaunchRolledBackVersion, deadlineNanos);
|
|
151
227
|
} catch (Throwable e) {
|
|
152
228
|
Log.w(UpdateContext.TAG, "native check failed: " + e);
|
|
229
|
+
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "internal_error");
|
|
153
230
|
} finally {
|
|
154
231
|
roundCompleted = true;
|
|
155
232
|
roundDone.countDown();
|
|
@@ -170,7 +247,7 @@ final class NativeCheckOrchestrator {
|
|
|
170
247
|
}
|
|
171
248
|
crashRescueActive = true;
|
|
172
249
|
startRound(deadlineNanos);
|
|
173
|
-
if (!roundCompleted) {
|
|
250
|
+
if (roundStarted.get() && !roundCompleted) {
|
|
174
251
|
long remainingNanos = deadlineNanos - System.nanoTime();
|
|
175
252
|
if (remainingNanos > 0) {
|
|
176
253
|
try {
|
|
@@ -223,32 +300,33 @@ final class NativeCheckOrchestrator {
|
|
|
223
300
|
String launchRolledBackVersion,
|
|
224
301
|
long deadlineNanos
|
|
225
302
|
) throws JSONException {
|
|
226
|
-
// Sampled before any IO: a reset landing while this round runs must
|
|
227
|
-
// win over the round's decision.
|
|
228
303
|
final long resetGeneration = UpdateContext.getResetGeneration();
|
|
304
|
+
roundGeneration = resetGeneration;
|
|
305
|
+
roundConfigGeneration = UpdateContext.getNativeConfigGeneration();
|
|
306
|
+
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "check_failed");
|
|
229
307
|
String configJson = context.getKv(KEY_CONFIG);
|
|
308
|
+
roundConfigJson = configJson;
|
|
230
309
|
if (configJson == null || configJson.isEmpty()) {
|
|
231
|
-
|
|
232
|
-
// native check silently does not run — this is the rollout gate.
|
|
310
|
+
roundResult = NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "not_configured");
|
|
233
311
|
return;
|
|
234
312
|
}
|
|
235
313
|
JSONObject config;
|
|
236
314
|
try {
|
|
237
315
|
config = new JSONObject(configJson);
|
|
238
316
|
} catch (JSONException e) {
|
|
317
|
+
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_config");
|
|
239
318
|
return;
|
|
240
319
|
}
|
|
241
320
|
if (config.optBoolean("disabled", false)) {
|
|
321
|
+
roundResult = NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "disabled");
|
|
242
322
|
return;
|
|
243
323
|
}
|
|
244
324
|
String appKey = config.optString("appKey", "");
|
|
245
325
|
if (appKey.isEmpty()) {
|
|
326
|
+
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_config");
|
|
246
327
|
return;
|
|
247
328
|
}
|
|
248
|
-
//
|
|
249
|
-
// the next launch reads to skip its 5s delay if we die mid-round.
|
|
250
|
-
// Best-effort: a lost breadcrumb costs one 5s delay, it must not
|
|
251
|
-
// abort the rescue round itself.
|
|
329
|
+
// Keep the existing interrupted-round breadcrumb and reset generation.
|
|
252
330
|
try {
|
|
253
331
|
context.setKv(KEY_ROUND_INCOMPLETE, "1");
|
|
254
332
|
} catch (IllegalStateException ignored) {
|
|
@@ -323,6 +401,7 @@ final class NativeCheckOrchestrator {
|
|
|
323
401
|
|
|
324
402
|
String body = NativeUpdateFlow.buildCheckRequestBody(input.toString());
|
|
325
403
|
if (body == null) {
|
|
404
|
+
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_request");
|
|
326
405
|
return;
|
|
327
406
|
}
|
|
328
407
|
|
|
@@ -339,19 +418,24 @@ final class NativeCheckOrchestrator {
|
|
|
339
418
|
String decisionJson = NativeUpdateFlow.handleCheckResponse(
|
|
340
419
|
responseText, identity.toString(), config.optString("afterDownload", ""));
|
|
341
420
|
if (decisionJson == null) {
|
|
421
|
+
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_response");
|
|
342
422
|
return;
|
|
343
423
|
}
|
|
344
424
|
JSONObject decision = new JSONObject(decisionJson);
|
|
345
425
|
if (!"download".equals(decision.optString("action"))) {
|
|
346
|
-
context.commitNativeCheckResult(
|
|
426
|
+
boolean committed = context.commitNativeCheckResult(
|
|
347
427
|
resetGeneration, null, null, false,
|
|
348
428
|
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
|
|
429
|
+
roundResult = committed
|
|
430
|
+
? NativeUpdateResult.of(NativeUpdateResult.NO_UPDATE, decision.optString("reason"))
|
|
431
|
+
: NativeUpdateResult.of(NativeUpdateResult.CANCELLED, "reset");
|
|
349
432
|
Log.i(UpdateContext.TAG,
|
|
350
433
|
"native check: nothing to do (" + decision.optString("reason") + ")");
|
|
351
434
|
return;
|
|
352
435
|
}
|
|
353
436
|
String hash = decision.optString("hash", "");
|
|
354
437
|
if (!UpdateFileUtils.isSafePathComponent(hash)) {
|
|
438
|
+
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_response");
|
|
355
439
|
return;
|
|
356
440
|
}
|
|
357
441
|
|
|
@@ -364,9 +448,12 @@ final class NativeCheckOrchestrator {
|
|
|
364
448
|
if (!downloaded) {
|
|
365
449
|
// The native attempt has finished, so JS may safely reuse the
|
|
366
450
|
// response and retry through its own strategy chain.
|
|
367
|
-
context.commitNativeCheckResult(
|
|
451
|
+
boolean committed = context.commitNativeCheckResult(
|
|
368
452
|
resetGeneration, null, null, false,
|
|
369
453
|
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
|
|
454
|
+
roundResult = NativeUpdateResult.of(
|
|
455
|
+
committed ? NativeUpdateResult.FAILED : NativeUpdateResult.CANCELLED,
|
|
456
|
+
committed ? "download_failed" : "reset");
|
|
370
457
|
return;
|
|
371
458
|
}
|
|
372
459
|
|
|
@@ -412,6 +499,7 @@ final class NativeCheckOrchestrator {
|
|
|
412
499
|
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
|
|
413
500
|
} catch (Exception e) {
|
|
414
501
|
Log.w(UpdateContext.TAG, "native check: commit failed: " + e);
|
|
502
|
+
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "commit_failed");
|
|
415
503
|
return;
|
|
416
504
|
}
|
|
417
505
|
if (!committed) {
|
|
@@ -428,6 +516,9 @@ final class NativeCheckOrchestrator {
|
|
|
428
516
|
Log.i(UpdateContext.TAG,
|
|
429
517
|
"native check: downloaded " + hash + ", activation left to JS");
|
|
430
518
|
}
|
|
519
|
+
roundResult = committed
|
|
520
|
+
? NativeUpdateResult.downloaded(hash, activate)
|
|
521
|
+
: NativeUpdateResult.of(NativeUpdateResult.CANCELLED, "reset");
|
|
431
522
|
}
|
|
432
523
|
|
|
433
524
|
private static String buildResponseCacheJson(
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
package cn.reactnative.modules.update;
|
|
2
|
+
|
|
3
|
+
import java.util.Arrays;
|
|
4
|
+
import java.util.HashSet;
|
|
5
|
+
import java.util.Iterator;
|
|
6
|
+
import java.util.Set;
|
|
7
|
+
import java.util.regex.Pattern;
|
|
8
|
+
import org.json.JSONArray;
|
|
9
|
+
import org.json.JSONException;
|
|
10
|
+
import org.json.JSONObject;
|
|
11
|
+
|
|
12
|
+
/** Internal normalizer for the JSONObject accepted by PushyNativeUpdate.configure. */
|
|
13
|
+
final class NativeUpdateConfig {
|
|
14
|
+
private static final Set<String> KEYS = new HashSet<>(Arrays.asList(
|
|
15
|
+
"appKey", "endpoints", "queryUrls", "afterDownload", "disabled",
|
|
16
|
+
"packageVersion", "rnu", "rn"));
|
|
17
|
+
private static final Pattern URL = Pattern.compile(
|
|
18
|
+
"^https?://(\\[[0-9a-fA-F:]+\\]|[a-zA-Z0-9.-]+)(:[0-9]+)?([/?#]|$)");
|
|
19
|
+
private static final String[] ENDPOINTS = {
|
|
20
|
+
"https://update.react-native.cn/api", "https://update.reactnative.cn/api"
|
|
21
|
+
};
|
|
22
|
+
private static final String[] QUERY_URLS = {
|
|
23
|
+
"https://gitee.com/sunnylqm/react-native-pushy/raw/master/endpoints.json",
|
|
24
|
+
"https://cdn.jsdelivr.net/gh/reactnativecn/react-native-update@master/endpoints.json"
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
private NativeUpdateConfig() {}
|
|
28
|
+
|
|
29
|
+
static String normalize(String json) throws JSONException {
|
|
30
|
+
JSONObject options = new JSONObject(json);
|
|
31
|
+
for (Iterator<String> keys = options.keys(); keys.hasNext();) {
|
|
32
|
+
String key = keys.next();
|
|
33
|
+
if (!KEYS.contains(key)) {
|
|
34
|
+
throw invalid("unknown option " + key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
String appKey = string(options.opt("appKey"), "appKey", false);
|
|
38
|
+
boolean customEndpoints = options.has("endpoints");
|
|
39
|
+
JSONArray endpoints = urls(customEndpoints ? options.get("endpoints")
|
|
40
|
+
: new JSONArray(Arrays.asList(ENDPOINTS)), "endpoints", true);
|
|
41
|
+
JSONArray queryUrls = urls(options.has("queryUrls") ? options.get("queryUrls")
|
|
42
|
+
: new JSONArray(Arrays.asList(customEndpoints ? new String[0] : QUERY_URLS)),
|
|
43
|
+
"queryUrls", false);
|
|
44
|
+
String afterDownload = options.has("afterDownload")
|
|
45
|
+
? string(options.get("afterDownload"), "afterDownload", false) : "none";
|
|
46
|
+
if (!"none".equals(afterDownload) && !"setNeedUpdate".equals(afterDownload)) {
|
|
47
|
+
throw invalid("afterDownload must be none or setNeedUpdate");
|
|
48
|
+
}
|
|
49
|
+
Object disabled = options.has("disabled") ? options.get("disabled") : Boolean.FALSE;
|
|
50
|
+
if (!(disabled instanceof Boolean)) {
|
|
51
|
+
throw invalid("disabled must be a boolean");
|
|
52
|
+
}
|
|
53
|
+
JSONObject result = new JSONObject();
|
|
54
|
+
result.put("appKey", appKey);
|
|
55
|
+
result.put("endpoints", endpoints);
|
|
56
|
+
result.put("queryUrls", queryUrls);
|
|
57
|
+
result.put("afterDownload", afterDownload);
|
|
58
|
+
result.put("disabled", disabled);
|
|
59
|
+
result.put("rnu", options.has("rnu") ? string(options.get("rnu"), "rnu", true) : "");
|
|
60
|
+
result.put("rn", options.has("rn") ? string(options.get("rn"), "rn", true) : "");
|
|
61
|
+
if (options.has("packageVersion")) {
|
|
62
|
+
result.put("packageVersion", string(options.get("packageVersion"), "packageVersion", false));
|
|
63
|
+
}
|
|
64
|
+
return result.toString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static String string(Object value, String name, boolean allowEmpty) {
|
|
68
|
+
if (!(value instanceof String) || (!allowEmpty && ((String) value).trim().isEmpty())) {
|
|
69
|
+
throw invalid(name + " must be a string" + (allowEmpty ? "" : " and must not be blank"));
|
|
70
|
+
}
|
|
71
|
+
return (String) value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private static JSONArray urls(Object value, String name, boolean base) throws JSONException {
|
|
75
|
+
if (!(value instanceof JSONArray) || (base && ((JSONArray) value).length() == 0)) {
|
|
76
|
+
throw invalid(name + " must be " + (base ? "a non-empty" : "an") + " array");
|
|
77
|
+
}
|
|
78
|
+
JSONArray values = (JSONArray) value;
|
|
79
|
+
JSONArray result = new JSONArray();
|
|
80
|
+
Set<String> seen = new HashSet<>();
|
|
81
|
+
for (int i = 0; i < values.length(); i++) {
|
|
82
|
+
String url = string(values.get(i), name, false).trim();
|
|
83
|
+
if (!URL.matcher(url).find() || url.matches("(?s).*\\s.*") || url.contains("\\")
|
|
84
|
+
|| (base && (url.contains("?") || url.contains("#")))) {
|
|
85
|
+
throw invalid(name + " requires absolute HTTP(S) URLs without credentials"
|
|
86
|
+
+ (base ? ", queries or fragments" : ""));
|
|
87
|
+
}
|
|
88
|
+
String normalized = base ? url.replaceAll("/+$", "") : url;
|
|
89
|
+
if (seen.add(normalized)) {
|
|
90
|
+
result.put(normalized);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private static IllegalArgumentException invalid(String message) {
|
|
97
|
+
return new IllegalArgumentException("Invalid native configuration: " + message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package cn.reactnative.modules.update;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Snapshot of a native check-and-update round. A downloaded result does not
|
|
5
|
+
* mean that the running React Native instance has been reloaded.
|
|
6
|
+
*/
|
|
7
|
+
public final class NativeUpdateResult {
|
|
8
|
+
public static final String SKIPPED = "skipped";
|
|
9
|
+
public static final String NO_UPDATE = "noUpdate";
|
|
10
|
+
public static final String DOWNLOADED = "downloaded";
|
|
11
|
+
public static final String FAILED = "failed";
|
|
12
|
+
public static final String CANCELLED = "cancelled";
|
|
13
|
+
|
|
14
|
+
private final String status;
|
|
15
|
+
private final String reason;
|
|
16
|
+
private final String hash;
|
|
17
|
+
private final boolean activated;
|
|
18
|
+
|
|
19
|
+
private NativeUpdateResult(String status, String reason, String hash, boolean activated) {
|
|
20
|
+
this.status = status;
|
|
21
|
+
this.reason = reason;
|
|
22
|
+
this.hash = hash;
|
|
23
|
+
this.activated = activated;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static NativeUpdateResult of(String status, String reason) {
|
|
27
|
+
return new NativeUpdateResult(status, reason, "", false);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static NativeUpdateResult downloaded(String hash, boolean activated) {
|
|
31
|
+
return new NativeUpdateResult(DOWNLOADED, "", hash, activated);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public String getStatus() {
|
|
35
|
+
return status;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public String getReason() {
|
|
39
|
+
return reason;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Installed update hash, or an empty string when this round installed nothing. */
|
|
43
|
+
public String getHash() {
|
|
44
|
+
return hash;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Whether this round selected the downloaded version for the next launch. */
|
|
48
|
+
public boolean isActivated() {
|
|
49
|
+
return activated;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
package cn.reactnative.modules.update;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.os.Handler;
|
|
5
|
+
import android.os.Looper;
|
|
6
|
+
import android.util.Log;
|
|
7
|
+
import androidx.annotation.Nullable;
|
|
8
|
+
import org.json.JSONObject;
|
|
9
|
+
import java.util.concurrent.Executor;
|
|
10
|
+
import java.util.concurrent.Executors;
|
|
11
|
+
import java.util.concurrent.ThreadFactory;
|
|
12
|
+
|
|
13
|
+
/** Bridge-free native configuration and update APIs. */
|
|
14
|
+
public final class PushyNativeUpdate {
|
|
15
|
+
public interface Callback {
|
|
16
|
+
/** Always called on the main thread, including skipped and failed checks. */
|
|
17
|
+
void onComplete(NativeUpdateResult result);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// A single waiting worker, not one thread per caller. The actual round is
|
|
21
|
+
// shared with cold start and crash rescue by NativeCheckOrchestrator.
|
|
22
|
+
private static final Executor WORKER = Executors.newSingleThreadExecutor(new ThreadFactory() {
|
|
23
|
+
@Override
|
|
24
|
+
public Thread newThread(Runnable runnable) {
|
|
25
|
+
Thread thread = new Thread(runnable, "pushy-host-check");
|
|
26
|
+
thread.setDaemon(true);
|
|
27
|
+
return thread;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
public interface ConfigurationCallback {
|
|
32
|
+
/** Main thread; null means configuration was persisted successfully. */
|
|
33
|
+
void onComplete(@Nullable Exception error);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Configuration must not wait behind a network round that it invalidates.
|
|
37
|
+
private static final Executor CONFIG_WORKER = Executors.newSingleThreadExecutor(new ThreadFactory() {
|
|
38
|
+
@Override
|
|
39
|
+
public Thread newThread(Runnable runnable) {
|
|
40
|
+
Thread thread = new Thread(runnable, "pushy-host-config");
|
|
41
|
+
thread.setDaemon(true);
|
|
42
|
+
return thread;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate and persist a complete configuration, even before JS or bundle
|
|
48
|
+
* resolution. This starts no network work and never resolves a bundle.
|
|
49
|
+
* Await the callback before continuing startup/checkAndUpdate. Unless JS
|
|
50
|
+
* uses nativeConfigSource: 'native', later JS config writes can replace it.
|
|
51
|
+
*/
|
|
52
|
+
public static void configure(Context context, JSONObject options, final ConfigurationCallback callback) {
|
|
53
|
+
if (context == null || options == null || callback == null) {
|
|
54
|
+
throw new IllegalArgumentException("context, options and callback are required");
|
|
55
|
+
}
|
|
56
|
+
final Context applicationContext = context.getApplicationContext();
|
|
57
|
+
// Snapshot caller-owned JSON before dispatch, not minutes later on a worker.
|
|
58
|
+
final String snapshot = options.toString();
|
|
59
|
+
CONFIG_WORKER.execute(new Runnable() {
|
|
60
|
+
@Override
|
|
61
|
+
public void run() {
|
|
62
|
+
Exception failure = null;
|
|
63
|
+
try {
|
|
64
|
+
String config = NativeUpdateConfig.normalize(snapshot);
|
|
65
|
+
UpdateContext.getInstance(applicationContext).setNativeConfig(config);
|
|
66
|
+
} catch (Exception e) {
|
|
67
|
+
failure = e;
|
|
68
|
+
} catch (LinkageError e) {
|
|
69
|
+
failure = new IllegalStateException("Native configuration failed", e);
|
|
70
|
+
}
|
|
71
|
+
final Exception error = failure;
|
|
72
|
+
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
|
73
|
+
@Override
|
|
74
|
+
public void run() {
|
|
75
|
+
callback.onComplete(error);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private PushyNativeUpdate() {
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Start the process's native check now, join its in-flight round, or return
|
|
87
|
+
* its completed result. This does not reload React Native or show UI.
|
|
88
|
+
*
|
|
89
|
+
* Call after the host has resolved its real launch bundle with
|
|
90
|
+
* UpdateContext.getBundleUrl. Do not resolve the bundle again just to call
|
|
91
|
+
* this method: bundle resolution consumes first-load/rollback markers.
|
|
92
|
+
* Missing persisted configuration and debug builds are reported as skipped.
|
|
93
|
+
*/
|
|
94
|
+
public static void checkAndUpdate(Context context, final Callback callback) {
|
|
95
|
+
if (context == null || callback == null) {
|
|
96
|
+
throw new IllegalArgumentException("context and callback are required");
|
|
97
|
+
}
|
|
98
|
+
final Context applicationContext = context.getApplicationContext();
|
|
99
|
+
WORKER.execute(new Runnable() {
|
|
100
|
+
@Override
|
|
101
|
+
public void run() {
|
|
102
|
+
NativeUpdateResult outcome;
|
|
103
|
+
try {
|
|
104
|
+
if (BuildConfig.DEBUG) {
|
|
105
|
+
outcome = NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "debug");
|
|
106
|
+
} else {
|
|
107
|
+
outcome = NativeCheckOrchestrator.checkAndUpdate(
|
|
108
|
+
UpdateContext.getInstance(applicationContext));
|
|
109
|
+
}
|
|
110
|
+
} catch (InterruptedException e) {
|
|
111
|
+
Thread.currentThread().interrupt();
|
|
112
|
+
outcome = NativeUpdateResult.of(NativeUpdateResult.CANCELLED, "interrupted");
|
|
113
|
+
} catch (Exception | LinkageError e) {
|
|
114
|
+
Log.w("react-native-update", "native host check failed", e);
|
|
115
|
+
outcome = NativeUpdateResult.of(NativeUpdateResult.FAILED, "internal_error");
|
|
116
|
+
}
|
|
117
|
+
final NativeUpdateResult result = outcome;
|
|
118
|
+
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
|
119
|
+
@Override
|
|
120
|
+
public void run() {
|
|
121
|
+
callback.onComplete(result);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -737,6 +737,41 @@ public class UpdateContext {
|
|
|
737
737
|
}
|
|
738
738
|
|
|
739
739
|
/** Sampled/compared by the native check orchestrator; see resetGeneration. */
|
|
740
|
+
/** Shared by JS and native hosts. Config replacement invalidates old native decisions. */
|
|
741
|
+
private static final java.util.concurrent.atomic.AtomicLong nativeConfigGeneration =
|
|
742
|
+
new java.util.concurrent.atomic.AtomicLong(0);
|
|
743
|
+
|
|
744
|
+
static long getNativeConfigGeneration() {
|
|
745
|
+
return nativeConfigGeneration.get();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
void setNativeConfig(String config) {
|
|
749
|
+
synchronized (commitLock) {
|
|
750
|
+
boolean changed = !config.equals(sp.getString(NativeCheckOrchestrator.KEY_CONFIG, null));
|
|
751
|
+
SharedPreferences.Editor editor = sp.edit();
|
|
752
|
+
if (changed) {
|
|
753
|
+
// Also invalidate on a failed persistence attempt: an older
|
|
754
|
+
// round must not commit over uncertain configuration state.
|
|
755
|
+
resetGeneration.incrementAndGet();
|
|
756
|
+
nativeConfigGeneration.incrementAndGet();
|
|
757
|
+
editor.remove(NativeCheckOrchestrator.KEY_RESP_CACHE);
|
|
758
|
+
NativeCheckOrchestrator.markJsCheckCompleted(null);
|
|
759
|
+
}
|
|
760
|
+
// A native-only first launch needs a stable gray-release identity
|
|
761
|
+
// before JS initializes. Never replace an existing installation ID.
|
|
762
|
+
String uuid = sp.getString("uuid", null);
|
|
763
|
+
if (uuid == null || uuid.isEmpty()) {
|
|
764
|
+
editor.putString("uuid", java.util.UUID.randomUUID().toString());
|
|
765
|
+
}
|
|
766
|
+
editor.putString(NativeCheckOrchestrator.KEY_CONFIG, config);
|
|
767
|
+
// Persist even an equal value: a previous commit may have updated
|
|
768
|
+
// SharedPreferences memory but failed to write its file.
|
|
769
|
+
persistEditorOrThrow(editor, "configure native update");
|
|
770
|
+
}
|
|
771
|
+
NativeCheckOrchestrator.onConfigured(this);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Native-decision generation: bumped by reset AND configuration replacement.
|
|
740
775
|
static long getResetGeneration() {
|
|
741
776
|
return resetGeneration.get();
|
|
742
777
|
}
|
|
@@ -339,7 +339,7 @@ public class UpdateModuleImpl {
|
|
|
339
339
|
StateSerialRunner.run(promise, ErrorCodes.FILE_OPERATION_FAILED, "syncNativeConfig", new StateSerialRunner.Operation() {
|
|
340
340
|
@Override
|
|
341
341
|
public void run() {
|
|
342
|
-
updateContext.
|
|
342
|
+
updateContext.setNativeConfig(config);
|
|
343
343
|
promise.resolve(true);
|
|
344
344
|
}
|
|
345
345
|
});
|