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:
parent
7844b99b3a
commit
7e5e2d6bfd
|
|
@ -1,7 +1,6 @@
|
||||||
package org.lsposed.npatch.loader;
|
package org.lsposed.npatch.loader;
|
||||||
|
|
||||||
import static org.lsposed.npatch.share.Constants.CONFIG_ASSET_PATH;
|
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 static org.lsposed.npatch.share.Constants.PROVIDER_DEX_ASSET_PATH;
|
||||||
|
|
||||||
import android.app.ActivityThread;
|
import android.app.ActivityThread;
|
||||||
|
|
@ -12,6 +11,7 @@ import android.content.pm.ApplicationInfo;
|
||||||
import android.content.res.CompatibilityInfo;
|
import android.content.res.CompatibilityInfo;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
|
import android.os.Process;
|
||||||
import android.system.Os;
|
import android.system.Os;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
|
@ -22,7 +22,6 @@ import org.json.JSONObject;
|
||||||
import org.lsposed.lspd.core.Startup;
|
import org.lsposed.lspd.core.Startup;
|
||||||
import org.lsposed.lspd.models.Module;
|
import org.lsposed.lspd.models.Module;
|
||||||
import org.lsposed.lspd.service.ILSPApplicationService;
|
import org.lsposed.lspd.service.ILSPApplicationService;
|
||||||
import org.lsposed.npatch.loader.util.FileUtils;
|
|
||||||
import org.lsposed.npatch.loader.util.XLog;
|
import org.lsposed.npatch.loader.util.XLog;
|
||||||
import org.lsposed.npatch.service.IntegrApplicationService;
|
import org.lsposed.npatch.service.IntegrApplicationService;
|
||||||
import org.lsposed.npatch.service.NeoLocalApplicationService;
|
import org.lsposed.npatch.service.NeoLocalApplicationService;
|
||||||
|
|
@ -40,13 +39,11 @@ import java.lang.reflect.Field;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.zip.ZipFile;
|
|
||||||
|
|
||||||
import dalvik.system.DexFile;
|
import dalvik.system.DexFile;
|
||||||
import de.robv.android.xposed.XposedBridge;
|
import de.robv.android.xposed.XposedBridge;
|
||||||
|
|
@ -73,7 +70,7 @@ public class LSPApplication {
|
||||||
private static PatchConfig config;
|
private static PatchConfig config;
|
||||||
|
|
||||||
public static boolean isIsolated() {
|
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) {
|
private static boolean hasEmbeddedModules(Context context) {
|
||||||
|
|
@ -173,30 +170,16 @@ public class LSPApplication {
|
||||||
Log.i(TAG, "Use manager: " + config.useManager);
|
Log.i(TAG, "Use manager: " + config.useManager);
|
||||||
Log.i(TAG, "Signature bypass level: " + config.sigBypassLevel);
|
Log.i(TAG, "Signature bypass level: " + config.sigBypassLevel);
|
||||||
|
|
||||||
Path originPath = Paths.get(appInfo.dataDir, "cache/npatch/origin/");
|
Path cacheApkPath = OriginApkHelper.prepareOriginApk(appInfo, baseClassLoader);
|
||||||
String originalSourceDir = appInfo.sourceDir;
|
long sourceCrc = OriginApkHelper.getOriginalApkCrc(appInfo.sourceDir);
|
||||||
|
|
||||||
long sourceCrc;
|
|
||||||
try (ZipFile sourceFile = new ZipFile(originalSourceDir)) {
|
|
||||||
sourceCrc = sourceFile.getEntry(ORIGINAL_APK_ASSET_PATH).getCrc();
|
|
||||||
}
|
|
||||||
Path cacheApkPath = originPath.resolve(sourceCrc + ".apk");
|
|
||||||
|
|
||||||
appInfo.sourceDir = cacheApkPath.toString();
|
appInfo.sourceDir = cacheApkPath.toString();
|
||||||
appInfo.publicSourceDir = cacheApkPath.toString();
|
appInfo.publicSourceDir = cacheApkPath.toString();
|
||||||
appInfo.appComponentFactory = config.appComponentFactory;
|
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;
|
Path providerPath = null;
|
||||||
if (config.injectProvider) {
|
if (config.injectProvider) {
|
||||||
providerPath = originPath.resolve("p_" + sourceCrc + ".dex");
|
providerPath = cacheApkPath.getParent().resolve("p_" + sourceCrc + ".dex");
|
||||||
try {
|
try {
|
||||||
Files.deleteIfExists(providerPath);
|
Files.deleteIfExists(providerPath);
|
||||||
try (InputStream is = baseClassLoader.getResourceAsStream(PROVIDER_DEX_ASSET_PATH)) {
|
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");
|
var mPackages = (Map<?, ?>) XposedHelpers.getObjectField(activityThread, "mPackages");
|
||||||
mPackages.remove(appInfo.packageName);
|
mPackages.remove(appInfo.packageName);
|
||||||
appLoadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo);
|
appLoadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue