Compare commits

..

10 Commits

Author SHA1 Message Date
NkBe 41872d8261
Simplify primary assignment in NPackageManager 2026-03-08 22:49:56 +08:00
NkBe e640da4f57
feat: 引入輕量級 MicroG 支援與 GMS 請求重導向機制
為 NPatch 引入了原生的 MicroG 整合支援,允許修補後的 Google 應用程式(例如 YouTube)在依賴 Google Play 服務的環境下,透過社群版 MicroG(如 ReVanced GmsCore)正常運作。

詳細改動範圍包含:

* ** UI Manager **
  - 於 `NewPatchScreen` 的修補選項清單中,新增「啟用 MicroG 支援」核取方塊與相容性說明。
  - 更新 `NewPatchViewModel`,透過 `Patcher.kt` 將使用者的 `useMicroG` 選擇狀態以 `--useMicroG` 指令列參數的形式,傳遞給底層修補引擎。

* ** Patcher **
  - 於 `NPatch.java` 中新增對 `--useMicroG` 參數的解析邏輯。
  - 擴充 `modifyManifestFile` 邏輯:當啟用 MicroG 支援時,自動讀取目標應用的原始簽名並轉碼為 Hex 格式。
  - 於 `AndroidManifest.xml` 中動態注入 `fake-signature` 的 `<meta-data>` 節點與 `android.permission.FAKE_PACKAGE_SIGNATURE` 權限,以滿足 MicroG 進行簽名欺騙(Signature Spoofing)時的驗證需求。

* ** Patch Loader **
  - 實作 `GmsRedirector.java` 類別,負責在應用程式執行期間動態攔截並重導向 IPC 通訊:
    1. 掛鉤 `Intent.setPackage`、`Intent.setComponent` 與建構子,將所有指向 `com.google.android.gms` 與 GSF 的意圖,強制導向至設備上已安裝的社群版 MicroG 套件(優先支援 `app.revanced.android.gms` 與 `org.microg.gms`)。
    2. 掛鉤 `ContentResolver`,替換帶有 GMS Authority 的 URI。針對真實 GMS 拋出的 `SecurityException`(憑證拒絕存取)實作了智慧攔截與重試機制。
    3. 掛鉤 `PackageManager.getPackageInfo` 實現動態簽名偽裝,使修補後的應用程式將 MicroG 視為官方正版 GMS。
  - 於 `LSPApplication.java` 的 `onLoad` 階段,依據 `PatchConfig` 設定檔動態喚醒並初始化 `GmsRedirector`。

* ** Share **
  - 更新 `PatchConfig.java` 以儲存與傳遞 `useMicroG` 布林值狀態。
  - 於 `Constants.java` 中定義 `REAL_GMS_PACKAGE_NAME` 常數,供各模組統一呼叫。

Co-Authored-By: MrZhongzq <108169409+MrZhongzq@users.noreply.github.com>
2026-03-08 21:53:41 +08:00
NkBe 01d344ed01
Clarify sigbypass lv3 string to 64-bit only 2026-03-08 21:53:41 +08:00
NkBe 46df4e85cf
Remove artificial +4200 offset from coreCommitCount 2026-03-06 23:28:54 +08:00
NkBe db64adde94
Use reflection for DexFile and XResources
Avoid direct references to dalvik.system.DexFile and android.content.res.XResources by using reflection and guarded calls. LSPApplication now constructs and injects the provider dex via reflective DexFile/DexPathList$Element creation inside a try/catch to handle deprecation/compat issues on newer Android versions and log failures. LSPLoader replaces the direct XResources.setPackageNameForResDir call with a reflective invocation (with warning logs) to prevent class resolution failures under strict boot classloader namespace delegation. Also added logging and minor import/constant adjustments to improve robustness and diagnostics.

Co-Authored-By: MrZhongzq <108169409+mrzhongzq@users.noreply.github.com>
Co-Authored-By: Claude <81847+claude@users.noreply.github.com>
2026-03-06 22:10:05 +08:00
NkBe 43ca1640ab
Fix .so file 16KB alignment in patched APKs:
- Change .so alignment from 4096 to 16384 bytes to comply with
     Android 15+ 16KB page alignment requirements.

Co-Authored-By: MrZhongzq <108169409+mrzhongzq@users.noreply.github.com>
Co-Authored-By: Claude <81847+claude@users.noreply.github.com>
2026-03-06 22:06:32 +08:00
NkBe cf6993baa0
Fix ConfigProvider crash: Make ConfigManager database initialization
lazy to prevent UninitializedPropertyAccessException when ContentProvider
   is queried before Application.onCreate() runs.
2026-03-06 21:31:19 +08:00
NkBe cf50194b29
chore: update core submodule URL to HSSkyBoy repository 2026-03-06 21:31:19 +08:00
NkBe 3c8964bc83
Enable resource shrinking; bump CMake min version 2026-02-23 18:02:04 +08:00
NkBe df7bf3eb58
修復模組加載備援機制
新增本地快取讀取:當系統無法透過 Content Provider 獲取管理器模組清單時,會自動切換至 loadModulesFromCache 模式,從本地的 SharedPreferences 讀取先前儲存的模組資訊。

支援路徑加載:快取機制支援直接透過檔案路徑(APK Path)加載模組,即使該 App 已被卸載或路徑變更,只要檔案存在即可嘗試加載。
2026-02-11 23:26:41 +08:00
21 changed files with 424 additions and 59 deletions

2
.gitmodules vendored
View File

@ -4,5 +4,5 @@
branch = android10-release
[submodule "core"]
path = core
url = https://github.com/HSSkyBoy/LSPosed.git
url = https://github.com/HSSkyBoy/LSPosed-JingMatrix.git
branch = master

View File

@ -37,13 +37,13 @@ val (coreCommitCount, coreLatestTag) = FileRepositoryBuilder().setGitDir(rootPro
val coreCommitCount =
git.log()
.add(repo.refDatabase.exactRef("HEAD").objectId)
.call().count() + 4200
.call().count()
val ver = git.describe()
.setTags(true)
.setAbbrev(0).call().removePrefix("v")
coreCommitCount to ver
}
}.getOrNull() ?: (1 to "1.0")
}.getOrNull() ?: (1145 to "1.0")
// sync from https://github.com/JingMartix/LSPosed/blob/master/build.gradle.kts
val defaultManagerPackageName by extra("org.lsposed.npatch")

2
core

@ -1 +1 @@
Subproject commit 83180d20de3ee55e8901cb1e74bf9895b793c629
Subproject commit 62ea99fa35dba5454b93db6eab8aced25c157383

View File

@ -27,7 +27,9 @@ android {
buildTypes {
release {
isMinifyEnabled = true
isMinifyEnabled = true // 启用 R8/ProGuard 进行代码压缩、优化和混淆。
isShrinkResources = true // 启用资源缩减,移除未被引用的资源文件。
isDebuggable = false // 发布版本禁止调试。
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"

View File

@ -201,9 +201,7 @@ object NPackageManager {
splits.add(dst.absolutePath)
return@mapNotNull null
}
if (primary == null) {
primary = appInfo
}
if (primary == null) primary = appInfo
val label = lspApp.packageManager.getApplicationLabel(appInfo).toString()
AppInfo(appInfo, label)
}

View File

@ -36,6 +36,7 @@ object Patcher {
}
if (config.injectProvider) add("--provider")
if(injectDex) add("--injectdex")
if (config.useMicroG) add("--useMicroG")
if (!MyKeyStore.useDefault) {
addAll(arrayOf("-k", MyKeyStore.file.path, Configs.keyStorePassword, Configs.keyStoreAlias, Configs.keyStoreAliasPassword))
}

View File

@ -20,12 +20,15 @@ object ConfigManager {
@OptIn(ExperimentalCoroutinesApi::class)
private val dispatcher = Dispatchers.Default.limitedParallelism(1)
private val db: LSPDatabase = Room.databaseBuilder(
lspApp, LSPDatabase::class.java, "modules_config.db"
).build()
private val db: LSPDatabase by lazy {
Room.databaseBuilder(
lspApp, LSPDatabase::class.java, "modules_config.db"
).build()
}
private val moduleDao = db.moduleDao()
private val scopeDao = db.scopeDao()
private val moduleDao get() = db.moduleDao()
private val scopeDao get() = db.scopeDao()
private val loadedModules = mutableMapOf<Module, org.lsposed.lspd.models.Module>()

View File

@ -365,6 +365,13 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) {
title = stringResource(R.string.patch_inject_mt_provider),
desc = stringResource(R.string.patch_inject_mt_provider_desc)
)
SettingsCheckBox(
modifier = Modifier.clickable { viewModel.useMicroG = !viewModel.useMicroG },
checked = viewModel.useMicroG,
icon = Icons.Outlined.CloudSync,
title = stringResource(R.string.patch_use_microg),
desc = stringResource(R.string.patch_use_microg_desc)
)
SettingsCheckBox(
modifier = Modifier.clickable { viewModel.outputLog = !viewModel.outputLog },
checked = viewModel.outputLog,

View File

@ -47,6 +47,7 @@ class NewPatchViewModel : ViewModel() {
var injectDex by mutableStateOf(false)
var injectProvider by mutableStateOf(false)
var outputLog by mutableStateOf(true)
var useMicroG by mutableStateOf(false)
var embeddedModules = emptyList<AppInfo>()
lateinit var patchApp: AppInfo
@ -99,7 +100,7 @@ class NewPatchViewModel : ViewModel() {
private fun submitPatch() {
Log.d(TAG, "Submit Patch")
if (useManager) embeddedModules = emptyList()
val config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null, injectProvider, outputLog, newPackageName)
val config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null, injectProvider, outputLog, newPackageName, useMicroG)
patchOptions = Patcher.Options(
newPackageName = newPackageName,
injectDex = injectDex,

View File

@ -64,7 +64,7 @@
<string name="patch_sigbypasslv0">lv0: 关闭</string>
<string name="patch_sigbypasslv1">lv1: 绕过 PM</string>
<string name="patch_sigbypasslv2">lv2: 绕过 PM + openat (libc)</string>
<string name="patch_sigbypasslv3">lv3: 绕过 PM + openat (libc) + SVC (仅 arm64) (测试版)</string>
<string name="patch_sigbypasslv3">lv3: 绕过 PM + openat (libc) + SVC (仅 arm64)</string>
<string name="patch_override_version_code">覆写版本号</string>
<string name="patch_override_version_code_desc">将修补的 App 版本号重写为 1\n这将允许后续降级安装并且通常来说这不会影响应用实际感知到的版本号</string>
<string name="patch_new_package">修补新包名</string>
@ -73,7 +73,8 @@
<string name="patch_inject_mt_provider_desc">注入文件提供器以在没有 Root 权限的情况下管理 data 目录的文件 (来自 MT 管理器)</string>
<string name="patch_inject_dex">注入加载器 Dex</string>
<string name="patch_inject_dex_desc">对那些需要孤立服务进程的应用程序,譬如说浏览器的渲染引擎,请勾选此选项以确保他们正常运行</string>
<string name="patch_output_log_to_media">日志输出到 Media 目录</string>
<string name="patch_use_microg">强制启用 MicroG 支持</string>
<string name="patch_use_microg_desc">重新导向 GMS 请求至社区版 MicroG。适用于 YouTube 等 Google 应用程序,需自行安装对应的 MicroG 服务。</string> <string name="patch_output_log_to_media">日志输出到 Media 目录</string>
<string name="patch_output_log_to_media_desc">将模块的 Xposed 日志输出到目标应用的 Media 目录</string>
<string name="patch_start">开始修补</string>
<string name="patch_return">返回</string>

View File

@ -64,7 +64,7 @@
<string name="patch_sigbypasslv0">lv0: 關閉</string>
<string name="patch_sigbypasslv1">lv1: 繞過 PM</string>
<string name="patch_sigbypasslv2">lv2: 繞過 PM + openat (libc)</string>
<string name="patch_sigbypasslv3">lv3: 繞過 PM + openat (libc) + SVC (僅 arm64) (測試用)</string>
<string name="patch_sigbypasslv3">lv3: 繞過 PM + openat (libc) + SVC (僅 64 位)</string>
<string name="patch_override_version_code">覆蓋版本編號</string>
<string name="patch_override_version_code_desc">將打包應用程式的版本編號改成 1\n允許以後降級安裝一般來說這不會影響應用程式實際感知的版本編號。</string>
<string name="patch_new_package">修補新套件名</string>
@ -73,6 +73,8 @@
<string name="patch_inject_mt_provider_desc">注入檔案选取器以在沒有 Root 權限的情況下管理 data 目錄的檔案(來自 MT 管理器)</string>
<string name="patch_inject_dex">注入載入器 Dex</string>
<string name="patch_inject_dex_desc">對那些需要孤立服務程序的應用程式,譬如說瀏覽器的渲染引擎,請勾選此選項以確保他們正常執行</string>
<string name="patch_use_microg">強制啟用 MicroG 支援</string>
<string name="patch_use_microg_desc">重新導向 GMS 請求至社群版 MicroG。適用於 YouTube 等 Google 應用程式,需自行安裝對應的 MicroG 服務。</string>
<string name="patch_output_log_to_media">日誌輸出到 Media 目錄</string>
<string name="patch_output_log_to_media_desc">將模組的 Xposed 日誌輸出到目標應用的 Media 目錄</string>
<string name="patch_start">開始打包</string>

View File

@ -66,7 +66,7 @@
<string name="patch_sigbypasslv0">lv0: Off</string>
<string name="patch_sigbypasslv1">lv1: Bypass PM</string>
<string name="patch_sigbypasslv2">lv2: Bypass PM + openat (libc)</string>
<string name="patch_sigbypasslv3">lv3: Bypass PM + openat(libc) + SVC (v8a only) (testing)</string>
<string name="patch_sigbypasslv3">lv3: Bypass PM + openat(libc) + SVC (64-bit only)</string>
<string name="patch_new_package">Patch New PackageName</string>
<string name="hint_patch_new_package">Input a new package for app</string>
<string name="patch_override_version_code">Override version code</string>
@ -75,6 +75,8 @@
<string name="patch_inject_mt_provider_desc">Inject file providers to manage files in the data directory without root privileges (From MT Manager)</string>
<string name="patch_inject_dex">Inject loader dex</string>
<string name="patch_inject_dex_desc">For applications with isolated services, such as the render engines of browsers, please turn on this option to ensure that they work properly.</string>
<string name="patch_use_microg">Force enable MicroG support</string>
<string name="patch_use_microg_desc">Redirect GMS requests to the community version of MicroG (such as ReVanced GmsCore). Applicable to Google apps like YouTube, requires manually installing the corresponding MicroG service.</string>
<string name="patch_output_log_to_media">Output Log to Media Directory</string>
<string name="patch_output_log_to_media_desc">Output the Xposed log to the target application Media directory.</string>
<string name="patch_start">Start Patch</string>

View File

@ -23,6 +23,7 @@ android {
externalNativeBuild {
cmake {
path("src/main/jni/CMakeLists.txt")
version = "3.31.6"
}
}
namespace = "org.lsposed.npatch.loader"

View File

@ -0,0 +1,256 @@
package org.lsposed.npatch.loader;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.net.Uri;
import android.util.Log;
import org.lsposed.npatch.share.Constants;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
public class GmsRedirector {
private static final String TAG = "NPatch-GmsRedirect";
private static final String REAL_GMS = Constants.REAL_GMS_PACKAGE_NAME;
// 鎖定社群主流的 MicroG 套件名稱
private static final String[] MICROG_PACKAGES = {
"app.revanced.android.gms", // ReVanced GmsCore (推薦)
"org.microg.gms", // Original MicroG
};
private static String targetGms = null;
private static String originalSignature;
public static void activate(Context context, String origSig) {
originalSignature = origSig;
targetGms = findInstalledMicroG(context);
if (targetGms == null) {
Log.w(TAG, "No MicroG/GmsCore found! GMS redirect disabled.");
return;
}
Log.i(TAG, "Activating GMS redirect: " + REAL_GMS + " -> " + targetGms);
hookIntentSetPackage();
hookIntentSetComponent();
hookIntentResolve();
hookContentResolverAcquire();
hookPackageManagerGetPackageInfo(context);
Log.i(TAG, "GMS redirect hooks installed");
}
private static String findInstalledMicroG(Context context) {
PackageManager pm = context.getPackageManager();
for (String pkg : MICROG_PACKAGES) {
try {
pm.getPackageInfo(pkg, 0);
return pkg;
} catch (PackageManager.NameNotFoundException ignored) {}
}
return null;
}
private static String redirectPackage(String pkg) {
if (REAL_GMS.equals(pkg) || "com.google.android.gsf".equals(pkg)) {
return targetGms;
}
return null;
}
private static String redirectAuthority(String authority) {
if (authority == null) return null;
if (authority.startsWith(REAL_GMS + ".")) {
return targetGms + authority.substring(REAL_GMS.length());
}
if (authority.equals(REAL_GMS)) {
return targetGms;
}
if (authority.startsWith("com.google.android.gsf")) {
return authority.replace("com.google.android.gsf", targetGms);
}
return null;
}
private static void hookIntentSetPackage() {
try {
XposedBridge.hookAllMethods(Intent.class, "setPackage", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
String pkg = (String) param.args[0];
String redirected = redirectPackage(pkg);
if (redirected != null) param.args[0] = redirected;
}
});
} catch (Throwable t) {
Log.e(TAG, "Failed to hook Intent.setPackage", t);
}
}
private static void hookIntentSetComponent() {
try {
XposedBridge.hookAllMethods(Intent.class, "setComponent", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
ComponentName cn = (ComponentName) param.args[0];
if (cn != null) {
String redirected = redirectPackage(cn.getPackageName());
if (redirected != null) {
param.args[0] = new ComponentName(redirected, cn.getClassName());
}
}
}
});
} catch (Throwable t) {
Log.e(TAG, "Failed to hook Intent.setComponent", t);
}
}
private static void hookIntentResolve() {
try {
XposedBridge.hookAllConstructors(Intent.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Intent intent = (Intent) param.thisObject;
ComponentName cn = intent.getComponent();
if (cn != null) {
String redirected = redirectPackage(cn.getPackageName());
if (redirected != null) {
intent.setComponent(new ComponentName(redirected, cn.getClassName()));
}
}
String pkg = intent.getPackage();
if (pkg != null) {
String redirected = redirectPackage(pkg);
if (redirected != null) {
intent.setPackage(redirected);
}
}
}
});
} catch (Throwable t) {
Log.e(TAG, "Failed to hook Intent constructors", t);
}
}
private static void hookContentResolverAcquire() {
try {
for (String method : new String[]{
"acquireProvider", "acquireContentProviderClient",
"acquireUnstableProvider", "acquireUnstableContentProviderClient"
}) {
try {
XposedBridge.hookAllMethods(ContentResolver.class, method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (param.args[0] instanceof Uri) {
Uri uri = (Uri) param.args[0];
String newAuth = redirectAuthority(uri.getAuthority());
if (newAuth != null) {
param.args[0] = uri.buildUpon().authority(newAuth).build();
}
} else if (param.args[0] instanceof String) {
String newAuth = redirectAuthority((String) param.args[0]);
if (newAuth != null) {
param.args[0] = newAuth;
}
}
}
});
} catch (Throwable ignored) {}
}
// 攔截 ContentResolver.call遇到 SecurityException 則自動重試
try {
XposedBridge.hookAllMethods(ContentResolver.class, "call", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
for (int i = 0; i < param.args.length; i++) {
if (param.args[i] instanceof Uri) {
Uri uri = (Uri) param.args[i];
String newAuth = redirectAuthority(uri.getAuthority());
if (newAuth != null) {
param.args[i] = uri.buildUpon().authority(newAuth).build();
}
} else if (param.args[i] instanceof String && i == 0) {
String newAuth = redirectAuthority((String) param.args[i]);
if (newAuth != null) {
param.args[i] = newAuth;
}
}
}
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (param.getThrowable() instanceof SecurityException) {
String msg = param.getThrowable().getMessage();
if (msg != null && (msg.contains("GoogleCertificatesRslt") ||
msg.contains("not allowed") ||
msg.contains("Access denied"))) {
Log.i(TAG, "GMS rejected call, retrying with MicroG");
for (int i = 0; i < param.args.length; i++) {
if (param.args[i] instanceof Uri) {
Uri uri = (Uri) param.args[i];
String authority = uri.getAuthority();
if (authority != null && authority.contains(REAL_GMS)) {
param.args[i] = uri.buildUpon()
.authority(authority.replace(REAL_GMS, targetGms))
.build();
}
} else if (param.args[i] instanceof String && i == 0) {
String s = (String) param.args[i];
if (s.contains(REAL_GMS)) {
param.args[i] = s.replace(REAL_GMS, targetGms);
}
}
}
param.setThrowable(null);
param.setResult(null);
}
}
}
});
} catch (Throwable ignored) {}
} catch (Throwable t) {
Log.e(TAG, "Failed to hook ContentResolver", t);
}
}
private static void hookPackageManagerGetPackageInfo(Context context) {
try {
XposedHelpers.findAndHookMethod(
context.getPackageManager().getClass(),
"getPackageInfo",
String.class, int.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
PackageInfo pi = (PackageInfo) param.getResult();
if (pi != null && targetGms != null) {
if (targetGms.equals(pi.packageName) && (((int) param.args[1]) & PackageManager.GET_SIGNATURES) != 0) {
if (originalSignature != null && !originalSignature.isEmpty()) {
try {
byte[] sigBytes = android.util.Base64.decode(originalSignature, android.util.Base64.DEFAULT);
pi.signatures = new Signature[]{new Signature(sigBytes)};
} catch (Throwable ignored) {}
}
}
}
}
}
);
} catch (Throwable t) {
Log.e(TAG, "Failed to hook PackageManager.getPackageInfo", t);
}
}
}

View File

@ -45,7 +45,6 @@ import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import dalvik.system.DexFile;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import hidden.HiddenApiBridge;
@ -146,6 +145,11 @@ public class LSPApplication {
switchAllClassLoader();
SigBypass.doSigBypass(context, config.sigBypassLevel);
if (config.useMicroG) {
Log.i(TAG, "Activating MicroG redirect via NPatch");
GmsRedirector.activate(context, config.originalSignature);
}
Log.i(TAG, "NPatch bootstrap completed");
}
@ -201,19 +205,24 @@ public class LSPApplication {
appLoadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo);
if (config.injectProvider && providerPath != null) {
ClassLoader loader = appLoadedApk.getClassLoader();
Object dexPathList = XposedHelpers.getObjectField(loader, "pathList");
Object dexElements = XposedHelpers.getObjectField(dexPathList, "dexElements");
int length = Array.getLength(dexElements);
Object newElements = Array.newInstance(dexElements.getClass().getComponentType(), length + 1);
System.arraycopy(dexElements, 0, newElements, 0, length);
try {
ClassLoader loader = appLoadedApk.getClassLoader();
Object dexPathList = XposedHelpers.getObjectField(loader, "pathList");
Object dexElements = XposedHelpers.getObjectField(dexPathList, "dexElements");
int length = Array.getLength(dexElements);
Object newElements = Array.newInstance(dexElements.getClass().getComponentType(), length + 1);
System.arraycopy(dexElements, 0, newElements, 0, length);
DexFile dexFile = new DexFile(providerPath.toString());
Object element = XposedHelpers.newInstance(XposedHelpers.findClass("dalvik.system.DexPathList$Element", loader), new Class[]{
DexFile.class
}, dexFile);
Array.set(newElements, length, element);
XposedHelpers.setObjectField(dexPathList, "dexElements", newElements);
// Use reflection for DexFile to handle deprecation on Android 14+
Class<?> dexFileClass = Class.forName("dalvik.system.DexFile");
Object dexFile = dexFileClass.getConstructor(String.class).newInstance(providerPath.toString());
Class<?> elementClass = Class.forName("dalvik.system.DexPathList$Element");
Object element = elementClass.getConstructor(dexFileClass).newInstance(dexFile);
Array.set(newElements, length, element);
XposedHelpers.setObjectField(dexPathList, "dexElements", newElements);
} catch (Throwable e) {
Log.e(TAG, "Failed to inject provider dex: " + e.getMessage(), e);
}
}
XposedHelpers.setObjectField(mBoundApplication, "info", appLoadedApk);

View File

@ -2,16 +2,20 @@ package org.lsposed.npatch.loader;
import android.app.ActivityThread;
import android.app.LoadedApk;
import android.content.res.XResources;
import android.util.Log;
import java.lang.reflect.Method;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedInit;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
public class LSPLoader {
private static final String TAG = "NPatch";
public static void initModules(LoadedApk loadedApk) {
XposedInit.loadedPackagesInProcess.add(loadedApk.getPackageName());
XResources.setPackageNameForResDir(loadedApk.getPackageName(), loadedApk.getResDir());
setPackageNameForResDir(loadedApk.getPackageName(), loadedApk.getResDir());
XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(
XposedBridge.sLoadedPackageCallbacks);
lpparam.packageName = loadedApk.getPackageName();
@ -21,4 +25,18 @@ public class LSPLoader {
lpparam.isFirstApplication = true;
XC_LoadPackage.callAll(lpparam);
}
}
private static void setPackageNameForResDir(String packageName, String resDir) {
try {
// Use reflection to avoid direct type reference to android.content.res.XResources
// which fails class resolution on Android 16+ due to strict boot classloader
// namespace delegation for the android.content.res.* package.
ClassLoader cl = LSPLoader.class.getClassLoader();
Class<?> xResourcesClass = cl.loadClass("android.content.res.XResources");
Method setMethod = xResourcesClass.getMethod("setPackageNameForResDir", String.class, String.class);
setMethod.invoke(null, packageName, resDir);
} catch (Throwable e) {
Log.w(TAG, "XResources.setPackageNameForResDir not available, skipping resource dir setup", e);
}
}
}

View File

@ -1,6 +1,7 @@
package org.lsposed.npatch.service;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
@ -10,6 +11,8 @@ import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import org.lsposed.npatch.util.ModuleLoader;
import org.lsposed.lspd.models.Module;
import org.lsposed.lspd.service.ILSPApplicationService;
@ -29,6 +32,49 @@ public class NeoLocalApplicationService extends ILSPApplicationService.Stub {
public NeoLocalApplicationService(Context context) {
cachedModule = Collections.synchronizedList(new ArrayList<>());
loadModulesFromProvider(context);
if (cachedModule.isEmpty()) {
Log.w(TAG, "NeoLocal: Provider returned empty, falling back to local cache.");
loadModulesFromCache(context);
}
}
private void loadModulesFromCache(Context context) {
try {
SharedPreferences shared = context.getSharedPreferences("npatch", Context.MODE_PRIVATE);
String jsonStr = shared.getString("modules", "[]");
JSONArray jsonArray = new JSONArray(jsonStr);
PackageManager pm = context.getPackageManager();
Log.i(TAG, "NeoLocal: Loading from cache: " + jsonStr);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject obj = jsonArray.getJSONObject(i);
String packageName = obj.optString("packageName");
String path = obj.optString("path");
if (path != null && !path.isEmpty() && new File(path).exists()) {
loadModuleByPath(packageName, path);
} else if (packageName != null) {
loadSingleModule(pm, packageName);
}
}
} catch (Exception e) {
Log.e(TAG, "NeoLocal: Failed to load from cache", e);
}
}
private void loadModuleByPath(String pkgName, String path) {
try {
Module m = new Module();
m.packageName = pkgName;
m.apkPath = path;
m.file = ModuleLoader.loadModule(m.apkPath);
cachedModule.add(m);
Log.i(TAG, "Loaded cached module " + pkgName);
} catch (Throwable e) {
Log.e(TAG, "Failed to load cached module " + pkgName, e);
}
}
private void loadModulesFromProvider(Context context) {

View File

@ -1,5 +1,5 @@
project(npatch)
cmake_minimum_required(VERSION 3.28)
cmake_minimum_required(VERSION 3.31.1)
set(CMAKE_CXX_STANDARD 23)
add_subdirectory(${CORE_ROOT} core)

View File

@ -89,6 +89,12 @@ public class NPatch {
@Parameter(names = {"--provider"}, description = "Inject Provider to manager data files")
private boolean isInjectProvider = false;
@Parameter(names = {"--installerSource"}, description = "Original app installer source")
private String installerSource = "";
@Parameter(names = {"--useMicroG"}, description = "Redirect GMS calls to community MicroG")
private boolean useMicroG = false;
@Parameter(names = {"--outputLog"}, description = "Output Log to Media")
private boolean outputLog = true;
@ -118,7 +124,7 @@ public class NPatch {
private static final ZFileOptions Z_FILE_OPTIONS = new ZFileOptions()
.setNoTimestamps(true)
.setAlignmentRule(AlignmentRules.compose(
AlignmentRules.constantForSuffix(".so", 4096),
AlignmentRules.constantForSuffix(".so", 16384),
AlignmentRules.constantForSuffix(ORIGINAL_APK_ASSET_PATH, 4096),
AlignmentRules.constantForSuffix(".arsc", 4)
));
@ -287,10 +293,10 @@ public class NPatch {
logger.i("Patching apk...");
// modify manifest
final var config = new PatchConfig(useManager, debuggableFlag, overrideVersionCode, sigbypassLevel, originalSignature, appComponentFactory, isInjectProvider, outputLog, newPackage);
final var config = new PatchConfig(useManager, debuggableFlag, overrideVersionCode, sigbypassLevel, originalSignature, appComponentFactory, isInjectProvider, outputLog, newPackage, useMicroG);
final var configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8);
final var metadata = Base64.getEncoder().encodeToString(configBytes);
try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, minSdkVersion, pair.packageName, newPackage))) {
try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, minSdkVersion, pair.packageName, newPackage, originalSignature))) {
dstZFile.add(ANDROID_MANIFEST_XML, is);
} catch (Throwable e) {
throw new PatchError("Error when modifying manifest", e);
@ -422,44 +428,52 @@ public class NPatch {
}
}
private byte[] modifyManifestFile(InputStream is, String metadata, int minSdkVersion, String originPackage, String newPackage) throws IOException {
private byte[] modifyManifestFile(InputStream is, String metadata, int minSdkVersion, String originPackage, String newPackage, String originalSignature) throws IOException {
ModificationProperty property = new ModificationProperty();
String targetPackage = (newPackage != null && !newPackage.isEmpty()) ? newPackage : originPackage;
if (overrideVersionCode)
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, 1));
if (minSdkVersion < 28)
if (minSdkVersion > 0)
property.addUsesSdkAttribute(new AttributeItem(NodeValue.UsesSDK.MIN_SDK_VERSION, minSdkVersion));
else
property.addUsesSdkAttribute(new AttributeItem(NodeValue.UsesSDK.MIN_SDK_VERSION, 27));
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggableFlag));
property.addApplicationAttribute(new AttributeItem("appComponentFactory", PROXY_APP_COMPONENT_FACTORY));
property.addApplicationAttribute(new AttributeItem("isSplitRequired", false));
if (!targetPackage.equals(originPackage)) {
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.PACKAGE, targetPackage).setNamespace(null));
}
property.setPermissionMapper((type, permission) -> {
if (permission.startsWith(originPackage)) {
return permission.replaceFirst(originPackage, targetPackage);
}
if (permission.startsWith("android")
|| permission.startsWith("com.android")
|| permission.startsWith("com.google.android")) {
return permission;
}
return targetPackage + "_" + permission;
});
property.setAuthorityMapper(value -> {
if (value.startsWith(originPackage)) {
return value.replaceFirst(originPackage, targetPackage);
}
return targetPackage + "_" + value;
modules.forEach(module -> {
property.addMetaData(new ModificationProperty.MetaData("xposedmodule", "true"));
property.addMetaData(new ModificationProperty.MetaData("xposeddescription", "NPatch Embed Module"));
property.addMetaData(new ModificationProperty.MetaData("xposedminversion", "93"));
});
property.addMetaData(new ModificationProperty.MetaData("npatch", metadata));
// 注入 MicroG 偽裝簽名與權限
if (useMicroG && originalSignature != null && !originalSignature.isEmpty()) {
try {
byte[] sigBytes = Base64.getDecoder().decode(originalSignature);
StringBuilder hex = new StringBuilder();
for (byte b : sigBytes) {
hex.append(String.format("%02x", b));
}
property.addMetaData(new ModificationProperty.MetaData("fake-signature", hex.toString()));
property.addUsesPermission("android.permission.FAKE_PACKAGE_SIGNATURE");
logger.d("Added fake-signature metadata for MicroG compatibility");
} catch (Exception e) {
logger.e("Failed to add fake-signature: " + e.getMessage());
}
}
// TODO: replace query_all with queries -> manager
if (useManager)
property.addUsesPermission("android.permission.QUERY_ALL_PACKAGES");
// 處理注入 Provider 的邏輯
if (isInjectProvider){
HashMap<String,String> providerMap = new HashMap<>();
providerMap.put("name","bin.mt.file.content.MTDataFilesProvider");

View File

@ -12,6 +12,7 @@ public class Constants {
final static public String PATCH_FILE_SUFFIX = "-npatched.apk";
final static public String PROXY_APP_COMPONENT_FACTORY = "org.lsposed.npatch.metaloader.LSPAppComponentFactoryStub";
final static public String MANAGER_PACKAGE_NAME = "org.lsposed.npatch";
final static public String REAL_GMS_PACKAGE_NAME = "com.google.android.gms";
final static public int MIN_ROLLING_VERSION_CODE = 400;
final static public int SIGBYPASS_LV_DISABLE = 0;

View File

@ -13,6 +13,7 @@ public class PatchConfig {
public final LSPConfig lspConfig;
public final String managerPackageName;
public final String newPackage;
public final boolean useMicroG;
public PatchConfig(
boolean useManager,
@ -23,7 +24,8 @@ public class PatchConfig {
String appComponentFactory,
boolean injectProvider,
boolean outputLog,
String newPackage
String newPackage,
boolean useMicroG
) {
this.useManager = useManager;
this.debuggable = debuggable;
@ -36,5 +38,6 @@ public class PatchConfig {
this.managerPackageName = Constants.MANAGER_PACKAGE_NAME;
this.newPackage = newPackage;
this.outputLog = outputLog;
this.useMicroG = useMicroG;
}
}