react-native-update 10.55.1 → 10.56.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 (76) hide show
  1. package/README-CN.md +35 -0
  2. package/README.md +37 -0
  3. package/android/build.gradle +53 -27
  4. package/android/jni/Application.mk +6 -1
  5. package/android/jni/hpatch.c +59 -4
  6. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  7. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  8. package/android/lib/x86/librnupdate.so +0 -0
  9. package/android/lib/x86_64/librnupdate.so +0 -0
  10. package/android/proguard.pro +70 -14
  11. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
  12. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
  14. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
  17. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
  18. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
  21. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
  22. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
  23. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
  24. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
  25. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
  26. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
  27. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
  28. package/cpp/patch_core/archive_limits.h +6 -0
  29. package/cpp/patch_core/jni_util.h +6 -2
  30. package/cpp/patch_core/patch_core.cpp +237 -84
  31. package/cpp/patch_core/patch_core.h +11 -0
  32. package/cpp/patch_core/patch_core_android.cpp +49 -33
  33. package/cpp/patch_core/update_core_android.cpp +134 -103
  34. package/cpp/update_flow_core/flow_json.cpp +150 -17
  35. package/cpp/update_flow_core/flow_json.h +13 -1
  36. package/cpp/update_flow_core/update_flow_core.cpp +78 -8
  37. package/cpp/update_flow_core/update_flow_core.h +9 -0
  38. package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
  39. package/harmony/hvigor-plugin.ts +168 -21
  40. package/harmony/pushy/src/README.md +74 -0
  41. package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
  42. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
  43. package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
  44. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
  45. package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
  46. package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
  47. package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
  48. package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
  49. package/harmony/pushy/src/main/ets/Logger.ts +42 -16
  50. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
  51. package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
  52. package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
  53. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
  54. package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
  55. package/harmony/pushy/src/main/module.json5 +5 -1
  56. package/harmony/pushy.har +0 -0
  57. package/ios/PrivacyInfo.xcprivacy +79 -0
  58. package/ios/RCTPushy/RCTPushy.mm +399 -170
  59. package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
  60. package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
  61. package/package.json +14 -24
  62. package/react-native-update.podspec +43 -3
  63. package/src/client.ts +236 -82
  64. package/src/context.ts +40 -27
  65. package/src/core.ts +41 -16
  66. package/src/endpoint.ts +2 -1
  67. package/src/error.ts +49 -11
  68. package/src/index.ts +14 -0
  69. package/src/locales/en.ts +1 -23
  70. package/src/locales/zh.ts +1 -22
  71. package/src/provider.tsx +159 -111
  72. package/src/type.ts +0 -2
  73. package/src/updateFlowCore.ts +10 -2
  74. package/src/utils.ts +61 -4
  75. package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
  76. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
@@ -4,8 +4,8 @@ import android.content.Context;
4
4
  import android.content.SharedPreferences;
5
5
  import android.content.pm.PackageInfo;
6
6
  import android.content.pm.PackageManager;
7
- import android.os.Build;
8
7
  import android.util.Log;
8
+ import androidx.annotation.Nullable;
9
9
  import com.facebook.react.ReactInstanceManager;
10
10
  import java.io.File;
11
11
  import org.json.JSONObject;
@@ -15,6 +15,7 @@ import java.security.MessageDigest;
15
15
  import java.security.NoSuchAlgorithmException;
16
16
  import java.util.concurrent.Executor;
17
17
  import java.util.concurrent.Executors;
18
+ import java.util.concurrent.ThreadFactory;
18
19
 
19
20
  public class UpdateContext {
20
21
  static {
@@ -28,8 +29,15 @@ public class UpdateContext {
28
29
  private final File rootDir;
29
30
  private final Executor executor;
30
31
  private final SharedPreferences sp;
31
-
32
- private ReactInstanceManager reactInstanceManager;
32
+ // Our own PackageInfo, looked up once: every getPackageInfo call is a
33
+ // Binder IPC, and the constructor, the bundle-hash cache and the native
34
+ // check all need the version name / install time.
35
+ @Nullable
36
+ private final PackageInfo packageInfo;
37
+
38
+ // Written under sLock (setCustomInstanceManager), read on the restart
39
+ // path without it.
40
+ private volatile ReactInstanceManager reactInstanceManager;
33
41
  // Written on the launch path, read from executor/JS threads.
34
42
  private volatile boolean isUsingBundleUrl;
35
43
  private volatile boolean ignoreRollback;
@@ -38,12 +46,12 @@ public class UpdateContext {
38
46
  // update assets (images/fonts) are read from it on demand at runtime, so
39
47
  // wiping it under a silent (no-restart) reset would break every image the
40
48
  // running app has not loaded yet. Volatile: written during launch, read
41
- // from the state serial executor.
49
+ // from the state serial and download executors.
42
50
  private volatile String launchVersion;
43
51
  private static final int STATE_OP_SWITCH_VERSION = 1;
44
52
  private static final int STATE_OP_MARK_SUCCESS = 2;
45
53
  private static final int STATE_OP_ROLLBACK = 3;
46
- private static final int STATE_OP_CLEAR_FIRST_TIME = 4;
54
+ // 4 (clear first time) exists in the state core but has no Android caller.
47
55
  private static final int STATE_OP_CLEAR_ROLLBACK_MARK = 5;
48
56
  private static final int STATE_OP_RESOLVE_LAUNCH = 6;
49
57
  private static final String KEY_FIRST_LOAD_MARKED = "firstLoadMarked";
@@ -55,9 +63,21 @@ public class UpdateContext {
55
63
  // can never resurrect the version the app just reset away from.
56
64
  private static final java.util.concurrent.atomic.AtomicLong resetGeneration =
57
65
  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.
66
+ // The one mutex for every snapshot -> state core -> commit sequence
67
+ // (switchVersion, markSuccess, the launch resolution and its rollback,
68
+ // the launch markers, the cold-start round's commit,
69
+ // resetToPackagedBundle). They are read-modify-write cycles on the same
70
+ // SharedPreferences from different threads (state serial executor,
71
+ // native check thread, JS thread); without a common lock a commit built
72
+ // on an older snapshot silently undoes an earlier one — e.g. a JS
73
+ // markSuccess erasing the activation a crash-rescue round just committed
74
+ // (CODE_AUDIT 2.1). Intrinsic locks are reentrant, so nested takers such
75
+ // as getBundleUrl -> rollBack are fine. Nothing slow runs inside it: the
76
+ // bundle digest of a switch is verified before the lock is taken.
60
77
  private static final Object commitLock = new Object();
78
+ // Cleanup and activation share this lock from directory verification through
79
+ // deletion/state commit. Slow filesystem work still stays outside commitLock.
80
+ private static final Object versionFilesLock = new Object();
61
81
 
62
82
  // Singleton instance
63
83
  private static volatile UpdateContext sInstance;
@@ -80,7 +100,12 @@ public class UpdateContext {
80
100
 
81
101
  private UpdateContext(Context context) {
82
102
  this.context = context.getApplicationContext();
83
- this.executor = Executors.newSingleThreadExecutor();
103
+ this.executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
104
+ @Override
105
+ public Thread newThread(Runnable r) {
106
+ return new Thread(r, "pushy-download");
107
+ }
108
+ });
84
109
 
85
110
  this.rootDir = new File(this.context.getFilesDir(), "_update");
86
111
 
@@ -89,6 +114,7 @@ public class UpdateContext {
89
114
  }
90
115
 
91
116
  this.sp = this.context.getSharedPreferences("update", Context.MODE_PRIVATE);
117
+ this.packageInfo = lookupPackageInfo(this.context);
92
118
  this.reactInstanceManager = pendingReactInstanceManager;
93
119
 
94
120
  String packageVersion = getPackageVersion();
@@ -113,16 +139,20 @@ public class UpdateContext {
113
139
  return rootDir.toString();
114
140
  }
115
141
 
116
- public String getPackageVersion() {
117
- PackageManager pm = context.getPackageManager();
118
- PackageInfo pi = null;
142
+ @Nullable
143
+ private static PackageInfo lookupPackageInfo(Context context) {
119
144
  try {
120
- pi = pm.getPackageInfo(context.getPackageName(), 0);
121
- return pi.versionName;
122
- } catch( PackageManager.NameNotFoundException e) {
123
- e.printStackTrace();
145
+ return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
146
+ } catch (PackageManager.NameNotFoundException e) {
147
+ // Our own package is always installed; a failure here is a
148
+ // platform bug worth a log line, not a crash on the launch path.
149
+ Log.e(TAG, "Unable to read own package info", e);
150
+ return null;
124
151
  }
125
- return null;
152
+ }
153
+
154
+ public String getPackageVersion() {
155
+ return packageInfo == null ? null : packageInfo.versionName;
126
156
  }
127
157
 
128
158
  public String getBuildTime() {
@@ -188,13 +218,7 @@ public class UpdateContext {
188
218
  }
189
219
 
190
220
  private long getPackageLastUpdateTime() {
191
- try {
192
- PackageInfo pi = context.getPackageManager()
193
- .getPackageInfo(context.getPackageName(), 0);
194
- return pi.lastUpdateTime;
195
- } catch (PackageManager.NameNotFoundException e) {
196
- return 0;
197
- }
221
+ return packageInfo == null ? 0 : packageInfo.lastUpdateTime;
198
222
  }
199
223
 
200
224
  private String sha256OfBundledAsset(String assetName) {
@@ -224,21 +248,8 @@ public class UpdateContext {
224
248
  executor.execute(new DownloadTask(context, params));
225
249
  }
226
250
 
227
- // Server-provided identifiers (hash/originHash/fileName) become child
228
- // names under rootDir; anything that could resolve outside of it (path
229
- // separators, "..", ".") must be rejected before touching the filesystem.
230
- static boolean isSafePathComponent(String name) {
231
- return name != null
232
- && !name.isEmpty()
233
- && !name.equals(".")
234
- && !name.equals("..")
235
- && !name.contains("/")
236
- && !name.contains("\\")
237
- && name.indexOf('\0') < 0;
238
- }
239
-
240
251
  private static boolean rejectUnsafeComponent(String name, DownloadFileListener listener) {
241
- if (isSafePathComponent(name)) {
252
+ if (UpdateFileUtils.isSafePathComponent(name)) {
242
253
  return false;
243
254
  }
244
255
  listener.onDownloadFailed(new IllegalArgumentException("Invalid path component: " + name));
@@ -254,11 +265,18 @@ public class UpdateContext {
254
265
  downloadFullUpdate(url, hash, listener, 0);
255
266
  }
256
267
 
257
- void downloadFullUpdate(
268
+ /**
269
+ * The deadline-taking overloads (native cold-start check) return the
270
+ * queued task's params so the orchestrator can cancel the task when its
271
+ * phase budget expires (CODE_AUDIT 2.12); null when the request was
272
+ * rejected up front (the listener has already been told).
273
+ */
274
+ @Nullable
275
+ DownloadTaskParams downloadFullUpdate(
258
276
  String url, String hash, DownloadFileListener listener, long deadlineNanos
259
277
  ) {
260
278
  if (rejectUnsafeComponent(hash, listener)) {
261
- return;
279
+ return null;
262
280
  }
263
281
  DownloadTaskParams params = new DownloadTaskParams();
264
282
  params.type = DownloadTaskParams.TASK_TYPE_PATCH_FULL;
@@ -269,6 +287,7 @@ public class UpdateContext {
269
287
  params.targetFile = new File(rootDir, hash + ".ppk");
270
288
  params.unzipDirectory = new File(rootDir, hash);
271
289
  enqueue(params);
290
+ return params;
272
291
  }
273
292
 
274
293
  public void downloadFile(String url, String hash, String fileName, DownloadFileListener listener) {
@@ -290,11 +309,12 @@ public class UpdateContext {
290
309
  downloadPatchFromApk(url, hash, listener, 0);
291
310
  }
292
311
 
293
- void downloadPatchFromApk(
312
+ @Nullable
313
+ DownloadTaskParams downloadPatchFromApk(
294
314
  String url, String hash, DownloadFileListener listener, long deadlineNanos
295
315
  ) {
296
316
  if (rejectUnsafeComponent(hash, listener)) {
297
- return;
317
+ return null;
298
318
  }
299
319
  DownloadTaskParams params = new DownloadTaskParams();
300
320
  params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK;
@@ -305,13 +325,15 @@ public class UpdateContext {
305
325
  params.targetFile = new File(rootDir, hash + ".apk.patch");
306
326
  params.unzipDirectory = new File(rootDir, hash);
307
327
  enqueue(params);
328
+ return params;
308
329
  }
309
330
 
310
331
  public void downloadPatchFromPpk(String url, String hash, String originHash, DownloadFileListener listener) {
311
332
  downloadPatchFromPpk(url, hash, originHash, listener, 0);
312
333
  }
313
334
 
314
- void downloadPatchFromPpk(
335
+ @Nullable
336
+ DownloadTaskParams downloadPatchFromPpk(
315
337
  String url,
316
338
  String hash,
317
339
  String originHash,
@@ -319,7 +341,7 @@ public class UpdateContext {
319
341
  long deadlineNanos
320
342
  ) {
321
343
  if (rejectUnsafeComponent(hash, listener) || rejectUnsafeComponent(originHash, listener)) {
322
- return;
344
+ return null;
323
345
  }
324
346
  DownloadTaskParams params = new DownloadTaskParams();
325
347
  params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
@@ -332,6 +354,7 @@ public class UpdateContext {
332
354
  params.unzipDirectory = new File(rootDir, hash);
333
355
  params.originDirectory = new File(rootDir, originHash);
334
356
  enqueue(params);
357
+ return params;
335
358
  }
336
359
 
337
360
  private StateCoreResult getStateSnapshot() {
@@ -396,12 +419,14 @@ public class UpdateContext {
396
419
  }
397
420
 
398
421
  /**
399
- * Validates that {@code hash} is a switchable version and computes the
400
- * resulting state without persisting it, so callers can fold the switch
401
- * into a larger single-commit transaction.
422
+ * Validates that {@code hash} is a switchable version: the slow half of
423
+ * a switch (file checks and the bundle digest of a multi-MB bundle),
424
+ * deliberately run without commitLock. The state snapshot read here only
425
+ * decides whether a markerless directory is grandfathered; a concurrent
426
+ * commit cannot make an unverified directory pass.
402
427
  */
403
- private StateCoreResult computeSwitchVersion(String hash) {
404
- if (!isSafePathComponent(hash)) {
428
+ private void verifySwitchTarget(String hash) {
429
+ if (!UpdateFileUtils.isSafePathComponent(hash)) {
405
430
  throw new IllegalArgumentException("Invalid hash: " + hash);
406
431
  }
407
432
  File versionDir = new File(rootDir, hash);
@@ -412,24 +437,34 @@ public class UpdateContext {
412
437
  StateCoreResult currentState = getStateSnapshot();
413
438
  boolean isLegacyActivatedVersion = hash.equals(currentState.currentVersion)
414
439
  || hash.equals(currentState.lastVersion);
415
- if (!InstallRecord.isComplete(versionDir, hash) && !isLegacyActivatedVersion) {
440
+ if (isLegacyActivatedVersion) {
416
441
  // Versions activated before completion markers were introduced are
417
- // explicitly grandfathered through current/last state. An arbitrary
418
- // markerless directory may be a crash-left partial install.
442
+ // explicitly grandfathered through current/last state.
443
+ return;
444
+ }
445
+ if (!InstallRecord.isComplete(versionDir, hash)) {
446
+ // An arbitrary markerless directory may be a crash-left partial
447
+ // install.
419
448
  throw new IllegalStateException("Bundle version " + hash + " is incomplete.");
420
449
  }
421
- if (!isLegacyActivatedVersion) {
422
- // The record's bundle digest must match the bytes on disk before
423
- // the next launch is pointed at them.
424
- try {
425
- InstallRecord.verifyForActivation(versionDir, hash, bundleFile);
426
- } catch (IOException e) {
427
- throw new IllegalStateException(e.getMessage(), e);
428
- }
450
+ // The record's bundle digest must match the bytes on disk before
451
+ // the next launch is pointed at them.
452
+ try {
453
+ InstallRecord.verifyForActivation(versionDir, hash, bundleFile);
454
+ } catch (IOException e) {
455
+ throw new IllegalStateException(e.getMessage(), e);
429
456
  }
457
+ }
458
+
459
+ /**
460
+ * The fast half of a switch: snapshot -> state core, without persisting,
461
+ * so callers can fold it into a larger single-commit transaction. Caller
462
+ * holds commitLock and has run verifySwitchTarget.
463
+ */
464
+ private StateCoreResult computeSwitchState(String hash) {
430
465
  return runStateCore(
431
466
  STATE_OP_SWITCH_VERSION,
432
- currentState,
467
+ getStateSnapshot(),
433
468
  hash,
434
469
  false,
435
470
  false
@@ -437,11 +472,16 @@ public class UpdateContext {
437
472
  }
438
473
 
439
474
  public void switchVersion(String hash) {
440
- StateCoreResult nextState = computeSwitchVersion(hash);
441
- SharedPreferences.Editor editor = sp.edit();
442
- applyState(editor, nextState);
443
- persistEditorOrThrow(editor, "switch version");
444
- ignoreRollback = false;
475
+ synchronized (versionFilesLock) {
476
+ verifySwitchTarget(hash);
477
+ synchronized (commitLock) {
478
+ StateCoreResult nextState = computeSwitchState(hash);
479
+ SharedPreferences.Editor editor = sp.edit();
480
+ applyState(editor, nextState);
481
+ persistEditorOrThrow(editor, "switch version");
482
+ ignoreRollback = false;
483
+ }
484
+ }
445
485
  }
446
486
 
447
487
  public void setKv(String key, String value) {
@@ -464,18 +504,53 @@ public class UpdateContext {
464
504
  return sp.getString("currentVersion", null);
465
505
  }
466
506
 
467
- public boolean isFirstTime() {
468
- return sp.getBoolean("firstTime", false);
507
+ /** The one-shot launch markers getConstants reports, consumed together. */
508
+ static final class LaunchMarkers {
509
+ final boolean isFirstTime;
510
+ @Nullable
511
+ final String rolledBackVersion;
512
+
513
+ LaunchMarkers(boolean isFirstTime, @Nullable String rolledBackVersion) {
514
+ this.isFirstTime = isFirstTime;
515
+ this.rolledBackVersion = rolledBackVersion;
516
+ }
469
517
  }
470
518
 
471
- public boolean consumeFirstLoadMarker() {
472
- boolean isFirstLoadMarked = sp.getBoolean(KEY_FIRST_LOAD_MARKED, false);
473
- if (isFirstLoadMarked) {
474
- SharedPreferences.Editor editor = sp.edit();
475
- editor.remove(KEY_FIRST_LOAD_MARKED);
476
- persistEditor(editor, "clear first load marker");
519
+ /**
520
+ * Reads and clears the first-load marker and the rollback mark in one
521
+ * commit. getConstants runs this synchronously on the JS thread (a
522
+ * TurboModule constant read), so the two writes are folded into a single
523
+ * editor; persistence is best-effort on this launch path.
524
+ */
525
+ LaunchMarkers consumeLaunchMarkers() {
526
+ LaunchMarkers markers;
527
+ synchronized (commitLock) {
528
+ boolean isFirstLoadMarked = sp.getBoolean(KEY_FIRST_LOAD_MARKED, false);
529
+ String rolledBackVersion = rolledBackVersion();
530
+ markers = new LaunchMarkers(isFirstLoadMarked, rolledBackVersion);
531
+ if (isFirstLoadMarked || rolledBackVersion != null) {
532
+ SharedPreferences.Editor editor = sp.edit();
533
+ if (isFirstLoadMarked) {
534
+ editor.remove(KEY_FIRST_LOAD_MARKED);
535
+ }
536
+ if (rolledBackVersion != null) {
537
+ StateCoreResult nextState = runStateCore(
538
+ STATE_OP_CLEAR_ROLLBACK_MARK,
539
+ getStateSnapshot(),
540
+ null,
541
+ false,
542
+ false
543
+ );
544
+ applyState(editor, nextState);
545
+ }
546
+ persistEditor(editor, "consume launch markers");
547
+ }
477
548
  }
478
- return isFirstLoadMarked;
549
+ if (markers.rolledBackVersion != null) {
550
+ // The rolled-back version's directory is now eligible for cleanup.
551
+ this.cleanUp();
552
+ }
553
+ return markers;
479
554
  }
480
555
 
481
556
  public String rolledBackVersion() {
@@ -483,7 +558,10 @@ public class UpdateContext {
483
558
  }
484
559
 
485
560
  public void markSuccess() {
486
- if (!BuildConfig.DEBUG) {
561
+ if (BuildConfig.DEBUG) {
562
+ return;
563
+ }
564
+ synchronized (commitLock) {
487
565
  StateCoreResult currentState = getStateSnapshot();
488
566
  StateCoreResult nextState = runStateCore(
489
567
  STATE_OP_MARK_SUCCESS,
@@ -498,24 +576,7 @@ public class UpdateContext {
498
576
  editor.remove("hash_" + nextState.staleVersionToDelete);
499
577
  }
500
578
  persistEditorOrThrow(editor, "mark success");
501
-
502
- this.cleanUp();
503
579
  }
504
- }
505
-
506
- public void clearFirstTime() {
507
- StateCoreResult currentState = getStateSnapshot();
508
- StateCoreResult nextState = runStateCore(
509
- STATE_OP_CLEAR_FIRST_TIME,
510
- currentState,
511
- null,
512
- false,
513
- false
514
- );
515
- SharedPreferences.Editor editor = sp.edit();
516
- applyState(editor, nextState);
517
- editor.remove(KEY_FIRST_LOAD_MARKED);
518
- persistEditorOrThrow(editor, "clear first time");
519
580
 
520
581
  this.cleanUp();
521
582
  }
@@ -560,32 +621,12 @@ public class UpdateContext {
560
621
  DownloadTaskParams params = new DownloadTaskParams();
561
622
  params.type = DownloadTaskParams.TASK_TYPE_CLEANUP;
562
623
  params.maxAgeDays = 0;
563
- // Keep the directory of the version this process is running from (a
564
- // silent reset would otherwise break its on-demand asset loads); the
565
- // orphaned directory is removed by the next regular cleanup.
566
- params.hash = launchVersion;
624
+ // Keep names are resolved by the queued task at execution time, so a
625
+ // later activation is protected too.
567
626
  params.unzipDirectory = rootDir;
568
627
  enqueue(params);
569
628
  }
570
629
 
571
- public void clearRollbackMark() {
572
- StateCoreResult currentState = getStateSnapshot();
573
- StateCoreResult nextState = runStateCore(
574
- STATE_OP_CLEAR_ROLLBACK_MARK,
575
- currentState,
576
- null,
577
- false,
578
- false
579
- );
580
- SharedPreferences.Editor editor = sp.edit();
581
- applyState(editor, nextState);
582
- // Runs from getConstants on the launch path: best-effort only.
583
- persistEditor(editor, "clear rollback mark");
584
-
585
- this.cleanUp();
586
- }
587
-
588
-
589
630
  public static void setCustomInstanceManager(ReactInstanceManager instanceManager) {
590
631
  synchronized (sLock) {
591
632
  pendingReactInstanceManager = instanceManager;
@@ -629,59 +670,64 @@ public class UpdateContext {
629
670
  isUsingBundleUrl = true;
630
671
  String nativeCheckRolledBackVersion = null;
631
672
  try {
632
- StateCoreResult currentState = getStateSnapshot();
633
- StateCoreResult launchState = runStateCore(
634
- STATE_OP_RESOLVE_LAUNCH,
635
- currentState,
636
- null,
637
- ignoreRollback,
638
- true
639
- );
640
- nativeCheckRolledBackVersion = launchState.rolledBackVersion;
641
- if (launchState.didRollback) {
642
- // The crash-protection rollback: the new version never called
643
- // markSuccess. Keep this visible in release logs.
644
- Log.e(TAG, "Version " + currentState.currentVersion
645
- + " was not marked as successful, rolling back to "
646
- + launchState.currentVersion);
647
- }
648
- if (launchState.didRollback || launchState.consumedFirstTime) {
649
- SharedPreferences.Editor editor = sp.edit();
650
- applyState(editor, launchState);
673
+ // The whole resolution is one read-modify-write on the state
674
+ // (resolve, then possibly roll back missing bundles); see
675
+ // commitLock.
676
+ synchronized (commitLock) {
677
+ StateCoreResult currentState = getStateSnapshot();
678
+ StateCoreResult launchState = runStateCore(
679
+ STATE_OP_RESOLVE_LAUNCH,
680
+ currentState,
681
+ null,
682
+ ignoreRollback,
683
+ true
684
+ );
685
+ nativeCheckRolledBackVersion = launchState.rolledBackVersion;
686
+ if (launchState.didRollback) {
687
+ // The crash-protection rollback: the new version never called
688
+ // markSuccess. Keep this visible in release logs.
689
+ Log.e(TAG, "Version " + currentState.currentVersion
690
+ + " was not marked as successful, rolling back to "
691
+ + launchState.currentVersion);
692
+ }
693
+ if (launchState.didRollback || launchState.consumedFirstTime) {
694
+ SharedPreferences.Editor editor = sp.edit();
695
+ applyState(editor, launchState);
696
+ if (launchState.consumedFirstTime) {
697
+ editor.putBoolean(KEY_FIRST_LOAD_MARKED, true);
698
+ }
699
+ persistEditor(editor, "resolve launch");
700
+ }
651
701
  if (launchState.consumedFirstTime) {
652
- editor.putBoolean(KEY_FIRST_LOAD_MARKED, true);
702
+ // bundleURL may be resolved multiple times in one process.
703
+ ignoreRollback = true;
653
704
  }
654
- persistEditor(editor, "resolve launch");
655
- }
656
- if (launchState.consumedFirstTime) {
657
- // bundleURL may be resolved multiple times in one process.
658
- ignoreRollback = true;
659
- }
660
705
 
661
- String currentVersion = launchState.loadVersion;
662
- if (currentVersion == null) {
663
- return defaultAssetsUrl;
664
- }
706
+ String currentVersion = launchState.loadVersion;
707
+ if (currentVersion == null) {
708
+ return defaultAssetsUrl;
709
+ }
665
710
 
666
- // Guard the rollback chain against cycles: a corrupted state returning
667
- // an already-visited version would otherwise spin this loop forever on
668
- // the main thread.
669
- java.util.HashSet<String> visitedVersions = new java.util.HashSet<>();
670
- while (currentVersion != null && visitedVersions.add(currentVersion)) {
671
- File bundleFile = new File(rootDir, currentVersion+"/index.bundlejs");
672
- if (!bundleFile.exists()) {
673
- Log.e(TAG, "Bundle version " + currentVersion + " not found.");
674
- currentVersion = this.rollBack();
711
+ // Guard the rollback chain against cycles: a corrupted state returning
712
+ // an already-visited version would otherwise spin this loop forever on
713
+ // the main thread.
714
+ java.util.HashSet<String> visitedVersions = new java.util.HashSet<>();
715
+ while (currentVersion != null && visitedVersions.add(currentVersion)) {
716
+ File bundleFile = new File(rootDir, currentVersion+"/index.bundlejs");
717
+ if (!bundleFile.exists()) {
718
+ Log.e(TAG, "Bundle version " + currentVersion + " not found.");
719
+ currentVersion = this.rollBack();
720
+ nativeCheckRolledBackVersion = rolledBackVersion();
721
+ continue;
722
+ }
723
+ launchVersion = currentVersion;
675
724
  nativeCheckRolledBackVersion = rolledBackVersion();
676
- continue;
725
+ return bundleFile.toString();
677
726
  }
678
- launchVersion = currentVersion;
727
+
679
728
  nativeCheckRolledBackVersion = rolledBackVersion();
680
- return bundleFile.toString();
729
+ return defaultAssetsUrl;
681
730
  }
682
-
683
- nativeCheckRolledBackVersion = rolledBackVersion();
684
- return defaultAssetsUrl;
685
731
  } finally {
686
732
  // Even corrupted state or a state-core exception must not disable
687
733
  // the next-launch rescue check. A null snapshot simply omits the
@@ -708,22 +754,36 @@ public class UpdateContext {
708
754
  String hashInfoJson,
709
755
  boolean activate,
710
756
  String responseCacheJson
757
+ ) {
758
+ boolean switching = activate && hash != null;
759
+ if (!switching) {
760
+ return commitNativeCheckResultState(
761
+ expectedGeneration, hash, hashInfoJson, false, responseCacheJson);
762
+ }
763
+ synchronized (versionFilesLock) {
764
+ verifySwitchTarget(hash);
765
+ return commitNativeCheckResultState(
766
+ expectedGeneration, hash, hashInfoJson, true, responseCacheJson);
767
+ }
768
+ }
769
+
770
+ private boolean commitNativeCheckResultState(
771
+ long expectedGeneration,
772
+ String hash,
773
+ String hashInfoJson,
774
+ boolean switching,
775
+ String responseCacheJson
711
776
  ) {
712
777
  synchronized (commitLock) {
713
778
  if (resetGeneration.get() != expectedGeneration) {
714
779
  return false;
715
780
  }
716
- // One editor, one commit: the version info, the activation and
717
- // the response cache land together or not at all. Three separate
718
- // commits under the same lock only prevented interleaving, not a
719
- // half-written round after a failed or interrupted write.
720
781
  SharedPreferences.Editor editor = sp.edit();
721
782
  if (hash != null && hashInfoJson != null) {
722
783
  editor.putString("hash_" + hash, hashInfoJson);
723
784
  }
724
- boolean switching = activate && hash != null;
725
785
  if (switching) {
726
- applyState(editor, computeSwitchVersion(hash));
786
+ applyState(editor, computeSwitchState(hash));
727
787
  }
728
788
  if (responseCacheJson != null) {
729
789
  editor.putString(NativeCheckOrchestrator.KEY_RESP_CACHE, responseCacheJson);
@@ -742,7 +802,7 @@ public class UpdateContext {
742
802
  * unreadable record. Exposed to JS for crash-report attribution.
743
803
  */
744
804
  String currentBundleSha256(String hash) {
745
- if (hash == null || hash.isEmpty() || !isSafePathComponent(hash)) {
805
+ if (hash == null || hash.isEmpty() || !UpdateFileUtils.isSafePathComponent(hash)) {
746
806
  return "";
747
807
  }
748
808
  JSONObject record = InstallRecord.read(new File(rootDir, hash));
@@ -750,7 +810,7 @@ public class UpdateContext {
750
810
  }
751
811
 
752
812
  boolean hasCompletedVersion(String hash) {
753
- if (!isSafePathComponent(hash)) {
813
+ if (!UpdateFileUtils.isSafePathComponent(hash)) {
754
814
  return false;
755
815
  }
756
816
  File versionDir = new File(rootDir, hash);
@@ -759,27 +819,63 @@ public class UpdateContext {
759
819
  }
760
820
 
761
821
  private String rollBack() {
762
- StateCoreResult currentState = getStateSnapshot();
763
- StateCoreResult nextState = runStateCore(
764
- STATE_OP_ROLLBACK,
765
- currentState,
766
- null,
767
- false,
768
- false
769
- );
770
- Log.e(TAG, "Rolling back version " + currentState.currentVersion
771
- + " to " + nextState.currentVersion);
772
- SharedPreferences.Editor editor = sp.edit();
773
- applyState(editor, nextState);
774
- persistEditor(editor, "rollback");
775
- return nextState.currentVersion;
822
+ synchronized (commitLock) {
823
+ StateCoreResult currentState = getStateSnapshot();
824
+ StateCoreResult nextState = runStateCore(
825
+ STATE_OP_ROLLBACK,
826
+ currentState,
827
+ null,
828
+ false,
829
+ false
830
+ );
831
+ Log.e(TAG, "Rolling back version " + currentState.currentVersion
832
+ + " to " + nextState.currentVersion);
833
+ SharedPreferences.Editor editor = sp.edit();
834
+ applyState(editor, nextState);
835
+ persistEditor(editor, "rollback");
836
+ return nextState.currentVersion;
837
+ }
838
+ }
839
+
840
+ /**
841
+ * The version this process is running from (null before getBundleUrl
842
+ * resolved one, or when the packaged bundle runs). Read by DownloadTask
843
+ * to refuse reinstalling that directory in place.
844
+ */
845
+ @Nullable
846
+ static String runningVersion() {
847
+ UpdateContext instance = sInstance;
848
+ return instance == null ? null : instance.launchVersion;
849
+ }
850
+
851
+ interface CleanupAction {
852
+ void run(@Nullable String keepCurrent, @Nullable String keepPrevious);
853
+ }
854
+
855
+ /** Resolve keep names immediately before deletion, never when enqueued. */
856
+ void runCleanupWithLatestState(CleanupAction action) {
857
+ synchronized (versionFilesLock) {
858
+ final String[] keepNames;
859
+ synchronized (commitLock) {
860
+ keepNames = CleanupKeepNames.select(
861
+ sp.getString("currentVersion", null),
862
+ sp.getString("lastVersion", null),
863
+ launchVersion
864
+ );
865
+ }
866
+ if (keepNames == null) {
867
+ // JNI has only two keep slots; three distinct live versions are
868
+ // safer to defer until the next process than to delete one.
869
+ Log.i(TAG, "Skipping cleanup: three live versions need protection");
870
+ return;
871
+ }
872
+ action.run(keepNames[0], keepNames[1]);
873
+ }
776
874
  }
777
875
 
778
876
  private void cleanUp() {
779
877
  DownloadTaskParams params = new DownloadTaskParams();
780
878
  params.type = DownloadTaskParams.TASK_TYPE_CLEANUP;
781
- params.hash = sp.getString("currentVersion", null);
782
- params.originHash = sp.getString("lastVersion", null);
783
879
  params.unzipDirectory = rootDir;
784
880
  enqueue(params);
785
881
  }