react-native-update 10.39.0 → 10.40.0-beta.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 (44) hide show
  1. package/android/build.gradle +0 -6
  2. package/android/jni/Application.mk +1 -1
  3. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  4. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  5. package/android/lib/x86/librnupdate.so +0 -0
  6. package/android/lib/x86_64/librnupdate.so +0 -0
  7. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +314 -0
  8. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +253 -586
  9. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateCore.java +1 -9
  10. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +220 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +9 -3
  12. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +36 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +36 -26
  14. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +39 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +74 -0
  16. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +143 -260
  17. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +63 -0
  18. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +1 -5
  19. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +26 -73
  20. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +28 -242
  21. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +89 -135
  22. package/harmony/pushy/src/main/cpp/PushyTurboModule.h +5 -5
  23. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +7 -7
  24. package/harmony/pushy/src/main/ets/PushyPackage.ets +3 -9
  25. package/harmony/pushy/src/main/ets/PushyPackageCompat.ts +3 -9
  26. package/harmony/pushy/src/main/ets/PushyPackageFactory.ts +14 -0
  27. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +124 -24
  28. package/harmony/pushy/src/main/ets/UpdateContext.ts +92 -70
  29. package/harmony/pushy.har +0 -0
  30. package/ios/Expo/ExpoPushyReactDelegateHandler.swift +6 -26
  31. package/ios/RCTPushy/RCTPushy.mm +315 -259
  32. package/ios/RCTPushy/RCTPushyDownloader.mm +52 -29
  33. package/package.json +2 -2
  34. package/react-native-update.podspec +3 -3
  35. package/harmony/pushy/src/main/ets/UpdateModuleImpl.ts +0 -123
  36. package/ios/ImportReact.h +0 -2
  37. package/ios/RCTPushy/HDiffPatch/HDiffPatch.h +0 -16
  38. package/ios/RCTPushy/HDiffPatch/HDiffPatch.mm +0 -35
  39. package/ios/RCTPushy/RCTPushyManager.h +0 -27
  40. package/ios/RCTPushy/RCTPushyManager.mm +0 -181
  41. package/ios/RCTPushy.xcodeproj/project.pbxproj +0 -479
  42. package/package/harmony/pushy.har +0 -0
  43. package/react-native-update-10.39.0-beta.3.tgz +0 -0
  44. package/scripts/prune-host-stl.sh +0 -6
@@ -1,198 +1,146 @@
1
1
  package cn.reactnative.modules.update;
2
2
 
3
3
  import android.content.Context;
4
- import android.content.pm.ApplicationInfo;
5
- import android.content.pm.PackageManager;
6
- import android.content.res.Resources;
7
- import android.os.AsyncTask;
8
- import android.os.Build;
9
- import android.util.DisplayMetrics;
4
+ import android.os.Handler;
5
+ import android.os.Looper;
10
6
  import android.util.Log;
11
- import android.util.TypedValue;
12
7
  import com.facebook.react.bridge.Arguments;
13
8
  import com.facebook.react.bridge.WritableMap;
14
-
15
- import okhttp3.OkHttpClient;
16
- import okhttp3.Request;
17
- import okhttp3.Response;
18
- import okhttp3.ResponseBody;
19
-
20
- import org.json.JSONException;
21
- import org.json.JSONObject;
22
- import org.json.JSONTokener;
23
-
24
9
  import java.io.ByteArrayOutputStream;
25
10
  import java.io.File;
26
- import java.io.FileInputStream;
27
- import java.io.FileOutputStream;
28
11
  import java.io.IOException;
29
12
  import java.io.InputStream;
13
+ import java.nio.charset.StandardCharsets;
30
14
  import java.util.ArrayList;
31
15
  import java.util.Enumeration;
16
+ import java.util.HashMap;
32
17
  import java.util.Iterator;
33
18
  import java.util.zip.ZipEntry;
34
- import java.util.HashMap;
35
- import java.util.regex.Pattern;
36
-
19
+ import okhttp3.OkHttpClient;
20
+ import okhttp3.Request;
21
+ import okhttp3.Response;
22
+ import okhttp3.ResponseBody;
37
23
  import okio.BufferedSink;
38
24
  import okio.BufferedSource;
39
25
  import okio.Okio;
40
- import static cn.reactnative.modules.update.UpdateModule.sendEvent;
41
-
42
-
43
- class DownloadTask extends AsyncTask<DownloadTaskParams, long[], Void> {
44
- final int DOWNLOAD_CHUNK_SIZE = 4096;
45
-
46
- Context context;
47
- String hash;
26
+ import org.json.JSONException;
27
+ import org.json.JSONObject;
28
+ import org.json.JSONTokener;
48
29
 
49
- DownloadTask(Context context) {
50
- this.context = context;
51
- }
30
+ class DownloadTask implements Runnable {
31
+ private static final int DOWNLOAD_CHUNK_SIZE = 4096;
32
+ private static final OkHttpClient HTTP_CLIENT = new OkHttpClient();
52
33
 
53
34
  static {
54
35
  NativeUpdateCore.ensureLoaded();
55
36
  }
56
37
 
57
- private void removeDirectory(File file) throws IOException {
58
- if (UpdateContext.DEBUG) {
59
- Log.d("react-native-update", "Removing " + file);
60
- }
61
- if (file.isDirectory()) {
62
- File[] files = file.listFiles();
63
- for (File f : files) {
64
- String name = f.getName();
65
- if (name.equals(".") || name.equals("..")) {
66
- continue;
67
- }
68
- removeDirectory(f);
69
- }
70
- }
71
- if (file.exists() && !file.delete()) {
72
- throw new IOException("Failed to delete directory");
73
- }
38
+ private static final class PatchArchiveContents {
39
+ final ArrayList<String> entryNames = new ArrayList<String>();
40
+ final ArrayList<String> copyFroms = new ArrayList<String>();
41
+ final ArrayList<String> copyTos = new ArrayList<String>();
42
+ final ArrayList<String> deletes = new ArrayList<String>();
74
43
  }
75
44
 
76
- private void downloadFile(DownloadTaskParams param) throws IOException {
77
- String url = param.url;
78
- File writePath = param.targetFile;
79
- this.hash = param.hash;
80
- OkHttpClient client = new OkHttpClient();
81
- Request request = new Request.Builder().url(url)
82
- .build();
83
- Response response = client.newCall(request).execute();
84
- if (response.code() > 299) {
85
- throw new Error("Server error:" + response.code() + " " + response.message());
86
- }
87
- ResponseBody body = response.body();
88
- long contentLength = body.contentLength();
89
- BufferedSource source = body.source();
45
+ private final Context context;
46
+ private final DownloadTaskParams params;
47
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
48
+ private final byte[] buffer = new byte[DOWNLOAD_CHUNK_SIZE];
49
+ private final BundledResourceCopier bundledResourceCopier;
50
+ private String hash;
51
+
52
+ DownloadTask(Context context, DownloadTaskParams params) {
53
+ this.context = context.getApplicationContext();
54
+ this.params = params;
55
+ this.bundledResourceCopier = new BundledResourceCopier(this.context);
56
+ }
90
57
 
91
- if (writePath.exists()) {
92
- writePath.delete();
93
- }
58
+ private void postProgress(final long received, final long total) {
59
+ mainHandler.post(new Runnable() {
60
+ @Override
61
+ public void run() {
62
+ WritableMap progress = Arguments.createMap();
63
+ progress.putDouble("received", received);
64
+ progress.putDouble("total", total);
65
+ progress.putString("hash", hash);
66
+ UpdateEventEmitter.sendEvent("RCTPushyDownloadProgress", progress);
67
+ }
68
+ });
69
+ }
94
70
 
95
- BufferedSink sink = Okio.buffer(Okio.sink(writePath));
71
+ private void downloadFile() throws IOException {
72
+ this.hash = params.hash;
73
+ String url = params.url;
74
+ File writePath = params.targetFile;
75
+ UpdateFileUtils.ensureParentDirectory(writePath);
76
+ Request request = new Request.Builder().url(url).build();
96
77
 
97
- if (UpdateContext.DEBUG) {
98
- Log.d("react-native-update", "Downloading " + url);
78
+ if (writePath.exists() && !writePath.delete()) {
79
+ throw new IOException("Failed to replace existing file: " + writePath);
99
80
  }
100
81
 
101
- long bytesRead = 0;
102
- long received = 0;
103
- int currentPercentage = 0;
104
- while ((bytesRead = source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1) {
105
- received += bytesRead;
106
- sink.emit();
107
-
108
- int percentage = (int)(received * 100.0 / contentLength + 0.5);
109
- if (percentage > currentPercentage) {
110
- currentPercentage = percentage;
111
- publishProgress(new long[]{received, contentLength});
112
- }
113
- }
114
- if (received != contentLength) {
115
- throw new Error("Unexpected eof while reading downloaded update");
116
- }
117
- publishProgress(new long[]{received, contentLength});
118
- sink.writeAll(source);
119
- sink.close();
120
-
121
82
  if (UpdateContext.DEBUG) {
122
- Log.d("react-native-update", "Download finished");
83
+ Log.d(UpdateContext.TAG, "Downloading " + url);
123
84
  }
124
- }
125
85
 
126
- @Override
127
- protected void onProgressUpdate(final long[]... values) {
128
- super.onProgressUpdate(values);
129
- WritableMap params = Arguments.createMap();
130
- params.putDouble("received", (values[0][0]));
131
- params.putDouble("total", (values[0][1]));
132
- params.putString("hash", this.hash);
133
- sendEvent("RCTPushyDownloadProgress", params);
134
- }
135
-
136
- byte[] buffer = new byte[1024*4];
86
+ try (Response response = HTTP_CLIENT.newCall(request).execute()) {
87
+ if (!response.isSuccessful()) {
88
+ throw new IOException("Server error: " + response.code() + " " + response.message());
89
+ }
137
90
 
138
- private static native void applyPatchFromFileSource(
139
- String sourceRoot,
140
- String targetRoot,
141
- String originBundlePath,
142
- String bundlePatchPath,
143
- String bundleOutputPath,
144
- String mergeSourceSubdir,
145
- boolean enableMerge,
146
- String[] copyFroms,
147
- String[] copyTos,
148
- String[] deletes
149
- );
150
- private static native void cleanupOldEntries(
151
- String rootDir,
152
- String keepCurrent,
153
- String keepPrevious,
154
- int maxAgeDays
155
- );
156
- private static native ArchivePatchPlanResult buildArchivePatchPlan(
157
- int patchType,
158
- String[] entryNames,
159
- String[] copyFroms,
160
- String[] copyTos,
161
- String[] deletes
162
- );
163
- private static native CopyGroupResult[] buildCopyGroups(
164
- String[] copyFroms,
165
- String[] copyTos
166
- );
91
+ ResponseBody body = response.body();
92
+ if (body == null) {
93
+ throw new IOException("Empty response body for " + url);
94
+ }
167
95
 
96
+ long contentLength = body.contentLength();
97
+ long bytesRead;
98
+ long received = 0;
99
+ int currentPercentage = 0;
168
100
 
169
- private void copyFile(File from, File fmd) throws IOException {
170
- int count;
101
+ try (
102
+ BufferedSource source = body.source();
103
+ BufferedSink sink = Okio.buffer(Okio.sink(writePath))
104
+ ) {
105
+ while ((bytesRead = source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1) {
106
+ received += bytesRead;
107
+ sink.emit();
171
108
 
172
- InputStream in = new FileInputStream(from);
173
- FileOutputStream fout = new FileOutputStream(fmd);
109
+ if (contentLength > 0) {
110
+ int percentage = (int) (received * 100.0 / contentLength + 0.5);
111
+ if (percentage > currentPercentage) {
112
+ currentPercentage = percentage;
113
+ postProgress(received, contentLength);
114
+ }
115
+ } else {
116
+ postProgress(received, contentLength);
117
+ }
118
+ }
119
+ sink.flush();
120
+ }
174
121
 
175
- while ((count = in.read(buffer)) != -1)
176
- {
177
- fout.write(buffer, 0, count);
122
+ if (contentLength >= 0 && received != contentLength) {
123
+ throw new IOException("Unexpected eof while reading downloaded update");
124
+ }
125
+ postProgress(received, contentLength);
178
126
  }
179
127
 
180
- fout.close();
181
- in.close();
128
+ if (UpdateContext.DEBUG) {
129
+ Log.d(UpdateContext.TAG, "Download finished");
130
+ }
182
131
  }
183
132
 
184
- private byte[] readBytes(InputStream zis) throws IOException {
185
- int count;
186
-
187
- ByteArrayOutputStream fout = new ByteArrayOutputStream();
188
- while ((count = zis.read(buffer)) != -1)
189
- {
190
- fout.write(buffer, 0, count);
133
+ private byte[] readBytes(InputStream input) throws IOException {
134
+ try (
135
+ InputStream in = input;
136
+ ByteArrayOutputStream out = new ByteArrayOutputStream()
137
+ ) {
138
+ int count;
139
+ while ((count = in.read(buffer)) != -1) {
140
+ out.write(buffer, 0, count);
141
+ }
142
+ return out.toByteArray();
191
143
  }
192
-
193
- fout.close();
194
- zis.close();
195
- return fout.toByteArray();
196
144
  }
197
145
 
198
146
  private void appendManifestEntries(
@@ -225,20 +173,8 @@ class DownloadTask extends AsyncTask<DownloadTaskParams, long[], Void> {
225
173
  }
226
174
 
227
175
  private void copyBundledAssetToFile(String assetName, File destination) throws IOException {
228
- InputStream in = context.getAssets().open(assetName);
229
- copyInputStreamToFile(in, destination);
230
- }
231
-
232
- private void copyInputStreamToFile(InputStream in, File destination) throws IOException {
233
- FileOutputStream fout = new FileOutputStream(destination);
234
- try {
235
- int count;
236
- while ((count = in.read(buffer)) != -1) {
237
- fout.write(buffer, 0, count);
238
- }
239
- } finally {
240
- fout.close();
241
- in.close();
176
+ try (InputStream in = context.getAssets().open(assetName)) {
177
+ UpdateFileUtils.copyInputStreamToFile(in, destination);
242
178
  }
243
179
  }
244
180
 
@@ -270,368 +206,85 @@ class DownloadTask extends AsyncTask<DownloadTaskParams, long[], Void> {
270
206
  return copyList;
271
207
  }
272
208
 
273
- private void doFullPatch(DownloadTaskParams param) throws IOException {
274
- downloadFile(param);
275
-
276
- removeDirectory(param.unzipDirectory);
277
- param.unzipDirectory.mkdirs();
278
-
279
- SafeZipFile zipFile = new SafeZipFile(param.targetFile);
280
- Enumeration<? extends ZipEntry> entries = zipFile.entries();
281
- while (entries.hasMoreElements()) {
282
- ZipEntry ze = entries.nextElement();
283
-
284
- zipFile.unzipToPath(ze, param.unzipDirectory);
285
- }
286
-
287
- zipFile.close();
288
-
289
-
290
- if (UpdateContext.DEBUG) {
291
- Log.d("react-native-update", "Unzip finished");
292
- }
293
- }
294
-
295
- // Pattern to strip -vN version qualifiers from resource directory paths
296
- // e.g., "res/drawable-xxhdpi-v4/img.png" → "res/drawable-xxhdpi/img.png"
297
- private static final Pattern VERSION_QUALIFIER_PATTERN =
298
- Pattern.compile("-v\\d+(?=/)");
299
- // AAB internal paths are prefixed with "base/" (e.g., "base/res/drawable-xxhdpi/img.png")
300
- // which does not exist in standard APK layout
301
- private static final String AAB_BASE_PREFIX = "base/";
302
-
303
- private String normalizeResPath(String path) {
304
- String result = path;
305
- if (result.startsWith(AAB_BASE_PREFIX)) {
306
- result = result.substring(AAB_BASE_PREFIX.length());
307
- }
308
- return VERSION_QUALIFIER_PATTERN.matcher(result).replaceAll("");
309
- }
310
-
311
- private static class ResolvedResourceSource {
312
- final int resourceId;
313
- final String assetPath;
314
-
315
- ResolvedResourceSource(int resourceId, String assetPath) {
316
- this.resourceId = resourceId;
317
- this.assetPath = assetPath;
318
- }
319
- }
320
-
321
- private String extractResourceType(String directoryName) {
322
- int qualifierIndex = directoryName.indexOf('-');
323
- if (qualifierIndex == -1) {
324
- return directoryName;
325
- }
326
- return directoryName.substring(0, qualifierIndex);
327
- }
209
+ private PatchArchiveContents extractPatchArchive(File archiveFile, File unzipDirectory)
210
+ throws IOException, JSONException {
211
+ UpdateFileUtils.removeDirectory(unzipDirectory);
212
+ UpdateFileUtils.ensureDirectory(unzipDirectory);
328
213
 
329
- private String extractResourceName(String fileName) {
330
- if (fileName.endsWith(".9.png")) {
331
- return fileName.substring(0, fileName.length() - ".9.png".length());
332
- }
333
- int extensionIndex = fileName.lastIndexOf('.');
334
- if (extensionIndex == -1) {
335
- return fileName;
336
- }
337
- return fileName.substring(0, extensionIndex);
338
- }
214
+ PatchArchiveContents contents = new PatchArchiveContents();
215
+ try (SafeZipFile zipFile = new SafeZipFile(archiveFile)) {
216
+ Enumeration<? extends ZipEntry> entries = zipFile.entries();
217
+ while (entries.hasMoreElements()) {
218
+ ZipEntry entry = entries.nextElement();
219
+ String name = entry.getName();
220
+ contents.entryNames.add(name);
221
+
222
+ if (name.equals("__diff.json")) {
223
+ byte[] bytes = readBytes(zipFile.getInputStream(entry));
224
+ String json = new String(bytes, StandardCharsets.UTF_8);
225
+ JSONObject manifest = (JSONObject) new JSONTokener(json).nextValue();
226
+ appendManifestEntries(
227
+ manifest,
228
+ contents.copyFroms,
229
+ contents.copyTos,
230
+ contents.deletes
231
+ );
232
+ continue;
233
+ }
339
234
 
340
- private Integer parseDensityQualifier(String directoryName) {
341
- String[] qualifiers = directoryName.split("-");
342
- for (String qualifier : qualifiers) {
343
- if ("ldpi".equals(qualifier)) {
344
- return DisplayMetrics.DENSITY_LOW;
345
- }
346
- if ("mdpi".equals(qualifier)) {
347
- return DisplayMetrics.DENSITY_MEDIUM;
348
- }
349
- if ("hdpi".equals(qualifier)) {
350
- return DisplayMetrics.DENSITY_HIGH;
351
- }
352
- if ("xhdpi".equals(qualifier)) {
353
- return DisplayMetrics.DENSITY_XHIGH;
354
- }
355
- if ("xxhdpi".equals(qualifier)) {
356
- return DisplayMetrics.DENSITY_XXHIGH;
357
- }
358
- if ("xxxhdpi".equals(qualifier)) {
359
- return DisplayMetrics.DENSITY_XXXHIGH;
360
- }
361
- if ("tvdpi".equals(qualifier)) {
362
- return DisplayMetrics.DENSITY_TV;
235
+ zipFile.unzipToPath(entry, unzipDirectory);
363
236
  }
364
237
  }
365
- return null;
238
+ return contents;
366
239
  }
367
240
 
368
- private ResolvedResourceSource resolveBundledResource(String resourcePath) {
369
- String normalizedPath = normalizeResPath(resourcePath);
370
- if (normalizedPath.startsWith("res/")) {
371
- normalizedPath = normalizedPath.substring("res/".length());
372
- }
373
-
374
- int slash = normalizedPath.indexOf('/');
375
- if (slash == -1 || slash == normalizedPath.length() - 1) {
376
- return null;
377
- }
378
-
379
- String directoryName = normalizedPath.substring(0, slash);
380
- String fileName = normalizedPath.substring(slash + 1);
381
- String resourceType = extractResourceType(directoryName);
382
- String resourceName = extractResourceName(fileName);
383
- if (resourceType == null || resourceType.isEmpty() || resourceName.isEmpty()) {
384
- return null;
385
- }
241
+ private void doFullPatch() throws IOException {
242
+ downloadFile();
386
243
 
387
- Resources resources = context.getResources();
388
- int resourceId = resources.getIdentifier(resourceName, resourceType, context.getPackageName());
389
- if (resourceId == 0) {
390
- return null;
391
- }
244
+ UpdateFileUtils.removeDirectory(params.unzipDirectory);
245
+ UpdateFileUtils.ensureDirectory(params.unzipDirectory);
392
246
 
393
- TypedValue typedValue = new TypedValue();
394
- try {
395
- Integer density = parseDensityQualifier(directoryName);
396
- if (density != null) {
397
- resources.getValueForDensity(resourceId, density, typedValue, true);
398
- } else {
399
- resources.getValue(resourceId, typedValue, true);
400
- }
401
- } catch (Resources.NotFoundException e) {
402
- if (UpdateContext.DEBUG) {
403
- Log.d("react-native-update", "Failed to resolve resource value for " + resourcePath + ": " + e.getMessage());
404
- }
405
- return null;
406
- }
407
-
408
- if (typedValue.string == null) {
409
- return null;
410
- }
411
-
412
- String assetPath = typedValue.string.toString();
413
- if (assetPath.startsWith("/")) {
414
- assetPath = assetPath.substring(1);
415
- }
416
-
417
- if (UpdateContext.DEBUG) {
418
- Log.d("react-native-update", "Resolved resource path " + resourcePath + " -> " + assetPath);
419
- }
420
- return new ResolvedResourceSource(resourceId, assetPath);
421
- }
422
-
423
- private InputStream openResolvedResourceStream(ResolvedResourceSource source) throws IOException {
424
- try {
425
- return context.getResources().openRawResource(source.resourceId);
426
- } catch (Resources.NotFoundException e) {
427
- throw new IOException("Unable to open resolved resource: " + source.assetPath, e);
428
- }
429
- }
430
-
431
- private void copyFromResource(HashMap<String, ArrayList<File> > resToCopy) throws IOException {
432
- if (UpdateContext.DEBUG) {
433
- Log.d("react-native-update", "copyFromResource called, resToCopy size: " + resToCopy.size());
434
- }
435
-
436
- // 收集所有 APK 路径(包括基础 APK 和所有 split APK)
437
- ArrayList<String> apkPaths = new ArrayList<>();
438
- apkPaths.add(context.getPackageResourcePath());
439
-
440
- // 获取所有 split APK 路径(用于资源分割的情况)
441
- try {
442
- ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(
443
- context.getPackageName(), 0);
444
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && appInfo.splitSourceDirs != null) {
445
- for (String splitPath : appInfo.splitSourceDirs) {
446
- apkPaths.add(splitPath);
447
- if (UpdateContext.DEBUG) {
448
- Log.d("react-native-update", "Found split APK: " + splitPath);
449
- }
450
- }
451
- }
452
- } catch (PackageManager.NameNotFoundException e) {
453
- if (UpdateContext.DEBUG) {
454
- Log.w("react-native-update", "Failed to get application info: " + e.getMessage());
455
- }
456
- }
457
-
458
- // 第一遍:从所有 APK 中收集所有可用的 zip 条目
459
- HashMap<String, ZipEntry> availableEntries = new HashMap<>();
460
- HashMap<String, SafeZipFile> zipFileMap = new HashMap<>(); // 保存每个路径对应的 ZipFile
461
- HashMap<String, SafeZipFile> entryToZipFileMap = new HashMap<>(); // 保存每个条目对应的 ZipFile
462
-
463
- for (String apkPath : apkPaths) {
464
- SafeZipFile zipFile = new SafeZipFile(new File(apkPath));
465
- zipFileMap.put(apkPath, zipFile);
247
+ try (SafeZipFile zipFile = new SafeZipFile(params.targetFile)) {
466
248
  Enumeration<? extends ZipEntry> entries = zipFile.entries();
467
249
  while (entries.hasMoreElements()) {
468
- ZipEntry ze = entries.nextElement();
469
- String entryName = ze.getName();
470
- // 如果条目已存在,保留第一个(基础 APK 优先)
471
- if (!availableEntries.containsKey(entryName)) {
472
- availableEntries.put(entryName, ze);
473
- entryToZipFileMap.put(entryName, zipFile);
474
- }
250
+ zipFile.unzipToPath(entries.nextElement(), params.unzipDirectory);
475
251
  }
476
252
  }
477
-
478
- // 构建规范化路径映射,用于 APK ↔ AAB 版本限定符无关匹配
479
- // 例如 "res/drawable-xxhdpi-v4/img.png" → "res/drawable-xxhdpi/img.png"
480
- HashMap<String, String> normalizedEntryMap = new HashMap<>();
481
- for (String entryName : availableEntries.keySet()) {
482
- String normalized = normalizeResPath(entryName);
483
- normalizedEntryMap.putIfAbsent(normalized, entryName);
484
- }
485
-
486
- // 使用基础 APK 的 ZipFile 作为主要操作对象
487
- SafeZipFile zipFile = zipFileMap.get(context.getPackageResourcePath());
488
-
489
- // 处理所有需要复制的文件
490
- HashMap<String, ArrayList<File>> remainingFiles = new HashMap<>(resToCopy);
491
-
492
- for (String fromPath : new ArrayList<>(remainingFiles.keySet())) {
493
- if (UpdateContext.DEBUG) {
494
- Log.d("react-native-update", "Processing fromPath: " + fromPath);
495
- }
496
- ArrayList<File> targets = remainingFiles.get(fromPath);
497
- if (targets == null || targets.isEmpty()) {
498
- continue;
499
- }
500
-
501
- ZipEntry ze = availableEntries.get(fromPath);
502
- String actualSourcePath = fromPath;
503
- ResolvedResourceSource resolvedResource = null;
504
-
505
- // 如果精确匹配找不到,尝试版本限定符无关匹配(APK ↔ AAB 兼容)
506
- // 例如 __diff.json 中的 "res/drawable-xxhdpi-v4/img.png" 匹配设备上的 "res/drawable-xxhdpi/img.png"
507
- if (ze == null) {
508
- String normalizedFrom = normalizeResPath(fromPath);
509
- String actualEntry = normalizedEntryMap.get(normalizedFrom);
510
- if (actualEntry != null) {
511
- ze = availableEntries.get(actualEntry);
512
- actualSourcePath = actualEntry;
513
- if (UpdateContext.DEBUG) {
514
- Log.d("react-native-update", "Normalized match: " + fromPath + " -> " + actualEntry);
515
- }
516
- }
517
- }
518
253
 
519
- // release APK 可能会将资源 entry 名压缩为 res/9w.png 之类的短路径;
520
- // 这时通过 Resources 解析逻辑资源名,再直接读取资源内容。
521
- if (ze == null) {
522
- resolvedResource = resolveBundledResource(fromPath);
523
- if (resolvedResource != null) {
524
- actualSourcePath = resolvedResource.assetPath;
525
- }
526
- }
527
-
528
- if (ze != null || resolvedResource != null) {
529
- File lastTarget = null;
530
- for (File target: targets) {
531
- if (UpdateContext.DEBUG) {
532
- Log.d("react-native-update", "Copying from resource " + actualSourcePath + " to " + target);
533
- }
534
- try {
535
- // 确保目标文件的父目录存在
536
- File parentDir = target.getParentFile();
537
- if (parentDir != null && !parentDir.exists()) {
538
- parentDir.mkdirs();
539
- }
540
-
541
- if (lastTarget != null) {
542
- copyFile(lastTarget, target);
543
- } else {
544
- if (ze != null) {
545
- // 从保存的映射中获取包含该条目的 ZipFile
546
- SafeZipFile sourceZipFile = entryToZipFileMap.get(actualSourcePath);
547
- if (sourceZipFile == null) {
548
- sourceZipFile = zipFile; // 回退到基础 APK
549
- }
550
- sourceZipFile.unzipToFile(ze, target);
551
- } else {
552
- InputStream in = openResolvedResourceStream(resolvedResource);
553
- copyInputStreamToFile(in, target);
554
- }
555
- lastTarget = target;
556
- }
557
- } catch (IOException e) {
558
- if (UpdateContext.DEBUG) {
559
- Log.w("react-native-update", "Failed to copy resource " + actualSourcePath + " to " + target + ": " + e.getMessage());
560
- }
561
- // 继续处理下一个目标
562
- }
563
- }
564
- remainingFiles.remove(fromPath);
565
- }
566
- }
567
-
568
- // 处理剩余的文件(如果还有的话)
569
- if (!remainingFiles.isEmpty() && UpdateContext.DEBUG) {
570
- for (String fromPath : remainingFiles.keySet()) {
571
- Log.w("react-native-update", "Resource not found and no fallback available: " + fromPath);
572
- }
573
- }
574
-
575
- // 关闭所有 ZipFile
576
- for (SafeZipFile zf : zipFileMap.values()) {
577
- zf.close();
254
+ if (UpdateContext.DEBUG) {
255
+ Log.d(UpdateContext.TAG, "Unzip finished");
578
256
  }
579
257
  }
580
258
 
581
- private void doPatchFromApk(DownloadTaskParams param) throws IOException, JSONException {
582
- downloadFile(param);
583
-
584
- removeDirectory(param.unzipDirectory);
585
- param.unzipDirectory.mkdirs();
586
- ArrayList<String> entryNames = new ArrayList<String>();
587
- ArrayList<String> copyFroms = new ArrayList<String>();
588
- ArrayList<String> copyTos = new ArrayList<String>();
589
- ArrayList<String> deletes = new ArrayList<String>();
590
-
591
- SafeZipFile zipFile = new SafeZipFile(param.targetFile);
592
- Enumeration<? extends ZipEntry> entries = zipFile.entries();
593
- while (entries.hasMoreElements()) {
594
- ZipEntry ze = entries.nextElement();
595
- String fn = ze.getName();
596
- entryNames.add(fn);
597
-
598
- if (fn.equals("__diff.json")) {
599
- // copy files from assets
600
- byte[] bytes = readBytes(zipFile.getInputStream(ze));
601
- String json = new String(bytes, "UTF-8");
602
- JSONObject obj = (JSONObject)new JSONTokener(json).nextValue();
603
- appendManifestEntries(obj, copyFroms, copyTos, deletes);
604
- continue;
605
- }
606
- zipFile.unzipToPath(ze, param.unzipDirectory);
607
- }
608
-
609
- zipFile.close();
259
+ private void doPatchFromApk() throws IOException, JSONException {
260
+ downloadFile();
261
+ PatchArchiveContents contents = extractPatchArchive(params.targetFile, params.unzipDirectory);
610
262
 
611
263
  buildArchivePatchPlan(
612
264
  DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK,
613
- entryNames.toArray(new String[0]),
614
- copyFroms.toArray(new String[0]),
615
- copyTos.toArray(new String[0]),
616
- deletes.toArray(new String[0])
265
+ contents.entryNames.toArray(new String[0]),
266
+ contents.copyFroms.toArray(new String[0]),
267
+ contents.copyTos.toArray(new String[0]),
268
+ contents.deletes.toArray(new String[0])
617
269
  );
270
+
618
271
  HashMap<String, ArrayList<File>> copyList = buildCopyList(
619
- param.unzipDirectory,
272
+ params.unzipDirectory,
620
273
  buildCopyGroups(
621
- copyFroms.toArray(new String[0]),
622
- copyTos.toArray(new String[0])
274
+ contents.copyFroms.toArray(new String[0]),
275
+ contents.copyTos.toArray(new String[0])
623
276
  )
624
277
  );
625
278
 
626
- File originBundleFile = new File(param.unzipDirectory, ".origin.bundle");
279
+ File originBundleFile = new File(params.unzipDirectory, ".origin.bundle");
627
280
  copyBundledAssetToFile("index.android.bundle", originBundleFile);
628
281
  try {
629
282
  applyPatchFromFileSource(
630
- param.unzipDirectory.getAbsolutePath(),
631
- param.unzipDirectory.getAbsolutePath(),
283
+ params.unzipDirectory.getAbsolutePath(),
284
+ params.unzipDirectory.getAbsolutePath(),
632
285
  originBundleFile.getAbsolutePath(),
633
- new File(param.unzipDirectory, "index.bundlejs.patch").getAbsolutePath(),
634
- new File(param.unzipDirectory, "index.bundlejs").getAbsolutePath(),
286
+ new File(params.unzipDirectory, "index.bundlejs.patch").getAbsolutePath(),
287
+ new File(params.unzipDirectory, "index.bundlejs").getAbsolutePath(),
635
288
  "",
636
289
  false,
637
290
  new String[0],
@@ -643,144 +296,158 @@ class DownloadTask extends AsyncTask<DownloadTaskParams, long[], Void> {
643
296
  }
644
297
 
645
298
  if (UpdateContext.DEBUG) {
646
- Log.d("react-native-update", "copyList size: " + copyList.size());
299
+ Log.d(UpdateContext.TAG, "copyList size: " + copyList.size());
647
300
  for (String from : copyList.keySet()) {
648
- Log.d("react-native-update", "copyList entry: " + from + " -> " + copyList.get(from).size() + " targets");
301
+ Log.d(
302
+ UpdateContext.TAG,
303
+ "copyList entry: " + from + " -> " + copyList.get(from).size() + " targets"
304
+ );
649
305
  }
650
306
  }
651
307
 
652
- copyFromResource(copyList);
308
+ bundledResourceCopier.copyFromResource(copyList);
653
309
 
654
310
  if (UpdateContext.DEBUG) {
655
- Log.d("react-native-update", "Unzip finished");
311
+ Log.d(UpdateContext.TAG, "Unzip finished");
656
312
  }
657
-
658
313
  }
659
314
 
660
- private void doPatchFromPpk(DownloadTaskParams param) throws IOException, JSONException {
661
- downloadFile(param);
662
-
663
- removeDirectory(param.unzipDirectory);
664
- param.unzipDirectory.mkdirs();
665
-
666
- ArrayList<String> entryNames = new ArrayList<String>();
667
- ArrayList<String> copyFroms = new ArrayList<String>();
668
- ArrayList<String> copyTos = new ArrayList<String>();
669
- ArrayList<String> deletes = new ArrayList<String>();
670
-
671
-
672
- SafeZipFile zipFile = new SafeZipFile(param.targetFile);
673
- Enumeration<? extends ZipEntry> entries = zipFile.entries();
674
- while (entries.hasMoreElements()) {
675
- ZipEntry ze = entries.nextElement();
676
- String fn = ze.getName();
677
- entryNames.add(fn);
678
-
679
- if (fn.equals("__diff.json")) {
680
- // copy files from assets
681
- byte[] bytes = readBytes(zipFile.getInputStream(ze));
682
- String json = new String(bytes, "UTF-8");
683
- JSONObject obj = (JSONObject)new JSONTokener(json).nextValue();
684
- appendManifestEntries(obj, copyFroms, copyTos, deletes);
685
- continue;
686
- }
687
- zipFile.unzipToPath(ze, param.unzipDirectory);
688
- }
689
-
690
- zipFile.close();
315
+ private void doPatchFromPpk() throws IOException, JSONException {
316
+ downloadFile();
317
+ PatchArchiveContents contents = extractPatchArchive(params.targetFile, params.unzipDirectory);
691
318
 
692
319
  ArchivePatchPlanResult plan = buildArchivePatchPlan(
693
320
  DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK,
694
- entryNames.toArray(new String[0]),
695
- copyFroms.toArray(new String[0]),
696
- copyTos.toArray(new String[0]),
697
- deletes.toArray(new String[0])
321
+ contents.entryNames.toArray(new String[0]),
322
+ contents.copyFroms.toArray(new String[0]),
323
+ contents.copyTos.toArray(new String[0]),
324
+ contents.deletes.toArray(new String[0])
698
325
  );
699
326
 
700
327
  applyPatchFromFileSource(
701
- param.originDirectory.getAbsolutePath(),
702
- param.unzipDirectory.getAbsolutePath(),
703
- new File(param.originDirectory, "index.bundlejs").getAbsolutePath(),
704
- new File(param.unzipDirectory, "index.bundlejs.patch").getAbsolutePath(),
705
- new File(param.unzipDirectory, "index.bundlejs").getAbsolutePath(),
328
+ params.originDirectory.getAbsolutePath(),
329
+ params.unzipDirectory.getAbsolutePath(),
330
+ new File(params.originDirectory, "index.bundlejs").getAbsolutePath(),
331
+ new File(params.unzipDirectory, "index.bundlejs.patch").getAbsolutePath(),
332
+ new File(params.unzipDirectory, "index.bundlejs").getAbsolutePath(),
706
333
  plan.mergeSourceSubdir,
707
334
  plan.enableMerge,
708
- copyFroms.toArray(new String[0]),
709
- copyTos.toArray(new String[0]),
710
- deletes.toArray(new String[0])
335
+ contents.copyFroms.toArray(new String[0]),
336
+ contents.copyTos.toArray(new String[0]),
337
+ contents.deletes.toArray(new String[0])
711
338
  );
712
339
 
713
340
  if (UpdateContext.DEBUG) {
714
- Log.d("react-native-update", "Unzip finished");
341
+ Log.d(UpdateContext.TAG, "Unzip finished");
715
342
  }
716
343
  }
717
- private void doCleanUp(DownloadTaskParams param) throws IOException {
344
+
345
+ private void doCleanUp() {
718
346
  if (UpdateContext.DEBUG) {
719
- Log.d("react-native-update", "Start cleaning up");
347
+ Log.d(UpdateContext.TAG, "Start cleaning up");
720
348
  }
721
349
  cleanupOldEntries(
722
- param.unzipDirectory.getAbsolutePath(),
723
- param.hash,
724
- param.originHash,
350
+ params.unzipDirectory.getAbsolutePath(),
351
+ params.hash,
352
+ params.originHash,
725
353
  7
726
354
  );
727
355
  }
728
356
 
357
+ private void cleanUpAfterFailure(int taskType) {
358
+ switch (taskType) {
359
+ case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
360
+ case DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK:
361
+ case DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK:
362
+ try {
363
+ UpdateFileUtils.removeDirectory(params.unzipDirectory);
364
+ } catch (IOException ioException) {
365
+ Log.e(UpdateContext.TAG, "Failed to clean patched directory", ioException);
366
+ }
367
+ break;
368
+ case DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD:
369
+ if (
370
+ params.targetFile.exists()
371
+ && !params.targetFile.delete()
372
+ && UpdateContext.DEBUG
373
+ ) {
374
+ Log.w(UpdateContext.TAG, "Failed to clean partial download " + params.targetFile);
375
+ }
376
+ break;
377
+ default:
378
+ break;
379
+ }
380
+ }
381
+
729
382
  @Override
730
- protected Void doInBackground(final DownloadTaskParams... params) {
731
- int taskType = params[0].type;
383
+ public void run() {
384
+ int taskType = params.type;
732
385
  try {
733
386
  switch (taskType) {
734
387
  case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
735
- doFullPatch(params[0]);
388
+ doFullPatch();
736
389
  break;
737
390
  case DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK:
738
- doPatchFromApk(params[0]);
391
+ doPatchFromApk();
739
392
  break;
740
393
  case DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK:
741
- doPatchFromPpk(params[0]);
394
+ doPatchFromPpk();
742
395
  break;
743
396
  case DownloadTaskParams.TASK_TYPE_CLEANUP:
744
- doCleanUp(params[0]);
397
+ doCleanUp();
745
398
  break;
746
399
  case DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD:
747
- downloadFile(params[0]);
400
+ downloadFile();
748
401
  break;
749
402
  default:
750
403
  break;
751
404
  }
752
- if (params[0].listener != null) {
753
- params[0].listener.onDownloadCompleted(params[0]);
405
+
406
+ if (params.listener != null) {
407
+ params.listener.onDownloadCompleted(params);
754
408
  }
755
- } catch (Throwable e) {
409
+ } catch (Throwable error) {
756
410
  if (UpdateContext.DEBUG) {
757
- e.printStackTrace();
411
+ Log.e(UpdateContext.TAG, "download task failed", error);
758
412
  }
759
- switch (taskType) {
760
- case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
761
- case DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK:
762
- case DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK:
763
- try {
764
- removeDirectory(params[0].unzipDirectory);
765
- } catch (IOException ioException) {
766
- ioException.printStackTrace();
767
- }
768
- break;
769
- case DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD:
770
- // if (targetToClean.exists()) {
771
- params[0].targetFile.delete();
772
- // }
773
- break;
774
- default:
775
- break;
776
- }
777
- Log.e("react-native-update", "download task failed", e);
413
+ cleanUpAfterFailure(taskType);
778
414
 
779
- if (params[0].listener != null) {
780
- params[0].listener.onDownloadFailed(e);
415
+ if (params.listener != null) {
416
+ params.listener.onDownloadFailed(error);
781
417
  }
782
418
  }
783
- return null;
784
419
  }
785
420
 
421
+ private static native void applyPatchFromFileSource(
422
+ String sourceRoot,
423
+ String targetRoot,
424
+ String originBundlePath,
425
+ String bundlePatchPath,
426
+ String bundleOutputPath,
427
+ String mergeSourceSubdir,
428
+ boolean enableMerge,
429
+ String[] copyFroms,
430
+ String[] copyTos,
431
+ String[] deletes
432
+ );
433
+
434
+ private static native void cleanupOldEntries(
435
+ String rootDir,
436
+ String keepCurrent,
437
+ String keepPrevious,
438
+ int maxAgeDays
439
+ );
440
+
441
+ private static native ArchivePatchPlanResult buildArchivePatchPlan(
442
+ int patchType,
443
+ String[] entryNames,
444
+ String[] copyFroms,
445
+ String[] copyTos,
446
+ String[] deletes
447
+ );
448
+
449
+ private static native CopyGroupResult[] buildCopyGroups(
450
+ String[] copyFroms,
451
+ String[] copyTos
452
+ );
786
453
  }