Skip to content

Commit 767841c

Browse files
sunnylqmclaude
andauthored
fix(android): content-based (CRC32) resource matching for AAB installs + openRawResource density fix (#577)
* 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> * fix(android): address review — exact CRC archive + real density-correct copy Two follow-ups from review on the resource copier: 1. CRC index stored only the entry name, so resolving it back through availableEntries could yield a different-content entry when two APKs (base + split) expose the same name with different bytes. Store the matched ZipEntry together with its SafeZipFile (ZipSource) and copy from that archive directly. 2. openRawResource(id, typedValue) is a no-op for density: AOSP's ResourcesImpl re-runs getValue(id, value, true) and overwrites the passed TypedValue at the current configuration before opening the stream. Instead of going through openRawResource (or the hidden openNonAsset API), take the density-correct file path resolved by getValueForDensity and copy that exact entry from the already-open archives — public API, correct variant. openRawResource(id) remains only as a defensive fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent edc40be commit 767841c

2 files changed

Lines changed: 75 additions & 5 deletions

File tree

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,36 @@ private static final class ResolvedResourceSource {
3333
}
3434
}
3535

36+
// Holds the exact archive a CRC32 match came from, so the fallback copy
37+
// reads from that archive even if another APK exposes the same entry name
38+
// with different bytes.
39+
private static final class ZipSource {
40+
final ZipEntry entry;
41+
final SafeZipFile zipFile;
42+
43+
ZipSource(ZipEntry entry, SafeZipFile zipFile) {
44+
this.entry = entry;
45+
this.zipFile = zipFile;
46+
}
47+
}
48+
3649
BundledResourceCopier(Context context) {
3750
this.context = context.getApplicationContext();
3851
}
3952

40-
void copyFromResource(HashMap<String, ArrayList<File>> resToCopy) throws IOException {
53+
void copyFromResource(
54+
HashMap<String, ArrayList<File>> resToCopy,
55+
HashMap<String, Long> crcByFrom
56+
) throws IOException {
4157
ArrayList<String> apkPaths = collectApkPaths();
4258
HashMap<String, ZipEntry> availableEntries = new HashMap<String, ZipEntry>();
4359
HashMap<String, SafeZipFile> zipFileMap = new HashMap<String, SafeZipFile>();
4460
HashMap<String, SafeZipFile> entryToZipFileMap = new HashMap<String, SafeZipFile>();
61+
// Content checksum index: CRC32 -> matched archive source. Lets us
62+
// locate a file by content when its origin path is not present verbatim
63+
// on device (e.g. APK baseline diff applied on an AAB/split-apk install
64+
// whose res/ paths were shortened). First entry for a given crc wins.
65+
HashMap<Long, ZipSource> crcToEntry = new HashMap<Long, ZipSource>();
4566

4667
try {
4768
for (String apkPath : apkPaths) {
@@ -55,6 +76,10 @@ void copyFromResource(HashMap<String, ArrayList<File>> resToCopy) throws IOExcep
5576
availableEntries.put(entryName, ze);
5677
entryToZipFileMap.put(entryName, zipFile);
5778
}
79+
long crc = ze.getCrc();
80+
if (crc != -1L && !crcToEntry.containsKey(crc)) {
81+
crcToEntry.put(crc, new ZipSource(ze, zipFile));
82+
}
5883
}
5984
}
6085

@@ -76,6 +101,7 @@ void copyFromResource(HashMap<String, ArrayList<File>> resToCopy) throws IOExcep
76101

77102
ZipEntry entry = availableEntries.get(fromPath);
78103
String actualSourcePath = fromPath;
104+
SafeZipFile matchedZipFile = null;
79105
ResolvedResourceSource resolvedResource = null;
80106

81107
if (entry == null) {
@@ -87,10 +113,35 @@ void copyFromResource(HashMap<String, ArrayList<File>> resToCopy) throws IOExcep
87113
}
88114
}
89115

116+
// Content (CRC32) match: robust across APK/AAB packaging because
117+
// the checksum is over the uncompressed file content, not its
118+
// path. Preferred over the resource-id heuristic below.
119+
if (entry == null && crcByFrom != null) {
120+
Long wantedCrc = crcByFrom.get(fromPath);
121+
if (wantedCrc != null) {
122+
ZipSource matched = crcToEntry.get(wantedCrc);
123+
if (matched != null) {
124+
entry = matched.entry;
125+
matchedZipFile = matched.zipFile;
126+
actualSourcePath = matched.entry.getName();
127+
}
128+
}
129+
}
130+
90131
if (entry == null) {
91132
resolvedResource = resolveBundledResource(fromPath);
92133
if (resolvedResource != null) {
93134
actualSourcePath = resolvedResource.assetPath;
135+
// resolveBundledResource resolved the density-correct
136+
// file path; copy that exact entry from the already-open
137+
// archives so the right variant is used. (openRawResource
138+
// would re-resolve the id at the current configuration
139+
// density and ignore the requested one.)
140+
ZipEntry resolvedEntry = availableEntries.get(actualSourcePath);
141+
if (resolvedEntry != null) {
142+
entry = resolvedEntry;
143+
resolvedResource = null;
144+
}
94145
}
95146
}
96147

@@ -104,7 +155,9 @@ void copyFromResource(HashMap<String, ArrayList<File>> resToCopy) throws IOExcep
104155
if (lastTarget != null) {
105156
UpdateFileUtils.copyFile(lastTarget, target);
106157
} else if (entry != null) {
107-
SafeZipFile sourceZipFile = entryToZipFileMap.get(actualSourcePath);
158+
SafeZipFile sourceZipFile = matchedZipFile != null
159+
? matchedZipFile
160+
: entryToZipFileMap.get(actualSourcePath);
108161
if (sourceZipFile == null) {
109162
sourceZipFile = baseZipFile;
110163
}
@@ -247,6 +300,9 @@ private ResolvedResourceSource resolveBundledResource(String resourcePath) {
247300
}
248301

249302
private InputStream openResolvedResourceStream(ResolvedResourceSource source) throws IOException {
303+
// Defensive fallback only: reached when the density-resolved assetPath
304+
// is not present as a zip entry in any loaded APK. Best-effort, resolves
305+
// at the current configuration density.
250306
try {
251307
return context.getResources().openRawResource(source.resourceId);
252308
} catch (Resources.NotFoundException e) {

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)