33import android .content .Context ;
44import android .content .pm .ApplicationInfo ;
55import android .content .pm .PackageManager ;
6+ import android .content .res .Resources ;
67import android .os .AsyncTask ;
78import android .os .Build ;
9+ import android .util .DisplayMetrics ;
810import android .util .Log ;
11+ import android .util .TypedValue ;
912import com .facebook .react .bridge .Arguments ;
1013import com .facebook .react .bridge .WritableMap ;
1114
2932import java .util .Iterator ;
3033import java .util .zip .ZipEntry ;
3134import java .util .HashMap ;
32- import java .util .regex .Matcher ;
3335import java .util .regex .Pattern ;
3436
3537import 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