Skip to content

Commit e901325

Browse files
sunnylqmclaude
andcommitted
fix(android): match copied resources by content (CRC32) for AAB installs
A from-package (PATCH_FROM_APK) hot update copies unchanged resources out of the on-device package using the path recorded in __diff.json `copies`. When the baseline uploaded to the server was an APK but the app is installed from an AAB (Play split APKs), res/ drawable paths are shortened on device, so the recorded path (e.g. res/drawable-xhdpi-v4/x.webp) does not exist verbatim and images (webp) silently fall through and go missing. Add a CRC32 content-match tier in BundledResourceCopier: build a crc32 -> entry index while scanning the base + split APKs, and when a `from` path is not found by exact/normalized path, locate the file by the content checksum supplied via the new manifest `copiesCrc` map. CRC32 is over the uncompressed content, so it is stable across APK/AAB packaging. This tier runs before resolveBundledResource (content match is more reliable than the resource-id heuristic). Also fix openResolvedResourceStream: use openRawResource(id, typedValue) with the density-resolved TypedValue instead of openRawResource(id), which ignored the requested density and could copy the wrong variant. Backward/forward compatible: manifests without `copiesCrc` (older CLI) simply skip the CRC tier and fall back to today's path-based behavior. Requires CLI support: reactnativecn/react-native-update-cli#54 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent edc40be commit e901325

2 files changed

Lines changed: 52 additions & 7 deletions

File tree

android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,32 @@ final class BundledResourceCopier {
2626
private static final class ResolvedResourceSource {
2727
final int resourceId;
2828
final String assetPath;
29+
final TypedValue typedValue;
2930

30-
ResolvedResourceSource(int resourceId, String assetPath) {
31+
ResolvedResourceSource(int resourceId, String assetPath, TypedValue typedValue) {
3132
this.resourceId = resourceId;
3233
this.assetPath = assetPath;
34+
this.typedValue = typedValue;
3335
}
3436
}
3537

3638
BundledResourceCopier(Context context) {
3739
this.context = context.getApplicationContext();
3840
}
3941

40-
void copyFromResource(HashMap<String, ArrayList<File>> resToCopy) throws IOException {
42+
void copyFromResource(
43+
HashMap<String, ArrayList<File>> resToCopy,
44+
HashMap<String, Long> crcByFrom
45+
) throws IOException {
4146
ArrayList<String> apkPaths = collectApkPaths();
4247
HashMap<String, ZipEntry> availableEntries = new HashMap<String, ZipEntry>();
4348
HashMap<String, SafeZipFile> zipFileMap = new HashMap<String, SafeZipFile>();
4449
HashMap<String, SafeZipFile> entryToZipFileMap = new HashMap<String, SafeZipFile>();
50+
// Content checksum index: CRC32 -> entry name. Lets us locate a file by
51+
// content when its origin path is not present verbatim on device (e.g.
52+
// APK baseline diff applied on an AAB/split-apk install whose res/
53+
// paths were shortened). First entry for a given crc wins.
54+
HashMap<Long, String> crcToEntryName = new HashMap<Long, String>();
4555

4656
try {
4757
for (String apkPath : apkPaths) {
@@ -55,6 +65,10 @@ void copyFromResource(HashMap<String, ArrayList<File>> resToCopy) throws IOExcep
5565
availableEntries.put(entryName, ze);
5666
entryToZipFileMap.put(entryName, zipFile);
5767
}
68+
long crc = ze.getCrc();
69+
if (crc != -1L && !crcToEntryName.containsKey(crc)) {
70+
crcToEntryName.put(crc, entryName);
71+
}
5872
}
5973
}
6074

@@ -87,6 +101,20 @@ void copyFromResource(HashMap<String, ArrayList<File>> resToCopy) throws IOExcep
87101
}
88102
}
89103

104+
// Content (CRC32) match: robust across APK/AAB packaging because
105+
// the checksum is over the uncompressed file content, not its
106+
// path. Preferred over the resource-id heuristic below.
107+
if (entry == null && crcByFrom != null) {
108+
Long wantedCrc = crcByFrom.get(fromPath);
109+
if (wantedCrc != null) {
110+
String matchedEntry = crcToEntryName.get(wantedCrc);
111+
if (matchedEntry != null) {
112+
entry = availableEntries.get(matchedEntry);
113+
actualSourcePath = matchedEntry;
114+
}
115+
}
116+
}
117+
90118
if (entry == null) {
91119
resolvedResource = resolveBundledResource(fromPath);
92120
if (resolvedResource != null) {
@@ -243,12 +271,15 @@ private ResolvedResourceSource resolveBundledResource(String resourcePath) {
243271
assetPath = assetPath.substring(1);
244272
}
245273

246-
return new ResolvedResourceSource(resourceId, assetPath);
274+
return new ResolvedResourceSource(resourceId, assetPath, typedValue);
247275
}
248276

249277
private InputStream openResolvedResourceStream(ResolvedResourceSource source) throws IOException {
250278
try {
251-
return context.getResources().openRawResource(source.resourceId);
279+
// Use the density-resolved TypedValue so we open the exact variant
280+
// that was requested, instead of openRawResource(id) which would
281+
// fall back to the device's current configuration density.
282+
return context.getResources().openRawResource(source.resourceId, source.typedValue);
252283
} catch (Resources.NotFoundException e) {
253284
throw new IOException("Unable to open resolved resource: " + source.assetPath, e);
254285
}

android/src/main/java/cn/reactnative/modules/update/DownloadTask.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ private static final class PatchArchiveContents {
4040
final ArrayList<String> copyFroms = new ArrayList<String>();
4141
final ArrayList<String> copyTos = new ArrayList<String>();
4242
final ArrayList<String> deletes = new ArrayList<String>();
43+
// Maps a copy source path ("from") to the CRC32 of the file content,
44+
// when provided by the manifest ("copiesCrc"). Lets the resource
45+
// copier locate the file by content if the path is not present on
46+
// device (APK baseline -> AAB install path shortening).
47+
final HashMap<String, Long> copyCrcs = new HashMap<String, Long>();
4348
}
4449

4550
private final Context context;
@@ -140,8 +145,11 @@ private void appendManifestEntries(
140145
JSONObject manifest,
141146
ArrayList<String> copyFroms,
142147
ArrayList<String> copyTos,
143-
ArrayList<String> deletes
148+
ArrayList<String> deletes,
149+
HashMap<String, Long> copyCrcs
144150
) throws JSONException {
151+
JSONObject copiesCrc = manifest.optJSONObject("copiesCrc");
152+
145153
JSONObject copies = manifest.optJSONObject("copies");
146154
if (copies != null) {
147155
Iterator<?> keys = copies.keys();
@@ -153,6 +161,11 @@ private void appendManifestEntries(
153161
}
154162
copyFroms.add(from);
155163
copyTos.add(to);
164+
if (copiesCrc != null && copyCrcs != null && copiesCrc.has(to)) {
165+
// Same content => same crc, so grouping multiple "to" under
166+
// one "from" stays consistent.
167+
copyCrcs.put(from, copiesCrc.getLong(to));
168+
}
156169
}
157170
}
158171

@@ -220,7 +233,8 @@ private PatchArchiveContents extractPatchArchive(File archiveFile, File unzipDir
220233
manifest,
221234
contents.copyFroms,
222235
contents.copyTos,
223-
contents.deletes
236+
contents.deletes,
237+
contents.copyCrcs
224238
);
225239
continue;
226240
}
@@ -285,7 +299,7 @@ private void doPatchFromApk() throws IOException, JSONException {
285299
originBundleFile.delete();
286300
}
287301

288-
bundledResourceCopier.copyFromResource(copyList);
302+
bundledResourceCopier.copyFromResource(copyList, contents.copyCrcs);
289303
}
290304

291305
private void doPatchFromPpk() throws IOException, JSONException {

0 commit comments

Comments
 (0)