From 7e5e2d6bfd39f591050ade8637028966f8a5774c Mon Sep 17 00:00:00 2001 From: NkBe Date: Wed, 11 Feb 2026 22:55:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=A4=96=E9=83=A8=20?= =?UTF-8?q?APK=20=E8=A6=86=E8=93=8B=E6=94=AF=E6=8F=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 將原始 APK 的處理邏輯 (計算 CRC、解壓、路徑分配) 抽離至獨立類別。 系統啟動時會優先檢查外部路徑 ` /sdcard/Android/data/[包名]/cache/npatch/origin/origin.apk ` 若偵測到外部檔案,將自動使用該檔案覆蓋內部快取,方便用戶在不重新打包的情況下更換 原始 APK 。 自動將快取後的 APK 設為「唯讀」狀態,並強化了資源缺失時的報錯機制。 Co-Authored-By: hw1020 <43195286+hw1020@users.noreply.github.com> --- .../lsposed/npatch/loader/LSPApplication.java | 29 ++----- .../npatch/loader/OriginApkHelper.java | 78 +++++++++++++++++++ 2 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 patch-loader/src/main/java/org/lsposed/npatch/loader/OriginApkHelper.java diff --git a/patch-loader/src/main/java/org/lsposed/npatch/loader/LSPApplication.java b/patch-loader/src/main/java/org/lsposed/npatch/loader/LSPApplication.java index 275b468..0eee448 100644 --- a/patch-loader/src/main/java/org/lsposed/npatch/loader/LSPApplication.java +++ b/patch-loader/src/main/java/org/lsposed/npatch/loader/LSPApplication.java @@ -1,7 +1,6 @@ package org.lsposed.npatch.loader; import static org.lsposed.npatch.share.Constants.CONFIG_ASSET_PATH; -import static org.lsposed.npatch.share.Constants.ORIGINAL_APK_ASSET_PATH; import static org.lsposed.npatch.share.Constants.PROVIDER_DEX_ASSET_PATH; import android.app.ActivityThread; @@ -12,6 +11,7 @@ import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; import android.os.Build; import android.os.RemoteException; +import android.os.Process; import android.system.Os; import android.util.Log; @@ -22,7 +22,6 @@ import org.json.JSONObject; import org.lsposed.lspd.core.Startup; import org.lsposed.lspd.models.Module; import org.lsposed.lspd.service.ILSPApplicationService; -import org.lsposed.npatch.loader.util.FileUtils; import org.lsposed.npatch.loader.util.XLog; import org.lsposed.npatch.service.IntegrApplicationService; import org.lsposed.npatch.service.NeoLocalApplicationService; @@ -40,13 +39,11 @@ import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; -import java.util.zip.ZipFile; import dalvik.system.DexFile; import de.robv.android.xposed.XposedBridge; @@ -73,7 +70,7 @@ public class LSPApplication { private static PatchConfig config; public static boolean isIsolated() { - return (android.os.Process.myUid() % PER_USER_RANGE) >= FIRST_APP_ZYGOTE_ISOLATED_UID; + return (Process.myUid() % PER_USER_RANGE) >= FIRST_APP_ZYGOTE_ISOLATED_UID; } private static boolean hasEmbeddedModules(Context context) { @@ -173,30 +170,16 @@ public class LSPApplication { Log.i(TAG, "Use manager: " + config.useManager); Log.i(TAG, "Signature bypass level: " + config.sigBypassLevel); - Path originPath = Paths.get(appInfo.dataDir, "cache/npatch/origin/"); - String originalSourceDir = appInfo.sourceDir; - - long sourceCrc; - try (ZipFile sourceFile = new ZipFile(originalSourceDir)) { - sourceCrc = sourceFile.getEntry(ORIGINAL_APK_ASSET_PATH).getCrc(); - } - Path cacheApkPath = originPath.resolve(sourceCrc + ".apk"); + Path cacheApkPath = OriginApkHelper.prepareOriginApk(appInfo, baseClassLoader); + long sourceCrc = OriginApkHelper.getOriginalApkCrc(appInfo.sourceDir); appInfo.sourceDir = cacheApkPath.toString(); appInfo.publicSourceDir = cacheApkPath.toString(); appInfo.appComponentFactory = config.appComponentFactory; - if (!Files.exists(cacheApkPath)) { - Log.i(TAG, "Extract original apk"); - FileUtils.deleteFolderIfExists(originPath); - Files.createDirectories(originPath); - try (InputStream is = baseClassLoader.getResourceAsStream(ORIGINAL_APK_ASSET_PATH)) { - if (is != null) Files.copy(is, cacheApkPath); - } - } Path providerPath = null; if (config.injectProvider) { - providerPath = originPath.resolve("p_" + sourceCrc + ".dex"); + providerPath = cacheApkPath.getParent().resolve("p_" + sourceCrc + ".dex"); try { Files.deleteIfExists(providerPath); try (InputStream is = baseClassLoader.getResourceAsStream(PROVIDER_DEX_ASSET_PATH)) { @@ -213,8 +196,6 @@ public class LSPApplication { } } - cacheApkPath.toFile().setWritable(false); - var mPackages = (Map) XposedHelpers.getObjectField(activityThread, "mPackages"); mPackages.remove(appInfo.packageName); appLoadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo); diff --git a/patch-loader/src/main/java/org/lsposed/npatch/loader/OriginApkHelper.java b/patch-loader/src/main/java/org/lsposed/npatch/loader/OriginApkHelper.java new file mode 100644 index 0000000..7452abe --- /dev/null +++ b/patch-loader/src/main/java/org/lsposed/npatch/loader/OriginApkHelper.java @@ -0,0 +1,78 @@ +package org.lsposed.npatch.loader; + +import static org.lsposed.npatch.share.Constants.ORIGINAL_APK_ASSET_PATH; + +import android.content.pm.ApplicationInfo; +import android.util.Log; + +import org.lsposed.npatch.loader.util.FileUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class OriginApkHelper { + + private static final String TAG = "NPatch-ApkHelper"; + private static final int PER_USER_RANGE = 100000; + + public static Path prepareOriginApk(ApplicationInfo appInfo, ClassLoader baseClassLoader) throws IOException { + Path internalOriginDir = Paths.get(appInfo.dataDir, "cache/npatch/origin/"); + long sourceCrc = getOriginalApkCrc(appInfo.sourceDir); + + Path internalCacheApk = internalOriginDir.resolve(sourceCrc + ".apk"); + + int userId = appInfo.uid / PER_USER_RANGE; + Path externalOriginPath = Paths.get("/storage/emulated/" + userId + "/Android/data/" + appInfo.packageName + "/cache/npatch/origin/origin.apk"); + + Log.d(TAG, "Checking external APK at: " + externalOriginPath); + + if (!Files.exists(internalOriginDir)) { + Files.createDirectories(internalOriginDir); + } + + boolean externalExists = Files.exists(externalOriginPath); + + if (externalExists) { + Log.i(TAG, "External origin.apk found! Overwriting internal cache."); + try (InputStream in = Files.newInputStream(externalOriginPath)) { + Files.copy(in, internalCacheApk, StandardCopyOption.REPLACE_EXISTING); + } + } else { + if (!Files.exists(internalCacheApk)) { + Log.i(TAG, "Extracting origin.apk from assets."); + FileUtils.deleteFolderIfExists(internalOriginDir); + Files.createDirectories(internalOriginDir); + + try (InputStream is = baseClassLoader.getResourceAsStream(ORIGINAL_APK_ASSET_PATH)) { + if (is == null) throw new IOException("Original APK not found in assets"); + Files.copy(is, internalCacheApk); + } + } else { + Log.d(TAG, "Internal cache hit: " + internalCacheApk); + } + } + + try { + internalCacheApk.toFile().setWritable(false); + } catch (Exception ignored) { + } + + return internalCacheApk; + } + + public static long getOriginalApkCrc(String sourceDir) throws IOException { + try (ZipFile sourceFile = new ZipFile(sourceDir)) { + ZipEntry entry = sourceFile.getEntry(ORIGINAL_APK_ASSET_PATH); + if (entry == null) { + return 0; + } + return entry.getCrc(); + } + } +}