diff --git a/manager/src/main/java/org/lsposed/npatch/Patcher.kt b/manager/src/main/java/org/lsposed/npatch/Patcher.kt index 49138f6..d36d2f8 100644 --- a/manager/src/main/java/org/lsposed/npatch/Patcher.kt +++ b/manager/src/main/java/org/lsposed/npatch/Patcher.kt @@ -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)) } diff --git a/manager/src/main/java/org/lsposed/npatch/ui/page/NewPatchScreen.kt b/manager/src/main/java/org/lsposed/npatch/ui/page/NewPatchScreen.kt index f8914cc..3e08162 100644 --- a/manager/src/main/java/org/lsposed/npatch/ui/page/NewPatchScreen.kt +++ b/manager/src/main/java/org/lsposed/npatch/ui/page/NewPatchScreen.kt @@ -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, diff --git a/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/NewPatchViewModel.kt b/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/NewPatchViewModel.kt index 3fc52cf..53b23d2 100644 --- a/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/NewPatchViewModel.kt +++ b/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/NewPatchViewModel.kt @@ -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() 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, diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index d6d3587..13f7a9f 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -73,7 +73,8 @@ 注入文件提供器以在没有 Root 权限的情况下管理 data 目录的文件 (来自 MT 管理器) 注入加载器 Dex 对那些需要孤立服务进程的应用程序,譬如说浏览器的渲染引擎,请勾选此选项以确保他们正常运行 - 日志输出到 Media 目录 + 强制启用 MicroG 支持 + 重新导向 GMS 请求至社区版 MicroG。适用于 YouTube 等 Google 应用程序,需自行安装对应的 MicroG 服务。 日志输出到 Media 目录 将模块的 Xposed 日志输出到目标应用的 Media 目录 开始修补 返回 diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index 171b06b..7dfe9f5 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -73,6 +73,8 @@ 注入檔案选取器以在沒有 Root 權限的情況下管理 data 目錄的檔案(來自 MT 管理器) 注入載入器 Dex 對那些需要孤立服務程序的應用程式,譬如說瀏覽器的渲染引擎,請勾選此選項以確保他們正常執行 + 強制啟用 MicroG 支援 + 重新導向 GMS 請求至社群版 MicroG。適用於 YouTube 等 Google 應用程式,需自行安裝對應的 MicroG 服務。 日誌輸出到 Media 目錄 將模組的 Xposed 日誌輸出到目標應用的 Media 目錄 開始打包 diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index 5d18112..ce476eb 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -75,6 +75,8 @@ Inject file providers to manage files in the data directory without root privileges (From MT Manager) Inject loader dex For applications with isolated services, such as the render engines of browsers, please turn on this option to ensure that they work properly. + Force enable MicroG support + 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. Output Log to Media Directory Output the Xposed log to the target application Media directory. Start Patch diff --git a/patch-loader/src/main/java/org/lsposed/npatch/loader/GmsRedirector.java b/patch-loader/src/main/java/org/lsposed/npatch/loader/GmsRedirector.java new file mode 100644 index 0000000..8c9cec6 --- /dev/null +++ b/patch-loader/src/main/java/org/lsposed/npatch/loader/GmsRedirector.java @@ -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); + } + } +} \ No newline at end of file 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 3cfbc11..61c2cf8 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 @@ -145,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"); } diff --git a/patch/src/main/java/org/lsposed/patch/NPatch.java b/patch/src/main/java/org/lsposed/patch/NPatch.java index 529fd02..32db440 100644 --- a/patch/src/main/java/org/lsposed/patch/NPatch.java +++ b/patch/src/main/java/org/lsposed/patch/NPatch.java @@ -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; @@ -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 providerMap = new HashMap<>(); providerMap.put("name","bin.mt.file.content.MTDataFilesProvider"); diff --git a/share/java/src/main/java/org/lsposed/npatch/share/Constants.java b/share/java/src/main/java/org/lsposed/npatch/share/Constants.java index 55546cd..e50a34f 100644 --- a/share/java/src/main/java/org/lsposed/npatch/share/Constants.java +++ b/share/java/src/main/java/org/lsposed/npatch/share/Constants.java @@ -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; diff --git a/share/java/src/main/java/org/lsposed/npatch/share/PatchConfig.java b/share/java/src/main/java/org/lsposed/npatch/share/PatchConfig.java index 79ecbbd..ab242f0 100644 --- a/share/java/src/main/java/org/lsposed/npatch/share/PatchConfig.java +++ b/share/java/src/main/java/org/lsposed/npatch/share/PatchConfig.java @@ -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; } } \ No newline at end of file