feat: 新增外部 APK 覆蓋支援

將原始 APK 的處理邏輯 (計算 CRC、解壓、路徑分配) 抽離至獨立類別。
系統啟動時會優先檢查外部路徑  ` /sdcard/Android/data/[包名]/cache/npatch/origin/origin.apk `
若偵測到外部檔案,將自動使用該檔案覆蓋內部快取,方便用戶在不重新打包的情況下更換 原始 APK 。
自動將快取後的 APK 設為「唯讀」狀態,並強化了資源缺失時的報錯機制。

Co-Authored-By: hw1020 <43195286+hw1020@users.noreply.github.com>
This commit is contained in:
NkBe 2026-02-11 22:55:02 +08:00
parent 7844b99b3a
commit 7e5e2d6bfd
No known key found for this signature in database
GPG Key ID: 9FACEE0DB6DF678E
2 changed files with 83 additions and 24 deletions

View File

@ -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);

View File

@ -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();
}
}
}