diff --git a/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java b/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java index ddd53121..a155ce5a 100644 --- a/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +++ b/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java @@ -33,15 +33,36 @@ private static final class ResolvedResourceSource { } } + // Holds the exact archive a CRC32 match came from, so the fallback copy + // reads from that archive even if another APK exposes the same entry name + // with different bytes. + private static final class ZipSource { + final ZipEntry entry; + final SafeZipFile zipFile; + + ZipSource(ZipEntry entry, SafeZipFile zipFile) { + this.entry = entry; + this.zipFile = zipFile; + } + } + BundledResourceCopier(Context context) { this.context = context.getApplicationContext(); } - void copyFromResource(HashMap> resToCopy) throws IOException { + void copyFromResource( + HashMap> resToCopy, + HashMap crcByFrom + ) throws IOException { ArrayList apkPaths = collectApkPaths(); HashMap availableEntries = new HashMap(); HashMap zipFileMap = new HashMap(); HashMap entryToZipFileMap = new HashMap(); + // Content checksum index: CRC32 -> matched archive source. Lets us + // locate a file by content when its origin path is not present verbatim + // on device (e.g. APK baseline diff applied on an AAB/split-apk install + // whose res/ paths were shortened). First entry for a given crc wins. + HashMap crcToEntry = new HashMap(); try { for (String apkPath : apkPaths) { @@ -55,6 +76,10 @@ void copyFromResource(HashMap> resToCopy) throws IOExcep availableEntries.put(entryName, ze); entryToZipFileMap.put(entryName, zipFile); } + long crc = ze.getCrc(); + if (crc != -1L && !crcToEntry.containsKey(crc)) { + crcToEntry.put(crc, new ZipSource(ze, zipFile)); + } } } @@ -76,6 +101,7 @@ void copyFromResource(HashMap> resToCopy) throws IOExcep ZipEntry entry = availableEntries.get(fromPath); String actualSourcePath = fromPath; + SafeZipFile matchedZipFile = null; ResolvedResourceSource resolvedResource = null; if (entry == null) { @@ -87,10 +113,35 @@ void copyFromResource(HashMap> resToCopy) throws IOExcep } } + // Content (CRC32) match: robust across APK/AAB packaging because + // the checksum is over the uncompressed file content, not its + // path. Preferred over the resource-id heuristic below. + if (entry == null && crcByFrom != null) { + Long wantedCrc = crcByFrom.get(fromPath); + if (wantedCrc != null) { + ZipSource matched = crcToEntry.get(wantedCrc); + if (matched != null) { + entry = matched.entry; + matchedZipFile = matched.zipFile; + actualSourcePath = matched.entry.getName(); + } + } + } + if (entry == null) { resolvedResource = resolveBundledResource(fromPath); if (resolvedResource != null) { actualSourcePath = resolvedResource.assetPath; + // resolveBundledResource resolved the density-correct + // file path; copy that exact entry from the already-open + // archives so the right variant is used. (openRawResource + // would re-resolve the id at the current configuration + // density and ignore the requested one.) + ZipEntry resolvedEntry = availableEntries.get(actualSourcePath); + if (resolvedEntry != null) { + entry = resolvedEntry; + resolvedResource = null; + } } } @@ -104,7 +155,9 @@ void copyFromResource(HashMap> resToCopy) throws IOExcep if (lastTarget != null) { UpdateFileUtils.copyFile(lastTarget, target); } else if (entry != null) { - SafeZipFile sourceZipFile = entryToZipFileMap.get(actualSourcePath); + SafeZipFile sourceZipFile = matchedZipFile != null + ? matchedZipFile + : entryToZipFileMap.get(actualSourcePath); if (sourceZipFile == null) { sourceZipFile = baseZipFile; } @@ -247,6 +300,9 @@ private ResolvedResourceSource resolveBundledResource(String resourcePath) { } private InputStream openResolvedResourceStream(ResolvedResourceSource source) throws IOException { + // Defensive fallback only: reached when the density-resolved assetPath + // is not present as a zip entry in any loaded APK. Best-effort, resolves + // at the current configuration density. try { return context.getResources().openRawResource(source.resourceId); } catch (Resources.NotFoundException e) { diff --git a/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java b/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java index a41eee2b..96db31bf 100644 --- a/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +++ b/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java @@ -40,6 +40,11 @@ private static final class PatchArchiveContents { final ArrayList copyFroms = new ArrayList(); final ArrayList copyTos = new ArrayList(); final ArrayList deletes = new ArrayList(); + // Maps a copy source path ("from") to the CRC32 of the file content, + // when provided by the manifest ("copiesCrc"). Lets the resource + // copier locate the file by content if the path is not present on + // device (APK baseline -> AAB install path shortening). + final HashMap copyCrcs = new HashMap(); } private final Context context; @@ -140,8 +145,11 @@ private void appendManifestEntries( JSONObject manifest, ArrayList copyFroms, ArrayList copyTos, - ArrayList deletes + ArrayList deletes, + HashMap copyCrcs ) throws JSONException { + JSONObject copiesCrc = manifest.optJSONObject("copiesCrc"); + JSONObject copies = manifest.optJSONObject("copies"); if (copies != null) { Iterator keys = copies.keys(); @@ -153,6 +161,11 @@ private void appendManifestEntries( } copyFroms.add(from); copyTos.add(to); + if (copiesCrc != null && copyCrcs != null && copiesCrc.has(to)) { + // Same content => same crc, so grouping multiple "to" under + // one "from" stays consistent. + copyCrcs.put(from, copiesCrc.getLong(to)); + } } } @@ -220,7 +233,8 @@ private PatchArchiveContents extractPatchArchive(File archiveFile, File unzipDir manifest, contents.copyFroms, contents.copyTos, - contents.deletes + contents.deletes, + contents.copyCrcs ); continue; } @@ -285,7 +299,7 @@ private void doPatchFromApk() throws IOException, JSONException { originBundleFile.delete(); } - bundledResourceCopier.copyFromResource(copyList); + bundledResourceCopier.copyFromResource(copyList, contents.copyCrcs); } private void doPatchFromPpk() throws IOException, JSONException {