react-native-update 10.59.1 → 10.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README-CN.md +7 -7
  2. package/README.md +7 -6
  3. package/android/build.gradle +25 -33
  4. package/android/jni/Android.mk +16 -24
  5. package/android/lib/arm64-v8a/librnpushy.so +0 -0
  6. package/android/lib/armeabi-v7a/librnpushy.so +0 -0
  7. package/android/lib/x86/librnpushy.so +0 -0
  8. package/android/lib/x86_64/librnpushy.so +0 -0
  9. package/android/proguard.pro +9 -7
  10. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +11 -12
  11. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
  12. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
  13. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
  14. package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
  17. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
  18. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
  21. package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
  22. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
  23. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
  24. package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
  25. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
  26. package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
  27. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
  28. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
  29. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
  30. package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
  31. package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
  32. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
  33. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
  34. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
  35. package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
  36. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
  37. package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
  38. package/cpp/android_jni/archive_limits_jni.cpp +83 -0
  39. package/cpp/android_jni/install_record_jni.cpp +89 -0
  40. package/cpp/android_jni/jni_log.h +31 -0
  41. package/cpp/android_jni/jni_natives.h +40 -0
  42. package/cpp/android_jni/jni_registration.cpp +133 -0
  43. package/cpp/android_jni/jni_util.h +160 -0
  44. package/cpp/android_jni/update_core_android.cpp +450 -0
  45. package/cpp/android_jni/update_flow_jni.cpp +133 -0
  46. package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
  47. package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
  48. package/cpp/patch_core/archive_limits.cpp +194 -0
  49. package/cpp/patch_core/archive_limits.h +99 -10
  50. package/cpp/patch_core/archive_patch_core.cpp +96 -12
  51. package/cpp/patch_core/archive_patch_core.h +49 -0
  52. package/cpp/patch_core/digest.h +5 -4
  53. package/cpp/patch_core/error_codes.h +5 -3
  54. package/cpp/patch_core/install_record.cpp +430 -0
  55. package/cpp/patch_core/install_record.h +163 -9
  56. package/cpp/patch_core/patch_core.cpp +62 -16
  57. package/cpp/patch_core/patch_core.h +27 -7
  58. package/cpp/patch_core/patch_manifest.cpp +201 -0
  59. package/cpp/patch_core/patch_manifest.h +74 -0
  60. package/cpp/patch_core/state_ops.h +3 -3
  61. package/cpp/update_flow_core/flow_json.cpp +82 -8
  62. package/cpp/update_flow_core/flow_json.h +21 -0
  63. package/cpp/update_flow_core/native_config.cpp +456 -0
  64. package/cpp/update_flow_core/native_config.h +84 -0
  65. package/cpp/update_flow_core/native_round.cpp +326 -0
  66. package/cpp/update_flow_core/native_round.h +90 -0
  67. package/cpp/update_flow_core/update_flow_core.h +2 -2
  68. package/harmony/hvigor-plugin.ts +11 -19
  69. package/harmony/pushy/src/main/cpp/CMakeLists.txt +19 -26
  70. package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
  71. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
  72. package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
  73. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
  74. package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
  75. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
  76. package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
  77. package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
  78. package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
  79. package/harmony/pushy/src/main/ets/Logger.ts +20 -34
  80. package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
  81. package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
  82. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
  83. package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
  84. package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
  85. package/harmony/pushy/src/main/ets/Texts.ts +0 -4
  86. package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
  87. package/harmony/pushy.har +0 -0
  88. package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
  89. package/ios/RCTPushy/RCTPushy.h +12 -0
  90. package/ios/RCTPushy/RCTPushy.mm +87 -2628
  91. package/ios/RCTPushy/RCTPushyCommon.h +34 -0
  92. package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
  93. package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
  94. package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
  95. package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
  96. package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
  97. package/ios/RCTPushy/RCTPushyInternal.h +144 -0
  98. package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
  99. package/ios/RCTPushy/RCTPushyState.mm +314 -0
  100. package/package.json +25 -23
  101. package/react-native-update.podspec +74 -83
  102. package/src/client.ts +361 -451
  103. package/src/context.ts +1 -1
  104. package/src/core.ts +35 -8
  105. package/src/endpoint.ts +0 -2
  106. package/src/error.ts +13 -32
  107. package/src/i18n.ts +18 -83
  108. package/src/locales/en.ts +0 -4
  109. package/src/locales/zh.ts +0 -4
  110. package/src/provider.tsx +60 -53
  111. package/src/telemetry.ts +0 -13
  112. package/src/type.ts +0 -15
  113. package/src/updateFlowCore.ts +116 -10
  114. package/src/utils.ts +49 -42
  115. package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
  116. package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
  117. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
  118. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
  119. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
  120. package/cpp/patch_core/jni_natives.h +0 -27
  121. package/cpp/patch_core/jni_registration.cpp +0 -103
  122. package/cpp/patch_core/jni_util.h +0 -60
  123. package/cpp/patch_core/patch_core_android.cpp +0 -97
  124. package/cpp/patch_core/update_core_android.cpp +0 -485
  125. package/cpp/update_flow_core/update_flow_jni.cpp +0 -150
@@ -8,27 +8,26 @@ import android.util.Log;
8
8
  import androidx.annotation.Nullable;
9
9
  import com.facebook.react.ReactInstanceManager;
10
10
  import java.io.File;
11
- import org.json.JSONObject;
12
11
  import java.io.IOException;
13
12
  import java.io.InputStream;
14
- import java.security.MessageDigest;
15
- import java.security.NoSuchAlgorithmException;
16
13
  import java.util.concurrent.Executor;
17
- import java.util.concurrent.Executors;
18
- import java.util.concurrent.ThreadFactory;
19
14
 
20
15
  public class UpdateContext {
16
+ // The one place that loads librnpushy.so (see NativeCore).
21
17
  static {
22
18
  NativeCore.ensureLoaded();
23
19
  }
24
20
 
25
21
  static final String TAG = Texts.LOG_TAG;
26
- static final boolean DEBUG = BuildConfig.DEBUG;
27
22
 
28
23
  private final Context context;
29
24
  private final File rootDir;
30
25
  private final Executor executor;
31
26
  private final SharedPreferences sp;
27
+ // The native check's raw response cache (SyncCoordinator.KEY_RESP_CACHE)
28
+ // lives in its own file: it is a whole server response, and keeping it in
29
+ // sp made every small state commit rewrite it.
30
+ private final SharedPreferences checkCacheSp;
32
31
  // Our own PackageInfo, looked up once: every getPackageInfo call is a
33
32
  // Binder IPC, and the constructor, the bundle-hash cache and the native
34
33
  // check all need the version name / install time.
@@ -48,6 +47,16 @@ public class UpdateContext {
48
47
  // running app has not loaded yet. Volatile: written during launch, read
49
48
  // from the state serial and download executors.
50
49
  private volatile String launchVersion;
50
+ // bundleSha256 of launchVersion's install record, read once at launch
51
+ // resolution so getConstants does not hit the file system on the JS
52
+ // thread. {hash, sha256}; null until a version was resolved.
53
+ private volatile String[] launchBundleSha256;
54
+ // bundleHash of the embedded bundle, computed once per process on its own
55
+ // thread (see startBundleHashComputation); null until known.
56
+ private final Object bundleHashLock = new Object();
57
+ private String bundleHash;
58
+ private java.util.ArrayList<BundleHashListener> pendingBundleHashListeners =
59
+ new java.util.ArrayList<>();
51
60
  private static final int STATE_OP_SWITCH_VERSION = 1;
52
61
  private static final int STATE_OP_MARK_SUCCESS = 2;
53
62
  private static final int STATE_OP_ROLLBACK = 3;
@@ -55,7 +64,6 @@ public class UpdateContext {
55
64
  private static final int STATE_OP_CLEAR_ROLLBACK_MARK = 5;
56
65
  private static final int STATE_OP_RESOLVE_LAUNCH = 6;
57
66
  private static final String KEY_FIRST_LOAD_MARKED = "firstLoadMarked";
58
- static final String VERSION_COMPLETE_FILE = InstallRecord.FILE_NAME;
59
67
  // Bumped by resetToPackagedBundle. The cold-start check runs for minutes
60
68
  // and may already hold a decision when the app resets to the packaged
61
69
  // bundle; the orchestrator samples this counter and abandons activation
@@ -100,12 +108,7 @@ public class UpdateContext {
100
108
 
101
109
  private UpdateContext(Context context) {
102
110
  this.context = context.getApplicationContext();
103
- this.executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
104
- @Override
105
- public Thread newThread(Runnable r) {
106
- return new Thread(r, "pushy-download");
107
- }
108
- });
111
+ this.executor = Threads.single("pushy-download", false);
109
112
 
110
113
  this.rootDir = new File(this.context.getFilesDir(), Texts.reveal("0502e4d5af9f6d"));
111
114
 
@@ -114,6 +117,8 @@ public class UpdateContext {
114
117
  }
115
118
 
116
119
  this.sp = this.context.getSharedPreferences(Texts.reveal("2f07f0d0ba8e"), Context.MODE_PRIVATE);
120
+ this.checkCacheSp = this.context.getSharedPreferences(
121
+ Texts.reveal("2f07f0d0ba8e") + "_check", Context.MODE_PRIVATE);
117
122
  this.packageInfo = lookupPackageInfo(this.context);
118
123
  this.reactInstanceManager = pendingReactInstanceManager;
119
124
 
@@ -127,11 +132,28 @@ public class UpdateContext {
127
132
 
128
133
  if (nextState.changed) {
129
134
  // Execute cleanUp before clearing SharedPreferences to avoid race condition
130
- this.cleanUp();
135
+ cleanUp(DownloadTaskParams.DEFAULT_MAX_AGE_DAYS);
131
136
  SharedPreferences.Editor editor = this.sp.edit();
132
137
  editor.clear();
133
138
  applyState(editor, nextState);
134
139
  persistEditor(editor, "sync state with binary version");
140
+ clearNativeCheckCache();
141
+ } else {
142
+ migrateNativeCheckCache();
143
+ }
144
+ startBundleHashComputation();
145
+ }
146
+
147
+ // One-time move of the response cache out of the main state file.
148
+ private void migrateNativeCheckCache() {
149
+ String legacy = sp.getString(SyncCoordinator.KEY_RESP_CACHE, null);
150
+ if (legacy == null) {
151
+ return;
152
+ }
153
+ if (checkCacheSp.edit().putString(SyncCoordinator.KEY_RESP_CACHE, legacy).commit()) {
154
+ SharedPreferences.Editor editor = sp.edit();
155
+ editor.remove(SyncCoordinator.KEY_RESP_CACHE);
156
+ persistEditor(editor, "migrate check cache");
135
157
  }
136
158
  }
137
159
 
@@ -177,32 +199,87 @@ public class UpdateContext {
177
199
  * identity of the binary itself, not of whatever hot update is currently
178
200
  * running. Hashes exactly the bytes pdiff patches from: the hardcoded
179
201
  * "index.android.bundle" asset read via AssetManager, same as
180
- * DownloadTask.copyBundledAssetToFile. Runs on the download executor;
181
- * never fails — an empty string means "unknown" and the server falls back
182
- * to the buildTime heuristic.
202
+ * DownloadTask.copyBundledAssetToFile. Never fails — an empty string
203
+ * means "unknown" and the server falls back to the buildTime heuristic.
183
204
  *
184
- * Deliberately java.security.MessageDigest instead of the C++
185
- * pushy::digest: librnpushy.so is a prebuilt artifact and this must not
186
- * force a rebuild. The NIST vectors in the patch_core tests anchor both
187
- * implementations to the same standard.
205
+ * Computed once per process on a dedicated thread started by the
206
+ * constructor (not on the download executor, where it used to queue
207
+ * behind cleanup and downloads and make the first JS checkUpdate send an
208
+ * empty hash). The listener runs on that thread, or on the caller's when
209
+ * the value is already known.
210
+ *
211
+ * java.security.MessageDigest rather than the C++ pushy::digest: the
212
+ * bytes come from an AssetManager stream on the Java side, and the
213
+ * platform digest is native-backed already. The NIST vectors in the
214
+ * patch_core tests anchor both implementations to the same standard.
188
215
  */
189
216
  public void getBundleHash(final BundleHashListener listener) {
190
- if (DEBUG) {
217
+ String known;
218
+ synchronized (bundleHashLock) {
219
+ known = bundleHash;
220
+ if (known == null) {
221
+ pendingBundleHashListeners.add(listener);
222
+ return;
223
+ }
224
+ }
225
+ listener.onBundleHash(known);
226
+ }
227
+
228
+ // Package-private: also the native cold-start check's request input
229
+ // (SyncCoordinator). Blocks until the shared computation finished — call
230
+ // off the main thread.
231
+ String computeBundleHash() {
232
+ synchronized (bundleHashLock) {
233
+ while (bundleHash == null) {
234
+ try {
235
+ bundleHashLock.wait();
236
+ } catch (InterruptedException e) {
237
+ Thread.currentThread().interrupt();
238
+ return "";
239
+ }
240
+ }
241
+ return bundleHash;
242
+ }
243
+ }
244
+
245
+ private void startBundleHashComputation() {
246
+ if (BuildConfig.DEBUG) {
191
247
  // Metro serves the bundle in debug; mirror the dev buildTime behaviour.
192
- listener.onBundleHash("");
248
+ publishBundleHash("");
193
249
  return;
194
250
  }
195
- executor.execute(new Runnable() {
196
- @Override
197
- public void run() {
198
- listener.onBundleHash(computeBundleHash());
251
+ Threads.startDaemon("pushy-bundle-hash", () -> {
252
+ String hash = "";
253
+ try {
254
+ hash = loadOrHashBundledAsset();
255
+ } catch (Throwable e) {
256
+ Log.w(TAG, "bundle hash failed: " + e);
257
+ } finally {
258
+ publishBundleHash(hash);
199
259
  }
200
260
  });
201
261
  }
202
262
 
203
- // Package-private: also the native cold-start check's request input
204
- // (SyncCoordinator). Blocking — call off the main thread.
205
- String computeBundleHash() {
263
+ private void publishBundleHash(String hash) {
264
+ java.util.ArrayList<BundleHashListener> listeners;
265
+ synchronized (bundleHashLock) {
266
+ bundleHash = hash;
267
+ listeners = pendingBundleHashListeners;
268
+ pendingBundleHashListeners = null;
269
+ bundleHashLock.notifyAll();
270
+ }
271
+ if (listeners != null) {
272
+ for (BundleHashListener listener : listeners) {
273
+ try {
274
+ listener.onBundleHash(hash);
275
+ } catch (Throwable e) {
276
+ Log.w(TAG, "bundle hash listener failed: " + e);
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ private String loadOrHashBundledAsset() {
206
283
  String cachePrefix = getPackageVersion() + "|" + getPackageLastUpdateTime() + "|";
207
284
  String cached = sp.getString(KEY_BUNDLE_HASH_CACHE, null);
208
285
  if (cached != null && cached.startsWith(cachePrefix)) {
@@ -223,20 +300,8 @@ public class UpdateContext {
223
300
 
224
301
  private String sha256OfBundledAsset(String assetName) {
225
302
  try (InputStream in = context.getAssets().open(assetName)) {
226
- MessageDigest md = MessageDigest.getInstance("SHA-256");
227
- byte[] buffer = new byte[64 * 1024];
228
- int read;
229
- while ((read = in.read(buffer)) != -1) {
230
- md.update(buffer, 0, read);
231
- }
232
- byte[] digest = md.digest();
233
- StringBuilder hex = new StringBuilder(digest.length * 2);
234
- for (byte b : digest) {
235
- hex.append(Character.forDigit((b >> 4) & 0xf, 16));
236
- hex.append(Character.forDigit(b & 0xf, 16));
237
- }
238
- return hex.toString();
239
- } catch (IOException | NoSuchAlgorithmException e) {
303
+ return UpdateFileUtils.sha256Hex(in);
304
+ } catch (IOException e) {
240
305
  // Expected when there is no embedded bundle (custom name, no
241
306
  // release bundle); "unknown" is a valid answer, not an error.
242
307
  Log.i(TAG, "Cannot hash bundled asset " + assetName + ": " + e);
@@ -261,16 +326,30 @@ public class UpdateContext {
261
326
  void onDownloadFailed(Throwable error);
262
327
  }
263
328
 
264
- public void downloadFullUpdate(String url, String hash, DownloadFileListener listener) {
265
- downloadFullUpdate(url, hash, listener, 0);
329
+ /**
330
+ * Params shared by every download task. deadlineNanos is an absolute
331
+ * System.nanoTime deadline for orchestrated (native cold-start) tasks; 0
332
+ * keeps the per-call timeout of a JS-initiated download.
333
+ */
334
+ private static DownloadTaskParams downloadParams(
335
+ int type, String url, String hash, DownloadFileListener listener, long deadlineNanos
336
+ ) {
337
+ DownloadTaskParams params = new DownloadTaskParams();
338
+ params.type = type;
339
+ params.url = url;
340
+ params.hash = hash;
341
+ params.listener = listener;
342
+ params.deadlineNanos = deadlineNanos;
343
+ return params;
266
344
  }
267
345
 
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).
346
+ /*
347
+ * The patch downloads return the queued task's params so the native
348
+ * orchestrator can cancel the task when its phase budget expires
349
+ * (CODE_AUDIT 2.12); null when the request was rejected up front (the
350
+ * listener has already been told).
273
351
  */
352
+
274
353
  @Nullable
275
354
  DownloadTaskParams downloadFullUpdate(
276
355
  String url, String hash, DownloadFileListener listener, long deadlineNanos
@@ -278,37 +357,25 @@ public class UpdateContext {
278
357
  if (rejectUnsafeComponent(hash, listener)) {
279
358
  return null;
280
359
  }
281
- DownloadTaskParams params = new DownloadTaskParams();
282
- params.type = DownloadTaskParams.TASK_TYPE_PATCH_FULL;
283
- params.url = url;
284
- params.hash = hash;
285
- params.listener = listener;
286
- params.deadlineNanos = deadlineNanos;
360
+ DownloadTaskParams params = downloadParams(
361
+ DownloadTaskParams.TASK_TYPE_PATCH_FULL, url, hash, listener, deadlineNanos);
287
362
  params.targetFile = new File(rootDir, hash + ".ppk");
288
363
  params.unzipDirectory = new File(rootDir, hash);
289
364
  enqueue(params);
290
365
  return params;
291
366
  }
292
367
 
368
+ /** A plain file download into the root dir; hash only keys progress events. */
293
369
  public void downloadFile(String url, String hash, String fileName, DownloadFileListener listener) {
294
370
  if (rejectUnsafeComponent(fileName, listener)) {
295
371
  return;
296
372
  }
297
- DownloadTaskParams params = new DownloadTaskParams();
298
- params.type = DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD;
299
- params.url = url;
300
- params.hash = hash;
301
- params.listener = listener;
302
-
373
+ DownloadTaskParams params = downloadParams(
374
+ DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD, url, hash, listener, 0);
303
375
  params.targetFile = new File(rootDir, fileName);
304
- // params.unzipDirectory = new File(rootDir, hash);
305
376
  enqueue(params);
306
377
  }
307
378
 
308
- public void downloadPatchFromApk(String url, String hash, DownloadFileListener listener) {
309
- downloadPatchFromApk(url, hash, listener, 0);
310
- }
311
-
312
379
  @Nullable
313
380
  DownloadTaskParams downloadPatchFromApk(
314
381
  String url, String hash, DownloadFileListener listener, long deadlineNanos
@@ -316,22 +383,14 @@ public class UpdateContext {
316
383
  if (rejectUnsafeComponent(hash, listener)) {
317
384
  return null;
318
385
  }
319
- DownloadTaskParams params = new DownloadTaskParams();
320
- params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK;
321
- params.url = url;
322
- params.hash = hash;
323
- params.listener = listener;
324
- params.deadlineNanos = deadlineNanos;
386
+ DownloadTaskParams params = downloadParams(
387
+ DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK, url, hash, listener, deadlineNanos);
325
388
  params.targetFile = new File(rootDir, hash + Texts.reveal("7416e4dae09b69512137"));
326
389
  params.unzipDirectory = new File(rootDir, hash);
327
390
  enqueue(params);
328
391
  return params;
329
392
  }
330
393
 
331
- public void downloadPatchFromPpk(String url, String hash, String originHash, DownloadFileListener listener) {
332
- downloadPatchFromPpk(url, hash, originHash, listener, 0);
333
- }
334
-
335
394
  @Nullable
336
395
  DownloadTaskParams downloadPatchFromPpk(
337
396
  String url,
@@ -343,13 +402,9 @@ public class UpdateContext {
343
402
  if (rejectUnsafeComponent(hash, listener) || rejectUnsafeComponent(originHash, listener)) {
344
403
  return null;
345
404
  }
346
- DownloadTaskParams params = new DownloadTaskParams();
347
- params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
348
- params.url = url;
349
- params.hash = hash;
405
+ DownloadTaskParams params = downloadParams(
406
+ DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK, url, hash, listener, deadlineNanos);
350
407
  params.originHash = originHash;
351
- params.listener = listener;
352
- params.deadlineNanos = deadlineNanos;
353
408
  params.targetFile = new File(rootDir, originHash + "-" + hash + Texts.reveal("7407e4dae09b69512137"));
354
409
  params.unzipDirectory = new File(rootDir, hash);
355
410
  params.originDirectory = new File(rootDir, originHash);
@@ -429,28 +484,16 @@ public class UpdateContext {
429
484
  if (!UpdateFileUtils.isSafePathComponent(hash)) {
430
485
  throw new IllegalArgumentException("Invalid hash: " + hash);
431
486
  }
432
- File versionDir = new File(rootDir, hash);
433
- File bundleFile = new File(versionDir, "index.bundlejs");
434
- if (!bundleFile.isFile()) {
435
- throw new IllegalStateException("Bundle version " + hash + " not found.");
436
- }
437
487
  StateCoreResult currentState = getStateSnapshot();
438
- boolean isLegacyActivatedVersion = hash.equals(currentState.currentVersion)
488
+ // Versions activated before completion records existed are
489
+ // grandfathered through current/last state, but only while their
490
+ // directory has no record; any other directory must carry a record
491
+ // whose bundle digest matches the bytes on disk before the next
492
+ // launch is pointed at them.
493
+ boolean grandfathered = hash.equals(currentState.currentVersion)
439
494
  || hash.equals(currentState.lastVersion);
440
- if (isLegacyActivatedVersion) {
441
- // Versions activated before completion markers were introduced are
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.
448
- throw new IllegalStateException("Bundle version " + hash + " is incomplete.");
449
- }
450
- // The record's bundle digest must match the bytes on disk before
451
- // the next launch is pointed at them.
452
495
  try {
453
- InstallRecord.verifyForActivation(versionDir, hash, bundleFile);
496
+ InstallRecord.verifyInstalled(new File(rootDir, hash), hash, grandfathered);
454
497
  } catch (IOException e) {
455
498
  throw new IllegalStateException(e.getMessage(), e);
456
499
  }
@@ -462,13 +505,12 @@ public class UpdateContext {
462
505
  * holds commitLock and has run verifySwitchTarget.
463
506
  */
464
507
  private StateCoreResult computeSwitchState(String hash) {
465
- return runStateCore(
466
- STATE_OP_SWITCH_VERSION,
467
- getStateSnapshot(),
468
- hash,
469
- false,
470
- false
471
- );
508
+ return runStateOp(STATE_OP_SWITCH_VERSION, hash);
509
+ }
510
+
511
+ /** One state-core operation on the current snapshot; caller holds commitLock. */
512
+ private StateCoreResult runStateOp(int operation, @Nullable String hash) {
513
+ return runStateCore(operation, getStateSnapshot(), hash, false, false);
472
514
  }
473
515
 
474
516
  public void switchVersion(String hash) {
@@ -518,9 +560,9 @@ public class UpdateContext {
518
560
 
519
561
  /**
520
562
  * 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.
563
+ * editor. getConstants runs this synchronously on the JS thread (a
564
+ * TurboModule constant read), so the write is applied asynchronously;
565
+ * persistence is best-effort on this launch path.
524
566
  */
525
567
  LaunchMarkers consumeLaunchMarkers() {
526
568
  LaunchMarkers markers;
@@ -534,21 +576,18 @@ public class UpdateContext {
534
576
  editor.remove(KEY_FIRST_LOAD_MARKED);
535
577
  }
536
578
  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);
579
+ applyState(editor, runStateOp(STATE_OP_CLEAR_ROLLBACK_MARK, null));
545
580
  }
546
- persistEditor(editor, "consume launch markers");
581
+ // Asynchronous on purpose: the in-memory state changes
582
+ // immediately, the disk write happens on the
583
+ // SharedPreferences writer. Losing it to a crash only
584
+ // re-reports the one-shot markers on the next launch.
585
+ editor.apply();
547
586
  }
548
587
  }
549
588
  if (markers.rolledBackVersion != null) {
550
589
  // The rolled-back version's directory is now eligible for cleanup.
551
- this.cleanUp();
590
+ cleanUp(DownloadTaskParams.DEFAULT_MAX_AGE_DAYS);
552
591
  }
553
592
  return markers;
554
593
  }
@@ -562,14 +601,7 @@ public class UpdateContext {
562
601
  return;
563
602
  }
564
603
  synchronized (commitLock) {
565
- StateCoreResult currentState = getStateSnapshot();
566
- StateCoreResult nextState = runStateCore(
567
- STATE_OP_MARK_SUCCESS,
568
- currentState,
569
- null,
570
- false,
571
- false
572
- );
604
+ StateCoreResult nextState = runStateOp(STATE_OP_MARK_SUCCESS, null);
573
605
  SharedPreferences.Editor editor = sp.edit();
574
606
  applyState(editor, nextState);
575
607
  if (nextState.staleVersionToDelete != null) {
@@ -578,7 +610,7 @@ public class UpdateContext {
578
610
  persistEditorOrThrow(editor, "mark success");
579
611
  }
580
612
 
581
- this.cleanUp();
613
+ cleanUp(DownloadTaskParams.DEFAULT_MAX_AGE_DAYS);
582
614
  }
583
615
 
584
616
  /**
@@ -606,6 +638,11 @@ public class UpdateContext {
606
638
  resetState.firstTime = false;
607
639
  resetState.firstTimeOk = true;
608
640
  String uuid = sp.getString("uuid", null);
641
+ // The cached check response still advertises the version this reset
642
+ // removes; drop it first so a failure aborts before the state wipe.
643
+ if (!clearNativeCheckCache()) {
644
+ throw new IllegalStateException("Failed to persist state for reset check cache");
645
+ }
609
646
  SharedPreferences.Editor editor = sp.edit();
610
647
  editor.clear();
611
648
  applyState(editor, resetState);
@@ -614,17 +651,8 @@ public class UpdateContext {
614
651
  }
615
652
  persistEditorOrThrow(editor, "reset to packaged bundle");
616
653
  ignoreRollback = false;
617
- // editor.clear() above already dropped the cached check response; it
618
- // still advertised the version this reset removed.
619
654
  Log.i(TAG, "Reset to packaged bundle");
620
-
621
- DownloadTaskParams params = new DownloadTaskParams();
622
- params.type = DownloadTaskParams.TASK_TYPE_CLEANUP;
623
- params.maxAgeDays = 0;
624
- // Keep names are resolved by the queued task at execution time, so a
625
- // later activation is protected too.
626
- params.unzipDirectory = rootDir;
627
- enqueue(params);
655
+ cleanUp(0);
628
656
  }
629
657
 
630
658
  public static void setCustomInstanceManager(ReactInstanceManager instanceManager) {
@@ -713,7 +741,7 @@ public class UpdateContext {
713
741
  // the main thread.
714
742
  java.util.HashSet<String> visitedVersions = new java.util.HashSet<>();
715
743
  while (currentVersion != null && visitedVersions.add(currentVersion)) {
716
- File bundleFile = new File(rootDir, currentVersion+"/index.bundlejs");
744
+ File bundleFile = InstallRecord.bundleFile(new File(rootDir, currentVersion));
717
745
  if (!bundleFile.exists()) {
718
746
  Log.e(TAG, "Bundle version " + currentVersion + " not found.");
719
747
  currentVersion = this.rollBack();
@@ -721,6 +749,8 @@ public class UpdateContext {
721
749
  continue;
722
750
  }
723
751
  launchVersion = currentVersion;
752
+ launchBundleSha256 = new String[] {
753
+ currentVersion, readBundleSha256(currentVersion)};
724
754
  syncRolledBackVersion = rolledBackVersion();
725
755
  return bundleFile.toString();
726
756
  }
@@ -736,7 +766,6 @@ public class UpdateContext {
736
766
  }
737
767
  }
738
768
 
739
- /** Sampled/compared by the native check orchestrator; see resetGeneration. */
740
769
  /** Shared by JS and native hosts. Config replacement invalidates old native decisions. */
741
770
  private static final java.util.concurrent.atomic.AtomicLong nativeConfigGeneration =
742
771
  new java.util.concurrent.atomic.AtomicLong(0);
@@ -747,15 +776,22 @@ public class UpdateContext {
747
776
 
748
777
  void setNativeConfig(String config) {
749
778
  synchronized (commitLock) {
750
- boolean changed = !config.equals(sp.getString(SyncCoordinator.KEY_CONFIG, null));
779
+ // Canonical comparison (C1, FlowBridge.configsEquivalent):
780
+ // re-sending the same config with another key order or trailing
781
+ // slashes must not cancel an in-flight round or drop its cache.
782
+ boolean changed = !FlowBridge.configsEquivalent(
783
+ config, sp.getString(SyncCoordinator.KEY_CONFIG, null));
751
784
  SharedPreferences.Editor editor = sp.edit();
752
785
  if (changed) {
753
786
  // Also invalidate on a failed persistence attempt: an older
754
787
  // round must not commit over uncertain configuration state.
755
788
  resetGeneration.incrementAndGet();
756
789
  nativeConfigGeneration.incrementAndGet();
757
- editor.remove(SyncCoordinator.KEY_RESP_CACHE);
758
790
  SyncCoordinator.recordJsRound(null);
791
+ if (!clearNativeCheckCache()) {
792
+ throw new IllegalStateException(
793
+ "Failed to persist state for configuration check cache");
794
+ }
759
795
  }
760
796
  // A native-only first launch needs a stable gray-release identity
761
797
  // before JS initializes. Never replace an existing installation ID.
@@ -771,7 +807,11 @@ public class UpdateContext {
771
807
  SyncCoordinator.onConfigured(this);
772
808
  }
773
809
 
774
- // Native-decision generation: bumped by reset AND configuration replacement.
810
+ /**
811
+ * Native-decision generation, sampled/compared by the native check
812
+ * orchestrator: bumped by reset AND configuration replacement (see
813
+ * resetGeneration).
814
+ */
775
815
  static long getResetGeneration() {
776
816
  return resetGeneration.get();
777
817
  }
@@ -820,13 +860,20 @@ public class UpdateContext {
820
860
  if (switching) {
821
861
  applyState(editor, computeSwitchState(hash));
822
862
  }
823
- if (responseCacheJson != null) {
824
- editor.putString(SyncCoordinator.KEY_RESP_CACHE, responseCacheJson);
825
- }
863
+ // The round is over once its result lands: clear the
864
+ // interrupted-round breadcrumb in the same commit.
865
+ editor.remove(SyncCoordinator.KEY_ROUND_INCOMPLETE);
826
866
  persistEditorOrThrow(editor, "commit sync result");
827
867
  if (switching) {
828
868
  ignoreRollback = false;
829
869
  }
870
+ // Advisory cache, separate file: written after the state it
871
+ // describes, still under commitLock so a reset cannot interleave.
872
+ if (responseCacheJson != null
873
+ && !checkCacheSp.edit()
874
+ .putString(SyncCoordinator.KEY_RESP_CACHE, responseCacheJson).commit()) {
875
+ Log.w(TAG, "Failed to persist native sync response cache");
876
+ }
830
877
  return true;
831
878
  }
832
879
  }
@@ -837,33 +884,49 @@ public class UpdateContext {
837
884
  * unreadable record. Exposed to JS for crash-report attribution.
838
885
  */
839
886
  String currentBundleSha256(String hash) {
887
+ String[] cached = launchBundleSha256;
888
+ if (cached != null && cached[0].equals(hash)) {
889
+ return cached[1];
890
+ }
891
+ return readBundleSha256(hash);
892
+ }
893
+
894
+ private String readBundleSha256(String hash) {
840
895
  if (hash == null || hash.isEmpty() || !UpdateFileUtils.isSafePathComponent(hash)) {
841
896
  return "";
842
897
  }
843
- JSONObject record = InstallRecord.read(new File(rootDir, hash));
844
- return record == null ? "" : record.optString("bundleSha256", "");
898
+ return InstallRecord.recordedBundleSha256(new File(rootDir, hash));
899
+ }
900
+
901
+ /** Raw native check response for JS (§10.3); null when absent. */
902
+ @Nullable
903
+ String getNativeCheckCache() {
904
+ return checkCacheSp.getString(SyncCoordinator.KEY_RESP_CACHE, null);
905
+ }
906
+
907
+ private boolean clearNativeCheckCache() {
908
+ if (!checkCacheSp.contains(SyncCoordinator.KEY_RESP_CACHE)) {
909
+ return true;
910
+ }
911
+ if (!checkCacheSp.edit().remove(SyncCoordinator.KEY_RESP_CACHE).commit()) {
912
+ Log.e(TAG, "Failed to clear native sync response cache");
913
+ return false;
914
+ }
915
+ return true;
845
916
  }
846
917
 
847
918
  boolean hasCompletedVersion(String hash) {
848
919
  if (!UpdateFileUtils.isSafePathComponent(hash)) {
849
920
  return false;
850
921
  }
851
- File versionDir = new File(rootDir, hash);
852
- return new File(versionDir, "index.bundlejs").isFile()
853
- && InstallRecord.isComplete(versionDir, hash);
922
+ return InstallRecord.isInstalled(new File(rootDir, hash), hash);
854
923
  }
855
924
 
856
925
  private String rollBack() {
857
926
  synchronized (commitLock) {
858
- StateCoreResult currentState = getStateSnapshot();
859
- StateCoreResult nextState = runStateCore(
860
- STATE_OP_ROLLBACK,
861
- currentState,
862
- null,
863
- false,
864
- false
865
- );
866
- Log.e(TAG, "Rolling back version " + currentState.currentVersion
927
+ String rollingBack = getCurrentVersion();
928
+ StateCoreResult nextState = runStateOp(STATE_OP_ROLLBACK, null);
929
+ Log.e(TAG, "Rolling back version " + rollingBack
867
930
  + " to " + nextState.currentVersion);
868
931
  SharedPreferences.Editor editor = sp.edit();
869
932
  applyState(editor, nextState);
@@ -884,10 +947,15 @@ public class UpdateContext {
884
947
  }
885
948
 
886
949
  interface CleanupAction {
887
- void run(@Nullable String keepCurrent, @Nullable String keepPrevious);
950
+ void run(String[] keepNames);
888
951
  }
889
952
 
890
- /** Resolve keep names immediately before deletion, never when enqueued. */
953
+ /**
954
+ * Resolve keep names immediately before the cleanup acts, never when
955
+ * enqueued. The action runs under versionFilesLock, so it must stay cheap
956
+ * (DownloadTask only renames doomed entries into a trash directory and
957
+ * deletes them after this returns).
958
+ */
891
959
  void runCleanupWithLatestState(CleanupAction action) {
892
960
  synchronized (versionFilesLock) {
893
961
  final String[] keepNames;
@@ -898,19 +966,19 @@ public class UpdateContext {
898
966
  launchVersion
899
967
  );
900
968
  }
901
- if (keepNames == null) {
902
- // JNI has only two keep slots; three distinct live versions are
903
- // safer to defer until the next process than to delete one.
904
- Log.i(TAG, "Skipping cleanup: three live versions need protection");
905
- return;
906
- }
907
- action.run(keepNames[0], keepNames[1]);
969
+ action.run(keepNames);
908
970
  }
909
971
  }
910
972
 
911
- private void cleanUp() {
973
+ /**
974
+ * Queues removal of every version entry older than maxAgeDays (0: all)
975
+ * except the keep names, which the queued task resolves at execution
976
+ * time so a later activation is protected too.
977
+ */
978
+ private void cleanUp(int maxAgeDays) {
912
979
  DownloadTaskParams params = new DownloadTaskParams();
913
980
  params.type = DownloadTaskParams.TASK_TYPE_CLEANUP;
981
+ params.maxAgeDays = maxAgeDays;
914
982
  params.unzipDirectory = rootDir;
915
983
  enqueue(params);
916
984
  }