react-native-update 10.43.3 → 10.45.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 (41) hide show
  1. package/android/jni/Android.mk +0 -1
  2. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  3. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  4. package/android/lib/x86/librnupdate.so +0 -0
  5. package/android/lib/x86_64/librnupdate.so +0 -0
  6. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +24 -12
  7. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +33 -9
  8. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +9 -1
  9. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +64 -0
  10. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +8 -4
  11. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +13 -1
  12. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +8 -8
  13. package/cpp/patch_core/archive_patch_core.cpp +16 -0
  14. package/cpp/patch_core/archive_patch_core.h +5 -0
  15. package/cpp/patch_core/jni_util.h +56 -0
  16. package/cpp/patch_core/patch_core.cpp +16 -1
  17. package/cpp/patch_core/patch_core.h +6 -0
  18. package/cpp/patch_core/patch_core_android.cpp +4 -37
  19. package/cpp/patch_core/state_ops.h +24 -0
  20. package/cpp/patch_core/tests/patch_core_test.cpp +109 -2
  21. package/cpp/patch_core/update_core_android.cpp +43 -69
  22. package/harmony/pushy/src/main/cpp/pushy.cpp +163 -128
  23. package/harmony/pushy/src/main/ets/DownloadTask.ts +72 -19
  24. package/harmony/pushy/src/main/ets/NativePatchCore.ts +2 -6
  25. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +10 -10
  26. package/harmony/pushy/src/main/ets/UpdateContext.ts +26 -9
  27. package/harmony/pushy.har +0 -0
  28. package/ios/RCTPushy/RCTPushy.mm +142 -111
  29. package/ios/RCTPushy/RCTPushyDownloader.mm +41 -2
  30. package/package.json +7 -3
  31. package/src/client.ts +161 -101
  32. package/src/context.ts +21 -7
  33. package/src/core.ts +5 -1
  34. package/src/index.ts +7 -1
  35. package/src/provider.tsx +91 -56
  36. package/src/utils.ts +58 -18
  37. package/android/jni/DownloadTask.c +0 -56
  38. package/android/jni/cn_reactnative_modules_update_DownloadTask.h +0 -21
  39. package/harmony/pushy/src/main/cpp/pushy.c +0 -117
  40. package/harmony/pushy/src/main/cpp/pushy.h +0 -8
  41. package/src/__tests__/setup.ts +0 -44
@@ -20,7 +20,6 @@ Hdp_Files := \
20
20
  lzma/C/Lzma2Dec.c
21
21
 
22
22
  LOCAL_SRC_FILES := \
23
- DownloadTask.c \
24
23
  ../../cpp/patch_core/archive_patch_core.cpp \
25
24
  ../../cpp/patch_core/patch_core.cpp \
26
25
  ../../cpp/patch_core/patch_core_android.cpp \
Binary file
Binary file
@@ -92,6 +92,10 @@ final class BundledResourceCopier {
92
92
  SafeZipFile baseZipFile = zipFileMap.get(context.getPackageResourcePath());
93
93
  HashMap<String, ArrayList<File>> remainingFiles =
94
94
  new HashMap<String, ArrayList<File>>(resToCopy);
95
+ // Track copies that were located but failed to write. These are real
96
+ // failures (disk full, corrupt archive) that must fail the update,
97
+ // not skips — otherwise the update activates with missing resources.
98
+ ArrayList<String> failedCopies = new ArrayList<String>();
95
99
 
96
100
  for (String fromPath : new ArrayList<String>(remainingFiles.keySet())) {
97
101
  ArrayList<File> targets = remainingFiles.get(fromPath);
@@ -168,23 +172,31 @@ final class BundledResourceCopier {
168
172
  }
169
173
  lastTarget = target;
170
174
  } catch (IOException e) {
171
- if (UpdateContext.DEBUG) {
172
- Log.w(
173
- UpdateContext.TAG,
174
- "Failed to copy resource "
175
- + actualSourcePath
176
- + " to "
177
- + target
178
- + ": "
179
- + e.getMessage()
180
- );
181
- }
175
+ // A located resource that fails to write is a hard
176
+ // failure, not a skip: record it and fail the update
177
+ // after the loop so a broken update is not activated.
178
+ Log.e(
179
+ UpdateContext.TAG,
180
+ "Failed to copy resource "
181
+ + actualSourcePath
182
+ + " to "
183
+ + target,
184
+ e
185
+ );
186
+ failedCopies.add(actualSourcePath + " -> " + target);
182
187
  }
183
188
  }
184
189
  remainingFiles.remove(fromPath);
185
190
  }
186
191
 
187
- if (!remainingFiles.isEmpty() && UpdateContext.DEBUG) {
192
+ if (!failedCopies.isEmpty()) {
193
+ throw new IOException(
194
+ "Failed to copy " + failedCopies.size()
195
+ + " bundled resource(s): " + failedCopies
196
+ );
197
+ }
198
+
199
+ if (!remainingFiles.isEmpty()) {
188
200
  Log.w(
189
201
  UpdateContext.TAG,
190
202
  "Skipped " + remainingFiles.size() + " missing bundled resources"
@@ -29,7 +29,20 @@ import org.json.JSONTokener;
29
29
 
30
30
  class DownloadTask implements Runnable {
31
31
  private static final int DOWNLOAD_CHUNK_SIZE = 4096;
32
- private static final OkHttpClient HTTP_CLIENT = new OkHttpClient();
32
+ // When the server does not report Content-Length we cannot key progress
33
+ // events on percentage change, so throttle by bytes to avoid flooding the
34
+ // bridge (e.g. a 20MB chunked download would otherwise emit ~5000 events).
35
+ private static final long PROGRESS_BYTES_THRESHOLD = 256 * 1024;
36
+ // Explicit timeouts: the default client has no call timeout, so a
37
+ // slow-dripping connection could occupy the single-threaded download
38
+ // executor indefinitely and starve queued tasks. The call timeout is a
39
+ // generous upper bound sized for large full-package downloads.
40
+ private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder()
41
+ .connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
42
+ .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
43
+ .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
44
+ .callTimeout(10, java.util.concurrent.TimeUnit.MINUTES)
45
+ .build();
33
46
 
34
47
  static {
35
48
  NativeUpdateCore.ensureLoaded();
@@ -98,6 +111,7 @@ class DownloadTask implements Runnable {
98
111
  long bytesRead;
99
112
  long received = 0;
100
113
  int currentPercentage = 0;
114
+ long lastPostedBytes = 0;
101
115
 
102
116
  try (
103
117
  BufferedSource source = body.source();
@@ -113,7 +127,8 @@ class DownloadTask implements Runnable {
113
127
  currentPercentage = percentage;
114
128
  postProgress(received, contentLength);
115
129
  }
116
- } else {
130
+ } else if (received - lastPostedBytes >= PROGRESS_BYTES_THRESHOLD) {
131
+ lastPostedBytes = received;
117
132
  postProgress(received, contentLength);
118
133
  }
119
134
  }
@@ -394,19 +409,28 @@ class DownloadTask implements Runnable {
394
409
  default:
395
410
  break;
396
411
  }
397
-
398
- if (params.listener != null) {
399
- params.listener.onDownloadCompleted(params);
400
- }
401
412
  } catch (Throwable error) {
402
- if (UpdateContext.DEBUG) {
403
- Log.e(UpdateContext.TAG, "download task failed", error);
404
- }
413
+ Log.e(UpdateContext.TAG, "download task failed", error);
405
414
  cleanUpAfterFailure(taskType);
406
415
 
407
416
  if (params.listener != null) {
408
417
  params.listener.onDownloadFailed(error);
409
418
  }
419
+ return;
420
+ }
421
+
422
+ // The task itself succeeded. Run the completion callback outside the
423
+ // try/catch above so an exception thrown by the callback (e.g. a
424
+ // FileProvider misconfiguration during installApk) is not mistaken for
425
+ // a download failure that deletes the successfully downloaded file and
426
+ // settles the promise a second time.
427
+ if (params.listener != null) {
428
+ try {
429
+ params.listener.onDownloadCompleted(params);
430
+ } catch (Throwable error) {
431
+ Log.e(UpdateContext.TAG, "download completion callback failed", error);
432
+ params.listener.onDownloadFailed(error);
433
+ }
410
434
  }
411
435
  }
412
436
 
@@ -75,7 +75,13 @@ final class ReactReloadManager {
75
75
  currentReactHost,
76
76
  createBundleLoader(application, updateBundlePath, true)
77
77
  );
78
- } catch (Throwable ignored) {
78
+ } catch (Throwable reloadError) {
79
+ Log.e(
80
+ UpdateContext.TAG,
81
+ "Failed to reload via ReactHost, falling back to Activity.recreate() "
82
+ + "(this may load the previous bundle until next cold start)",
83
+ reloadError
84
+ );
79
85
  currentActivity.recreate();
80
86
  }
81
87
  }
@@ -172,6 +178,7 @@ final class ReactReloadManager {
172
178
  }
173
179
  }
174
180
  } catch (Throwable ignored) {
181
+ Log.w(UpdateContext.TAG, "getReactHost via ReactDelegate reflection failed", ignored);
175
182
  }
176
183
  }
177
184
 
@@ -179,6 +186,7 @@ final class ReactReloadManager {
179
186
  Method getReactHostMethod = application.getClass().getMethod("getReactHost");
180
187
  return getReactHostMethod.invoke(application);
181
188
  } catch (Throwable ignored) {
189
+ Log.w(UpdateContext.TAG, "getReactHost via Application.getReactHost() failed", ignored);
182
190
  }
183
191
 
184
192
  return null;
@@ -0,0 +1,64 @@
1
+ package cn.reactnative.modules.update;
2
+
3
+ import android.util.Log;
4
+ import androidx.annotation.Nullable;
5
+ import com.facebook.react.bridge.Promise;
6
+
7
+ import java.util.concurrent.Executor;
8
+ import java.util.concurrent.Executors;
9
+ import java.util.concurrent.ThreadFactory;
10
+
11
+ /**
12
+ * Runs state-persistence operations (switchVersion / markSuccess / setUuid /
13
+ * setLocalHashInfo) on a dedicated single background thread.
14
+ *
15
+ * These operations only read/modify SharedPreferences via a synchronous
16
+ * commit(); they were previously dispatched to the UI thread purely to
17
+ * serialize them. markSuccess in particular runs on every cold start, so doing
18
+ * its blocking disk write on the main thread caused jank/ANR on low-end
19
+ * devices. A single-thread executor preserves the same serialization guarantee
20
+ * while keeping the disk I/O off the UI thread.
21
+ *
22
+ * Note: reload/restart operations must still run on the UI thread and therefore
23
+ * keep using {@link UiThreadRunner}.
24
+ */
25
+ final class StateSerialRunner {
26
+ interface Operation {
27
+ void run() throws Throwable;
28
+ }
29
+
30
+ // Single worker thread -> operations stay serialized in submission order,
31
+ // matching the previous UI-thread behavior. The thread is named so it is
32
+ // identifiable in thread dumps / ANR traces when diagnosing persistence.
33
+ private static final Executor EXECUTOR = Executors.newSingleThreadExecutor(
34
+ new ThreadFactory() {
35
+ @Override
36
+ public Thread newThread(Runnable r) {
37
+ return new Thread(r, "pushy-state-serial");
38
+ }
39
+ });
40
+
41
+ private StateSerialRunner() {
42
+ }
43
+
44
+ static void run(
45
+ @Nullable final Promise promise,
46
+ final String operationName,
47
+ final Operation operation
48
+ ) {
49
+ EXECUTOR.execute(new Runnable() {
50
+ @Override
51
+ public void run() {
52
+ try {
53
+ operation.run();
54
+ } catch (Throwable error) {
55
+ if (promise != null) {
56
+ promise.reject(operationName + " failed", error);
57
+ } else {
58
+ Log.e(UpdateContext.TAG, operationName + " failed", error);
59
+ }
60
+ }
61
+ }
62
+ });
63
+ }
64
+ }
@@ -37,7 +37,7 @@ public class UpdateContext {
37
37
  private static final String KEY_FIRST_LOAD_MARKED = "firstLoadMarked";
38
38
 
39
39
  // Singleton instance
40
- private static UpdateContext sInstance;
40
+ private static volatile UpdateContext sInstance;
41
41
  private static final Object sLock = new Object();
42
42
  private static ReactInstanceManager pendingReactInstanceManager;
43
43
 
@@ -55,7 +55,7 @@ public class UpdateContext {
55
55
  boolean flagB
56
56
  );
57
57
 
58
- public UpdateContext(Context context) {
58
+ private UpdateContext(Context context) {
59
59
  this.context = context.getApplicationContext();
60
60
  this.executor = Executors.newSingleThreadExecutor();
61
61
 
@@ -214,7 +214,7 @@ public class UpdateContext {
214
214
 
215
215
  public void switchVersion(String hash) {
216
216
  if (!new File(rootDir, hash+"/index.bundlejs").exists()) {
217
- throw new Error("Bundle version " + hash + " not found.");
217
+ throw new IllegalStateException("Bundle version " + hash + " not found.");
218
218
  }
219
219
  StateCoreResult currentState = getStateSnapshot();
220
220
  StateCoreResult nextState = runStateCore(
@@ -385,7 +385,11 @@ public class UpdateContext {
385
385
  return defaultAssetsUrl;
386
386
  }
387
387
 
388
- while (currentVersion != null) {
388
+ // Guard the rollback chain against cycles: a corrupted state returning
389
+ // an already-visited version would otherwise spin this loop forever on
390
+ // the main thread.
391
+ java.util.HashSet<String> visitedVersions = new java.util.HashSet<>();
392
+ while (currentVersion != null && visitedVersions.add(currentVersion)) {
389
393
  File bundleFile = new File(rootDir, currentVersion+"/index.bundlejs");
390
394
  if (!bundleFile.exists()) {
391
395
  Log.e(TAG, "Bundle version " + currentVersion + " not found.");
@@ -24,7 +24,7 @@ final class UpdateEventEmitter {
24
24
 
25
25
  static void sendEvent(String eventName, WritableMap params) {
26
26
  ReactApplicationContext reactContext = getReactContext();
27
- if (reactContext == null || !reactContext.hasActiveCatalystInstance()) {
27
+ if (reactContext == null || !hasActiveInstance(reactContext)) {
28
28
  return;
29
29
  }
30
30
 
@@ -32,4 +32,16 @@ final class UpdateEventEmitter {
32
32
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
33
33
  .emit(eventName, params);
34
34
  }
35
+
36
+ @SuppressWarnings("deprecation")
37
+ private static boolean hasActiveInstance(ReactApplicationContext reactContext) {
38
+ try {
39
+ // hasActiveCatalystInstance() is always false in bridgeless mode, which
40
+ // silently drops every progress event on the new architecture.
41
+ return reactContext.hasActiveReactInstance();
42
+ } catch (NoSuchMethodError e) {
43
+ // RN < 0.68 has no hasActiveReactInstance(); fall back for old peers.
44
+ return reactContext.hasActiveCatalystInstance();
45
+ }
46
+ }
35
47
  }
@@ -149,7 +149,7 @@ public class UpdateModuleImpl {
149
149
  final Promise promise
150
150
  ) {
151
151
  final String hash = options.getString("hash");
152
- UiThreadRunner.run(promise, "switchVersionLater", new UiThreadRunner.Operation() {
152
+ StateSerialRunner.run(promise, "switchVersionLater", new StateSerialRunner.Operation() {
153
153
  @Override
154
154
  public void run() {
155
155
  setNeedUpdateInternal(updateContext, hash);
@@ -160,7 +160,7 @@ public class UpdateModuleImpl {
160
160
 
161
161
  public static void setNeedUpdate(final UpdateContext updateContext, final ReadableMap options) {
162
162
  final String hash = options.getString("hash");
163
- UiThreadRunner.run(null, "switchVersionLater", new UiThreadRunner.Operation() {
163
+ StateSerialRunner.run(null, "switchVersionLater", new StateSerialRunner.Operation() {
164
164
  @Override
165
165
  public void run() {
166
166
  setNeedUpdateInternal(updateContext, hash);
@@ -173,7 +173,7 @@ public class UpdateModuleImpl {
173
173
  }
174
174
 
175
175
  public static void markSuccess(final UpdateContext updateContext, final Promise promise) {
176
- UiThreadRunner.run(promise, "markSuccess", new UiThreadRunner.Operation() {
176
+ StateSerialRunner.run(promise, "markSuccess", new StateSerialRunner.Operation() {
177
177
  @Override
178
178
  public void run() {
179
179
  markSuccessInternal(updateContext);
@@ -183,7 +183,7 @@ public class UpdateModuleImpl {
183
183
  }
184
184
 
185
185
  public static void markSuccess(final UpdateContext updateContext) {
186
- UiThreadRunner.run(null, "markSuccess", new UiThreadRunner.Operation() {
186
+ StateSerialRunner.run(null, "markSuccess", new StateSerialRunner.Operation() {
187
187
  @Override
188
188
  public void run() {
189
189
  markSuccessInternal(updateContext);
@@ -200,7 +200,7 @@ public class UpdateModuleImpl {
200
200
  final String uuid,
201
201
  final Promise promise
202
202
  ) {
203
- UiThreadRunner.run(promise, "setUuid", new UiThreadRunner.Operation() {
203
+ StateSerialRunner.run(promise, "setUuid", new StateSerialRunner.Operation() {
204
204
  @Override
205
205
  public void run() {
206
206
  setUuidInternal(updateContext, uuid);
@@ -210,7 +210,7 @@ public class UpdateModuleImpl {
210
210
  }
211
211
 
212
212
  public static void setUuid(final UpdateContext updateContext, final String uuid) {
213
- UiThreadRunner.run(null, "setUuid", new UiThreadRunner.Operation() {
213
+ StateSerialRunner.run(null, "setUuid", new StateSerialRunner.Operation() {
214
214
  @Override
215
215
  public void run() {
216
216
  setUuidInternal(updateContext, uuid);
@@ -235,7 +235,7 @@ public class UpdateModuleImpl {
235
235
  final String info,
236
236
  final Promise promise
237
237
  ) {
238
- UiThreadRunner.run(promise, "setLocalHashInfo", new UiThreadRunner.Operation() {
238
+ StateSerialRunner.run(promise, "setLocalHashInfo", new StateSerialRunner.Operation() {
239
239
  @Override
240
240
  public void run() {
241
241
  setLocalHashInfoInternal(updateContext, hash, info);
@@ -249,7 +249,7 @@ public class UpdateModuleImpl {
249
249
  final String hash,
250
250
  final String info
251
251
  ) {
252
- UiThreadRunner.run(null, "setLocalHashInfo", new UiThreadRunner.Operation() {
252
+ StateSerialRunner.run(null, "setLocalHashInfo", new StateSerialRunner.Operation() {
253
253
  @Override
254
254
  public void run() {
255
255
  setLocalHashInfoInternal(updateContext, hash, info);
@@ -25,6 +25,22 @@ EntryAction ClassifyEntry(
25
25
  return entry_name == kManifestEntryName ? EntryAction::kSkip : EntryAction::kExtract;
26
26
  }
27
27
 
28
+ bool TryParseArchivePatchType(int value, ArchivePatchType* out) {
29
+ switch (value) {
30
+ case 1:
31
+ *out = ArchivePatchType::kFull;
32
+ return true;
33
+ case 2:
34
+ *out = ArchivePatchType::kPatchFromPackage;
35
+ return true;
36
+ case 3:
37
+ *out = ArchivePatchType::kPatchFromPpk;
38
+ return true;
39
+ default:
40
+ return false;
41
+ }
42
+ }
43
+
28
44
  patch::Status BuildArchivePatchPlan(
29
45
  ArchivePatchType type,
30
46
  const patch::PatchManifest& manifest,
@@ -35,6 +35,11 @@ EntryAction ClassifyEntry(
35
35
  ArchivePatchType type,
36
36
  const std::string& entry_name);
37
37
 
38
+ // Convert a platform-supplied integer to an ArchivePatchType. Returns false for
39
+ // unknown values so callers can fail loudly instead of silently treating an
40
+ // incremental patch as a full package (which would skip validation).
41
+ bool TryParseArchivePatchType(int value, ArchivePatchType* out);
42
+
38
43
  patch::Status BuildArchivePatchPlan(
39
44
  ArchivePatchType type,
40
45
  const patch::PatchManifest& manifest,
@@ -0,0 +1,56 @@
1
+ #pragma once
2
+
3
+ #include <jni.h>
4
+
5
+ #include <string>
6
+ #include <vector>
7
+
8
+ // Small JNI helpers shared by the Android glue translation units
9
+ // (patch_core_android.cpp and update_core_android.cpp). Header-only with inline
10
+ // linkage so both can include it without a separate compilation unit.
11
+
12
+ namespace pushy {
13
+ namespace jni_util {
14
+
15
+ inline std::string JStringToString(JNIEnv* env, jstring value) {
16
+ if (value == nullptr) {
17
+ return std::string();
18
+ }
19
+
20
+ const char* chars = env->GetStringUTFChars(value, nullptr);
21
+ if (chars == nullptr) {
22
+ return std::string();
23
+ }
24
+
25
+ std::string result(chars);
26
+ env->ReleaseStringUTFChars(value, chars);
27
+ return result;
28
+ }
29
+
30
+ inline std::vector<std::string> JArrayToVector(
31
+ JNIEnv* env,
32
+ jobjectArray values) {
33
+ std::vector<std::string> result;
34
+ if (values == nullptr) {
35
+ return result;
36
+ }
37
+
38
+ const jsize size = env->GetArrayLength(values);
39
+ result.reserve(static_cast<size_t>(size));
40
+ for (jsize index = 0; index < size; ++index) {
41
+ auto* item = static_cast<jstring>(env->GetObjectArrayElement(values, index));
42
+ result.push_back(JStringToString(env, item));
43
+ env->DeleteLocalRef(item);
44
+ }
45
+ return result;
46
+ }
47
+
48
+ inline void ThrowRuntimeException(JNIEnv* env, const std::string& message) {
49
+ jclass exception = env->FindClass("java/lang/RuntimeException");
50
+ if (exception != nullptr) {
51
+ env->ThrowNew(exception, message.c_str());
52
+ }
53
+ }
54
+
55
+ } // namespace jni_util
56
+ } // namespace pushy
@@ -8,7 +8,6 @@
8
8
  #include <sys/types.h>
9
9
  #include <unistd.h>
10
10
 
11
- #include <set>
12
11
  #include <vector>
13
12
 
14
13
  extern "C" {
@@ -17,6 +16,11 @@ extern "C" {
17
16
 
18
17
  namespace pushy {
19
18
  namespace patch {
19
+
20
+ namespace internal {
21
+ bool g_disable_hard_links = false;
22
+ } // namespace internal
23
+
20
24
  namespace {
21
25
 
22
26
  constexpr size_t kCopyBufferSize = 16 * 1024;
@@ -202,6 +206,17 @@ Status CopyFile(const std::string& from, const std::string& to, bool overwrite)
202
206
  }
203
207
  }
204
208
 
209
+ // Prefer a hard link over copying bytes: unchanged files between versions
210
+ // are identical, so linking is O(1) per file, writes nothing to flash, and
211
+ // shares disk blocks. Version directories are immutable once created (patch
212
+ // outputs are always written as new files), so sharing the inode with the
213
+ // source is safe. Fall back to a byte copy whenever linking is not possible
214
+ // (cross-device source such as the installed app bundle, EPERM, EMLINK, or
215
+ // filesystems without hard-link support).
216
+ if (!internal::g_disable_hard_links && link(from.c_str(), to.c_str()) == 0) {
217
+ return Status::Ok();
218
+ }
219
+
205
220
  FILE* source = std::fopen(from.c_str(), "rb");
206
221
  if (!source) {
207
222
  return MakeErrnoStatus("Failed to open source file " + from);
@@ -64,5 +64,11 @@ Status CleanupOldEntries(
64
64
 
65
65
  bool IsSafeRelativePath(const std::string& path);
66
66
 
67
+ namespace internal {
68
+ // Test-only escape hatch: forces file copies to take the byte-copy fallback
69
+ // instead of hard-linking, so tests can cover both paths on one filesystem.
70
+ extern bool g_disable_hard_links;
71
+ } // namespace internal
72
+
67
73
  } // namespace patch
68
74
  } // namespace pushy
@@ -3,47 +3,14 @@
3
3
  #include <string>
4
4
  #include <vector>
5
5
 
6
+ #include "jni_util.h"
6
7
  #include "patch_core.h"
7
8
 
8
9
  namespace {
9
10
 
10
- std::string JStringToString(JNIEnv* env, jstring value) {
11
- if (value == nullptr) {
12
- return std::string();
13
- }
14
-
15
- const char* chars = env->GetStringUTFChars(value, nullptr);
16
- if (chars == nullptr) {
17
- return std::string();
18
- }
19
-
20
- std::string result(chars);
21
- env->ReleaseStringUTFChars(value, chars);
22
- return result;
23
- }
24
-
25
- std::vector<std::string> JArrayToVector(JNIEnv* env, jobjectArray values) {
26
- std::vector<std::string> result;
27
- if (values == nullptr) {
28
- return result;
29
- }
30
-
31
- const jsize size = env->GetArrayLength(values);
32
- result.reserve(static_cast<size_t>(size));
33
- for (jsize i = 0; i < size; ++i) {
34
- auto* item = static_cast<jstring>(env->GetObjectArrayElement(values, i));
35
- result.push_back(JStringToString(env, item));
36
- env->DeleteLocalRef(item);
37
- }
38
- return result;
39
- }
40
-
41
- void ThrowRuntimeException(JNIEnv* env, const std::string& message) {
42
- jclass exception = env->FindClass("java/lang/RuntimeException");
43
- if (exception != nullptr) {
44
- env->ThrowNew(exception, message.c_str());
45
- }
46
- }
11
+ using pushy::jni_util::JArrayToVector;
12
+ using pushy::jni_util::JStringToString;
13
+ using pushy::jni_util::ThrowRuntimeException;
47
14
 
48
15
  } // namespace
49
16
 
@@ -0,0 +1,24 @@
1
+ #pragma once
2
+
3
+ // Single source of truth for the state-machine operation codes shared across
4
+ // the platform glue layers (Android JNI in update_core_android.cpp and
5
+ // HarmonyOS NAPI in pushy.cpp). The integer values MUST stay in sync with the
6
+ // callers on each platform:
7
+ // - Android: UpdateContext.java (STATE_OP_* constants)
8
+ // - HarmonyOS: UpdateContext.ts (StateOperation usage)
9
+ // Do not renumber existing entries; only append new ones.
10
+
11
+ namespace pushy {
12
+ namespace state_ops {
13
+
14
+ enum class StateOperation {
15
+ kSwitchVersion = 1,
16
+ kMarkSuccess = 2,
17
+ kRollback = 3,
18
+ kClearFirstTime = 4,
19
+ kClearRollbackMark = 5,
20
+ kResolveLaunch = 6,
21
+ };
22
+
23
+ } // namespace state_ops
24
+ } // namespace pushy