react-native-update 10.50.0 → 10.51.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/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
- package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
- package/README-CN.md +14 -1
- package/README.md +14 -1
- package/android/build.gradle +10 -0
- package/android/jni/Android.mk +5 -1
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +76 -21
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +160 -42
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +44 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/cpp/update_flow_core/flow_json.cpp +522 -0
- package/cpp/update_flow_core/flow_json.h +103 -0
- package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
- package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
- package/cpp/update_flow_core/update_flow_core.cpp +301 -0
- package/cpp/update_flow_core/update_flow_core.h +77 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -1
- package/harmony/pushy/src/main/ets/DownloadTask.ts +48 -9
- package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
- package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +14 -0
- package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +148 -69
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +779 -61
- package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
- package/package.json +3 -1
- package/react-native-update.podspec +2 -0
- package/src/NativePushy.ts +14 -0
- package/src/client.ts +341 -118
- package/src/endpoint.ts +7 -29
- package/src/provider.tsx +38 -2
- package/src/type.ts +26 -0
- package/src/updateFlowCore.ts +320 -0
- package/src/utils.ts +1 -12
- package/src/isInRollout.ts +0 -87
- package/src/resolveCheckResult.ts +0 -30
|
@@ -47,6 +47,17 @@ public class UpdateContext {
|
|
|
47
47
|
private static final int STATE_OP_CLEAR_ROLLBACK_MARK = 5;
|
|
48
48
|
private static final int STATE_OP_RESOLVE_LAUNCH = 6;
|
|
49
49
|
private static final String KEY_FIRST_LOAD_MARKED = "firstLoadMarked";
|
|
50
|
+
static final String VERSION_COMPLETE_FILE = ".pushy-complete";
|
|
51
|
+
// Bumped by resetToPackagedBundle. The cold-start check runs for minutes
|
|
52
|
+
// and may already hold a decision when the app resets to the packaged
|
|
53
|
+
// bundle; the orchestrator samples this counter and abandons activation
|
|
54
|
+
// (and its response cache) when the value moved, so an in-flight rescue
|
|
55
|
+
// can never resurrect the version the app just reset away from.
|
|
56
|
+
private static final java.util.concurrent.atomic.AtomicLong resetGeneration =
|
|
57
|
+
new java.util.concurrent.atomic.AtomicLong(0);
|
|
58
|
+
// Held by resetToPackagedBundle and by the cold-start check's commit, so
|
|
59
|
+
// the generation check and the writes it guards are one atomic step.
|
|
60
|
+
private static final Object commitLock = new Object();
|
|
50
61
|
|
|
51
62
|
// Singleton instance
|
|
52
63
|
private static volatile UpdateContext sInstance;
|
|
@@ -159,7 +170,9 @@ public class UpdateContext {
|
|
|
159
170
|
});
|
|
160
171
|
}
|
|
161
172
|
|
|
162
|
-
private
|
|
173
|
+
// Package-private: also the native cold-start check's request input
|
|
174
|
+
// (NativeCheckOrchestrator). Blocking — call off the main thread.
|
|
175
|
+
String computeBundleHash() {
|
|
163
176
|
String cachePrefix = getPackageVersion() + "|" + getPackageLastUpdateTime() + "|";
|
|
164
177
|
String cached = sp.getString(KEY_BUNDLE_HASH_CACHE, null);
|
|
165
178
|
if (cached != null && cached.startsWith(cachePrefix)) {
|
|
@@ -238,6 +251,12 @@ public class UpdateContext {
|
|
|
238
251
|
}
|
|
239
252
|
|
|
240
253
|
public void downloadFullUpdate(String url, String hash, DownloadFileListener listener) {
|
|
254
|
+
downloadFullUpdate(url, hash, listener, 0);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
void downloadFullUpdate(
|
|
258
|
+
String url, String hash, DownloadFileListener listener, long deadlineNanos
|
|
259
|
+
) {
|
|
241
260
|
if (rejectUnsafeComponent(hash, listener)) {
|
|
242
261
|
return;
|
|
243
262
|
}
|
|
@@ -246,6 +265,7 @@ public class UpdateContext {
|
|
|
246
265
|
params.url = url;
|
|
247
266
|
params.hash = hash;
|
|
248
267
|
params.listener = listener;
|
|
268
|
+
params.deadlineNanos = deadlineNanos;
|
|
249
269
|
params.targetFile = new File(rootDir, hash + ".ppk");
|
|
250
270
|
params.unzipDirectory = new File(rootDir, hash);
|
|
251
271
|
enqueue(params);
|
|
@@ -273,6 +293,12 @@ public class UpdateContext {
|
|
|
273
293
|
}
|
|
274
294
|
|
|
275
295
|
public void downloadPatchFromApk(String url, String hash, DownloadFileListener listener) {
|
|
296
|
+
downloadPatchFromApk(url, hash, listener, 0);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
void downloadPatchFromApk(
|
|
300
|
+
String url, String hash, DownloadFileListener listener, long deadlineNanos
|
|
301
|
+
) {
|
|
276
302
|
if (rejectUnsafeComponent(hash, listener)) {
|
|
277
303
|
return;
|
|
278
304
|
}
|
|
@@ -281,12 +307,23 @@ public class UpdateContext {
|
|
|
281
307
|
params.url = url;
|
|
282
308
|
params.hash = hash;
|
|
283
309
|
params.listener = listener;
|
|
310
|
+
params.deadlineNanos = deadlineNanos;
|
|
284
311
|
params.targetFile = new File(rootDir, hash + ".apk.patch");
|
|
285
312
|
params.unzipDirectory = new File(rootDir, hash);
|
|
286
313
|
enqueue(params);
|
|
287
314
|
}
|
|
288
315
|
|
|
289
316
|
public void downloadPatchFromPpk(String url, String hash, String originHash, DownloadFileListener listener) {
|
|
317
|
+
downloadPatchFromPpk(url, hash, originHash, listener, 0);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
void downloadPatchFromPpk(
|
|
321
|
+
String url,
|
|
322
|
+
String hash,
|
|
323
|
+
String originHash,
|
|
324
|
+
DownloadFileListener listener,
|
|
325
|
+
long deadlineNanos
|
|
326
|
+
) {
|
|
290
327
|
if (rejectUnsafeComponent(hash, listener) || rejectUnsafeComponent(originHash, listener)) {
|
|
291
328
|
return;
|
|
292
329
|
}
|
|
@@ -296,6 +333,7 @@ public class UpdateContext {
|
|
|
296
333
|
params.hash = hash;
|
|
297
334
|
params.originHash = originHash;
|
|
298
335
|
params.listener = listener;
|
|
336
|
+
params.deadlineNanos = deadlineNanos;
|
|
299
337
|
params.targetFile = new File(rootDir, originHash + "-" + hash + ".ppk.patch");
|
|
300
338
|
params.unzipDirectory = new File(rootDir, hash);
|
|
301
339
|
params.originDirectory = new File(rootDir, originHash);
|
|
@@ -348,10 +386,21 @@ public class UpdateContext {
|
|
|
348
386
|
if (!isSafePathComponent(hash)) {
|
|
349
387
|
throw new IllegalArgumentException("Invalid hash: " + hash);
|
|
350
388
|
}
|
|
351
|
-
|
|
389
|
+
File versionDir = new File(rootDir, hash);
|
|
390
|
+
File bundleFile = new File(versionDir, "index.bundlejs");
|
|
391
|
+
if (!bundleFile.isFile()) {
|
|
352
392
|
throw new IllegalStateException("Bundle version " + hash + " not found.");
|
|
353
393
|
}
|
|
354
394
|
StateCoreResult currentState = getStateSnapshot();
|
|
395
|
+
boolean isLegacyActivatedVersion = hash.equals(currentState.currentVersion)
|
|
396
|
+
|| hash.equals(currentState.lastVersion);
|
|
397
|
+
if (!new File(versionDir, VERSION_COMPLETE_FILE).isFile()
|
|
398
|
+
&& !isLegacyActivatedVersion) {
|
|
399
|
+
// Versions activated before completion markers were introduced are
|
|
400
|
+
// explicitly grandfathered through current/last state. An arbitrary
|
|
401
|
+
// markerless directory may be a crash-left partial install.
|
|
402
|
+
throw new IllegalStateException("Bundle version " + hash + " is incomplete.");
|
|
403
|
+
}
|
|
355
404
|
StateCoreResult nextState = runStateCore(
|
|
356
405
|
STATE_OP_SWITCH_VERSION,
|
|
357
406
|
currentState,
|
|
@@ -445,6 +494,16 @@ public class UpdateContext {
|
|
|
445
494
|
* for gray release bucketing and must not change on reset.
|
|
446
495
|
*/
|
|
447
496
|
public void resetToPackagedBundle() {
|
|
497
|
+
synchronized (commitLock) {
|
|
498
|
+
resetToPackagedBundleLocked();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private void resetToPackagedBundleLocked() {
|
|
503
|
+
// Invalidate any in-flight cold-start round before clearing state: a
|
|
504
|
+
// round committing under the same lock afterwards sees the new
|
|
505
|
+
// generation and drops its result.
|
|
506
|
+
resetGeneration.incrementAndGet();
|
|
448
507
|
StateCoreResult resetState = new StateCoreResult();
|
|
449
508
|
resetState.packageVersion = getPackageVersion();
|
|
450
509
|
resetState.buildTime = getBuildTime();
|
|
@@ -459,6 +518,8 @@ public class UpdateContext {
|
|
|
459
518
|
}
|
|
460
519
|
persistEditor(editor, "reset to packaged bundle");
|
|
461
520
|
ignoreRollback = false;
|
|
521
|
+
// editor.clear() above already dropped the cached check response; it
|
|
522
|
+
// still advertised the version this reset removed.
|
|
462
523
|
Log.i(TAG, "Reset to packaged bundle");
|
|
463
524
|
|
|
464
525
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
@@ -530,55 +591,112 @@ public class UpdateContext {
|
|
|
530
591
|
|
|
531
592
|
public String getBundleUrl(String defaultAssetsUrl) {
|
|
532
593
|
isUsingBundleUrl = true;
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
594
|
+
String nativeCheckRolledBackVersion = null;
|
|
595
|
+
try {
|
|
596
|
+
StateCoreResult currentState = getStateSnapshot();
|
|
597
|
+
StateCoreResult launchState = runStateCore(
|
|
598
|
+
STATE_OP_RESOLVE_LAUNCH,
|
|
599
|
+
currentState,
|
|
600
|
+
null,
|
|
601
|
+
ignoreRollback,
|
|
602
|
+
true
|
|
603
|
+
);
|
|
604
|
+
nativeCheckRolledBackVersion = launchState.rolledBackVersion;
|
|
605
|
+
if (launchState.didRollback) {
|
|
606
|
+
// The crash-protection rollback: the new version never called
|
|
607
|
+
// markSuccess. Keep this visible in release logs.
|
|
608
|
+
Log.e(TAG, "Version " + currentState.currentVersion
|
|
609
|
+
+ " was not marked as successful, rolling back to "
|
|
610
|
+
+ launchState.currentVersion);
|
|
611
|
+
}
|
|
612
|
+
if (launchState.didRollback || launchState.consumedFirstTime) {
|
|
613
|
+
SharedPreferences.Editor editor = sp.edit();
|
|
614
|
+
applyState(editor, launchState);
|
|
615
|
+
if (launchState.consumedFirstTime) {
|
|
616
|
+
editor.putBoolean(KEY_FIRST_LOAD_MARKED, true);
|
|
617
|
+
}
|
|
618
|
+
persistEditor(editor, "resolve launch");
|
|
619
|
+
}
|
|
551
620
|
if (launchState.consumedFirstTime) {
|
|
552
|
-
|
|
621
|
+
// bundleURL may be resolved multiple times in one process.
|
|
622
|
+
ignoreRollback = true;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
String currentVersion = launchState.loadVersion;
|
|
626
|
+
if (currentVersion == null) {
|
|
627
|
+
return defaultAssetsUrl;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Guard the rollback chain against cycles: a corrupted state returning
|
|
631
|
+
// an already-visited version would otherwise spin this loop forever on
|
|
632
|
+
// the main thread.
|
|
633
|
+
java.util.HashSet<String> visitedVersions = new java.util.HashSet<>();
|
|
634
|
+
while (currentVersion != null && visitedVersions.add(currentVersion)) {
|
|
635
|
+
File bundleFile = new File(rootDir, currentVersion+"/index.bundlejs");
|
|
636
|
+
if (!bundleFile.exists()) {
|
|
637
|
+
Log.e(TAG, "Bundle version " + currentVersion + " not found.");
|
|
638
|
+
currentVersion = this.rollBack();
|
|
639
|
+
nativeCheckRolledBackVersion = rolledBackVersion();
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
launchVersion = currentVersion;
|
|
643
|
+
nativeCheckRolledBackVersion = rolledBackVersion();
|
|
644
|
+
return bundleFile.toString();
|
|
553
645
|
}
|
|
554
|
-
persistEditor(editor, "resolve launch");
|
|
555
|
-
}
|
|
556
|
-
if (launchState.consumedFirstTime) {
|
|
557
|
-
// bundleURL may be resolved multiple times in one process.
|
|
558
|
-
ignoreRollback = true;
|
|
559
|
-
}
|
|
560
646
|
|
|
561
|
-
|
|
562
|
-
if (currentVersion == null) {
|
|
647
|
+
nativeCheckRolledBackVersion = rolledBackVersion();
|
|
563
648
|
return defaultAssetsUrl;
|
|
649
|
+
} finally {
|
|
650
|
+
// Even corrupted state or a state-core exception must not disable
|
|
651
|
+
// the next-launch rescue check. A null snapshot simply omits the
|
|
652
|
+
// rollback guard for this exceptional launch.
|
|
653
|
+
NativeCheckOrchestrator.schedule(this, nativeCheckRolledBackVersion);
|
|
564
654
|
}
|
|
655
|
+
}
|
|
565
656
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
657
|
+
/** Sampled/compared by the native check orchestrator; see resetGeneration. */
|
|
658
|
+
static long getResetGeneration() {
|
|
659
|
+
return resetGeneration.get();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Commit everything a cold-start round persists — version info, the
|
|
664
|
+
* activation, the response cache — under one lock that first re-checks the
|
|
665
|
+
* reset generation. resetToPackagedBundle takes the same lock, so there is
|
|
666
|
+
* no compare-and-act window: either the whole round lands, or the reset
|
|
667
|
+
* wins and none of it does. Returns whether the round was committed.
|
|
668
|
+
*/
|
|
669
|
+
boolean commitNativeCheckResult(
|
|
670
|
+
long expectedGeneration,
|
|
671
|
+
String hash,
|
|
672
|
+
String hashInfoJson,
|
|
673
|
+
boolean activate,
|
|
674
|
+
String responseCacheJson
|
|
675
|
+
) {
|
|
676
|
+
synchronized (commitLock) {
|
|
677
|
+
if (resetGeneration.get() != expectedGeneration) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
if (hash != null && hashInfoJson != null) {
|
|
681
|
+
setKv("hash_" + hash, hashInfoJson);
|
|
682
|
+
}
|
|
683
|
+
if (activate && hash != null) {
|
|
684
|
+
switchVersion(hash);
|
|
576
685
|
}
|
|
577
|
-
|
|
578
|
-
|
|
686
|
+
if (responseCacheJson != null) {
|
|
687
|
+
setKv(NativeCheckOrchestrator.KEY_RESP_CACHE, responseCacheJson);
|
|
688
|
+
}
|
|
689
|
+
return true;
|
|
579
690
|
}
|
|
691
|
+
}
|
|
580
692
|
|
|
581
|
-
|
|
693
|
+
boolean hasCompletedVersion(String hash) {
|
|
694
|
+
if (!isSafePathComponent(hash)) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
File versionDir = new File(rootDir, hash);
|
|
698
|
+
return new File(versionDir, "index.bundlejs").isFile()
|
|
699
|
+
&& new File(versionDir, VERSION_COMPLETE_FILE).isFile();
|
|
582
700
|
}
|
|
583
701
|
|
|
584
702
|
private String rollBack() {
|
|
@@ -224,10 +224,54 @@ public class UpdateModuleImpl {
|
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Raw response cached by the native cold-start check, for the JS side to
|
|
229
|
+
* reuse instead of re-checking (§10.3). Empty string when absent; never
|
|
230
|
+
* rejects.
|
|
231
|
+
*/
|
|
232
|
+
public static void getNativeCheckCache(
|
|
233
|
+
final UpdateContext updateContext,
|
|
234
|
+
final Promise promise
|
|
235
|
+
) {
|
|
236
|
+
String cached = updateContext.getKv(NativeCheckOrchestrator.KEY_RESP_CACHE);
|
|
237
|
+
promise.resolve(cached == null ? "" : cached);
|
|
238
|
+
}
|
|
239
|
+
|
|
227
240
|
private static void setUuidInternal(UpdateContext updateContext, String uuid) {
|
|
228
241
|
updateContext.setKv("uuid", uuid);
|
|
229
242
|
}
|
|
230
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Provisioning for the native cold-start update check
|
|
246
|
+
* (NATIVE_CHECKUPDATE_DESIGN §10.1): the raw JSON persists as-is and is
|
|
247
|
+
* parsed on read by the orchestrator; absent config = check disabled.
|
|
248
|
+
* Validated at write time — a corrupt config would otherwise silently
|
|
249
|
+
* disable the native check forever with no signal.
|
|
250
|
+
*/
|
|
251
|
+
public static void syncNativeConfig(
|
|
252
|
+
final UpdateContext updateContext,
|
|
253
|
+
final String config,
|
|
254
|
+
final Promise promise
|
|
255
|
+
) {
|
|
256
|
+
if (config == null || config.isEmpty()) {
|
|
257
|
+
promise.reject(ErrorCodes.INVALID_OPTIONS, "config must be a JSON object string");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
new JSONObject(config);
|
|
262
|
+
} catch (JSONException e) {
|
|
263
|
+
promise.reject(ErrorCodes.INVALID_OPTIONS, "config must be a JSON object string", e);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
StateSerialRunner.run(promise, ErrorCodes.FILE_OPERATION_FAILED, "syncNativeConfig", new StateSerialRunner.Operation() {
|
|
267
|
+
@Override
|
|
268
|
+
public void run() {
|
|
269
|
+
updateContext.setKv(NativeCheckOrchestrator.KEY_CONFIG, config);
|
|
270
|
+
promise.resolve(true);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
231
275
|
public static void setUuid(
|
|
232
276
|
final UpdateContext updateContext,
|
|
233
277
|
final String uuid,
|
|
@@ -88,6 +88,16 @@ public class UpdateModule extends NativePushySpec {
|
|
|
88
88
|
UpdateModuleImpl.setUuid(updateContext, uuid, promise);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
@Override
|
|
92
|
+
public void syncNativeConfig(String config, Promise promise) {
|
|
93
|
+
UpdateModuleImpl.syncNativeConfig(updateContext, config, promise);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Override
|
|
97
|
+
public void getNativeCheckCache(Promise promise) {
|
|
98
|
+
UpdateModuleImpl.getNativeCheckCache(updateContext, promise);
|
|
99
|
+
}
|
|
100
|
+
|
|
91
101
|
@Override
|
|
92
102
|
public void setLocalHashInfo(String hash, String info, Promise promise) {
|
|
93
103
|
UpdateModuleImpl.setLocalHashInfo(updateContext, hash, info, promise);
|
|
@@ -90,6 +90,16 @@ public class UpdateModule extends ReactContextBaseJavaModule {
|
|
|
90
90
|
UpdateModuleImpl.setUuid(updateContext, uuid);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
@ReactMethod
|
|
94
|
+
public void syncNativeConfig(String config, Promise promise) {
|
|
95
|
+
UpdateModuleImpl.syncNativeConfig(updateContext, config, promise);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@ReactMethod
|
|
99
|
+
public void getNativeCheckCache(Promise promise) {
|
|
100
|
+
UpdateModuleImpl.getNativeCheckCache(updateContext, promise);
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
@ReactMethod
|
|
94
104
|
public void setLocalHashInfo(String hash, String info) {
|
|
95
105
|
UpdateModuleImpl.setLocalHashInfo(updateContext, hash, info);
|