Skip to content

Commit ead5af4

Browse files
committed
fix resource detect
1 parent 3d56fa1 commit ead5af4

1 file changed

Lines changed: 150 additions & 94 deletions

File tree

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

Lines changed: 150 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
import android.content.Context;
44
import android.content.pm.ApplicationInfo;
55
import android.content.pm.PackageManager;
6+
import android.content.res.Resources;
67
import android.os.AsyncTask;
78
import android.os.Build;
9+
import android.util.DisplayMetrics;
810
import android.util.Log;
11+
import android.util.TypedValue;
912
import com.facebook.react.bridge.Arguments;
1013
import com.facebook.react.bridge.WritableMap;
1114

@@ -29,7 +32,6 @@
2932
import java.util.Iterator;
3033
import java.util.zip.ZipEntry;
3134
import java.util.HashMap;
32-
import java.util.regex.Matcher;
3335
import java.util.regex.Pattern;
3436

3537
import okio.BufferedSink;
@@ -197,8 +199,7 @@ private void appendManifestEntries(
197199
JSONObject manifest,
198200
ArrayList<String> copyFroms,
199201
ArrayList<String> copyTos,
200-
ArrayList<String> deletes,
201-
HashMap<String, String> copiesMap
202+
ArrayList<String> deletes
202203
) throws JSONException {
203204
JSONObject copies = manifest.optJSONObject("copies");
204205
if (copies != null) {
@@ -211,9 +212,6 @@ private void appendManifestEntries(
211212
}
212213
copyFroms.add(from);
213214
copyTos.add(to);
214-
if (copiesMap != null) {
215-
copiesMap.put(to, from);
216-
}
217215
}
218216
}
219217

@@ -228,13 +226,20 @@ private void appendManifestEntries(
228226

229227
private void copyBundledAssetToFile(String assetName, File destination) throws IOException {
230228
InputStream in = context.getAssets().open(assetName);
229+
copyInputStreamToFile(in, destination);
230+
}
231+
232+
private void copyInputStreamToFile(InputStream in, File destination) throws IOException {
231233
FileOutputStream fout = new FileOutputStream(destination);
232-
int count;
233-
while ((count = in.read(buffer)) != -1) {
234-
fout.write(buffer, 0, count);
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();
235242
}
236-
fout.close();
237-
in.close();
238243
}
239244

240245
private HashMap<String, ArrayList<File>> buildCopyList(
@@ -303,53 +308,127 @@ private String normalizeResPath(String path) {
303308
return VERSION_QUALIFIER_PATTERN.matcher(result).replaceAll("");
304309
}
305310

306-
private String findDrawableFallback(String originalToPath, HashMap<String, String> copiesMap, HashMap<String, ZipEntry> availableEntries, HashMap<String, String> normalizedEntryMap) {
307-
// 检查是否是 drawable 路径
308-
if (!originalToPath.contains("drawable")) {
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+
}
328+
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+
}
339+
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;
363+
}
364+
}
365+
return null;
366+
}
367+
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) {
309376
return null;
310377
}
311378

312-
// 提取文件名(路径的最后部分)
313-
int lastSlash = originalToPath.lastIndexOf('/');
314-
if (lastSlash == -1) {
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()) {
315384
return null;
316385
}
317-
String fileName = originalToPath.substring(lastSlash + 1);
318-
319-
// 定义密度优先级(从高到低)
320-
String[] densities = {"xxxhdpi", "xxhdpi", "xhdpi", "hdpi", "mdpi", "ldpi"};
321-
322-
// 尝试找到相同文件名但不同密度的 key
323-
for (String density : densities) {
324-
// 构建可能的 key 路径(替换密度部分)
325-
String fallbackToPath = originalToPath.replaceFirst("drawable-[^/]+", "drawable-" + density);
326-
327-
// 检查这个 key 是否在 copies 映射中
328-
if (copiesMap.containsKey(fallbackToPath)) {
329-
String fallbackFromPath = copiesMap.get(fallbackToPath);
330-
// 检查对应的 value 路径是否在 APK 中存在(精确匹配)
331-
if (availableEntries.containsKey(fallbackFromPath)) {
332-
if (UpdateContext.DEBUG) {
333-
Log.d("react-native-update", "Found fallback for " + originalToPath + ": " + fallbackToPath + " -> " + fallbackFromPath);
334-
}
335-
return fallbackFromPath;
336-
}
337-
// 尝试版本限定符无关匹配(APK ↔ AAB 兼容)
338-
String normalizedFallback = normalizeResPath(fallbackFromPath);
339-
String actualEntry = normalizedEntryMap.get(normalizedFallback);
340-
if (actualEntry != null) {
341-
if (UpdateContext.DEBUG) {
342-
Log.d("react-native-update", "Found normalized fallback for " + originalToPath + ": " + fallbackToPath + " -> " + actualEntry);
343-
}
344-
return actualEntry;
345-
}
386+
387+
Resources resources = context.getResources();
388+
int resourceId = resources.getIdentifier(resourceName, resourceType, context.getPackageName());
389+
if (resourceId == 0) {
390+
return null;
391+
}
392+
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());
346404
}
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);
347428
}
348-
349-
return null;
350429
}
351430

352-
private void copyFromResource(HashMap<String, ArrayList<File> > resToCopy, HashMap<String, String> copiesMap) throws IOException {
431+
private void copyFromResource(HashMap<String, ArrayList<File> > resToCopy) throws IOException {
353432
if (UpdateContext.DEBUG) {
354433
Log.d("react-native-update", "copyFromResource called, resToCopy size: " + resToCopy.size());
355434
}
@@ -421,6 +500,7 @@ private void copyFromResource(HashMap<String, ArrayList<File> > resToCopy, HashM
421500

422501
ZipEntry ze = availableEntries.get(fromPath);
423502
String actualSourcePath = fromPath;
503+
ResolvedResourceSource resolvedResource = null;
424504

425505
// 如果精确匹配找不到,尝试版本限定符无关匹配(APK ↔ AAB 兼容)
426506
// 例如 __diff.json 中的 "res/drawable-xxhdpi-v4/img.png" 匹配设备上的 "res/drawable-xxhdpi/img.png"
@@ -435,45 +515,17 @@ private void copyFromResource(HashMap<String, ArrayList<File> > resToCopy, HashM
435515
}
436516
}
437517
}
438-
439-
// 如果仍然找不到,尝试 drawable 密度降级 fallback
518+
519+
// release APK 可能会将资源 entry 名压缩为 res/9w.png 之类的短路径;
520+
// 这时通过 Resources 解析逻辑资源名,再直接读取资源内容。
440521
if (ze == null) {
441-
if (UpdateContext.DEBUG) {
442-
Log.d("react-native-update", "File not found in APK: " + fromPath + ", trying fallback");
443-
}
444-
// 找到对应的 to 路径(从 copiesMap 的反向查找)
445-
String toPath = null;
446-
for (String to : copiesMap.keySet()) {
447-
if (copiesMap.get(to).equals(fromPath)) {
448-
toPath = to;
449-
break;
450-
}
451-
}
452-
453-
if (toPath != null) {
454-
if (UpdateContext.DEBUG) {
455-
Log.d("react-native-update", "Found toPath: " + toPath + " for fromPath: " + fromPath);
456-
}
457-
String fallbackFromPath = findDrawableFallback(toPath, copiesMap, availableEntries, normalizedEntryMap);
458-
if (fallbackFromPath != null) {
459-
ze = availableEntries.get(fallbackFromPath);
460-
actualSourcePath = fallbackFromPath;
461-
if (UpdateContext.DEBUG) {
462-
Log.w("react-native-update", "Using fallback: " + fallbackFromPath + " for " + fromPath);
463-
}
464-
} else {
465-
if (UpdateContext.DEBUG) {
466-
Log.w("react-native-update", "No fallback found for: " + fromPath + " (toPath: " + toPath + ")");
467-
}
468-
}
469-
} else {
470-
if (UpdateContext.DEBUG) {
471-
Log.w("react-native-update", "No toPath found for fromPath: " + fromPath);
472-
}
522+
resolvedResource = resolveBundledResource(fromPath);
523+
if (resolvedResource != null) {
524+
actualSourcePath = resolvedResource.assetPath;
473525
}
474526
}
475527

476-
if (ze != null) {
528+
if (ze != null || resolvedResource != null) {
477529
File lastTarget = null;
478530
for (File target: targets) {
479531
if (UpdateContext.DEBUG) {
@@ -489,12 +541,17 @@ private void copyFromResource(HashMap<String, ArrayList<File> > resToCopy, HashM
489541
if (lastTarget != null) {
490542
copyFile(lastTarget, target);
491543
} else {
492-
// 从保存的映射中获取包含该条目的 ZipFile
493-
SafeZipFile sourceZipFile = entryToZipFileMap.get(actualSourcePath);
494-
if (sourceZipFile == null) {
495-
sourceZipFile = zipFile; // 回退到基础 APK
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);
496554
}
497-
sourceZipFile.unzipToFile(ze, target);
498555
lastTarget = target;
499556
}
500557
} catch (IOException e) {
@@ -526,7 +583,6 @@ private void doPatchFromApk(DownloadTaskParams param) throws IOException, JSONEx
526583

527584
removeDirectory(param.unzipDirectory);
528585
param.unzipDirectory.mkdirs();
529-
HashMap<String, String> copiesMap = new HashMap<String, String>(); // to -> from 映射
530586
ArrayList<String> entryNames = new ArrayList<String>();
531587
ArrayList<String> copyFroms = new ArrayList<String>();
532588
ArrayList<String> copyTos = new ArrayList<String>();
@@ -544,7 +600,7 @@ private void doPatchFromApk(DownloadTaskParams param) throws IOException, JSONEx
544600
byte[] bytes = readBytes(zipFile.getInputStream(ze));
545601
String json = new String(bytes, "UTF-8");
546602
JSONObject obj = (JSONObject)new JSONTokener(json).nextValue();
547-
appendManifestEntries(obj, copyFroms, copyTos, deletes, copiesMap);
603+
appendManifestEntries(obj, copyFroms, copyTos, deletes);
548604
continue;
549605
}
550606
zipFile.unzipToPath(ze, param.unzipDirectory);
@@ -587,13 +643,13 @@ private void doPatchFromApk(DownloadTaskParams param) throws IOException, JSONEx
587643
}
588644

589645
if (UpdateContext.DEBUG) {
590-
Log.d("react-native-update", "copyList size: " + copyList.size() + ", copiesMap size: " + copiesMap.size());
646+
Log.d("react-native-update", "copyList size: " + copyList.size());
591647
for (String from : copyList.keySet()) {
592648
Log.d("react-native-update", "copyList entry: " + from + " -> " + copyList.get(from).size() + " targets");
593649
}
594650
}
595651

596-
copyFromResource(copyList, copiesMap);
652+
copyFromResource(copyList);
597653

598654
if (UpdateContext.DEBUG) {
599655
Log.d("react-native-update", "Unzip finished");
@@ -625,7 +681,7 @@ private void doPatchFromPpk(DownloadTaskParams param) throws IOException, JSONEx
625681
byte[] bytes = readBytes(zipFile.getInputStream(ze));
626682
String json = new String(bytes, "UTF-8");
627683
JSONObject obj = (JSONObject)new JSONTokener(json).nextValue();
628-
appendManifestEntries(obj, copyFroms, copyTos, deletes, null);
684+
appendManifestEntries(obj, copyFroms, copyTos, deletes);
629685
continue;
630686
}
631687
zipFile.unzipToPath(ze, param.unzipDirectory);

0 commit comments

Comments
 (0)