diff --git a/.gitignore b/.gitignore index 4387765..a484b18 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ libxposed .cxx /out /.idea + +# JVM/Android local build artifacts +**/bin/ +**/release/ diff --git a/build.gradle.kts b/build.gradle.kts index 6654405..e23b7e8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,8 +46,8 @@ val (coreCommitCount, coreLatestTag) = FileRepositoryBuilder().setGitDir(rootPro }.getOrNull() ?: (1145 to "1.0") // sync from https://github.com/JingMartix/LSPosed/blob/master/build.gradle.kts -val defaultManagerPackageName by extra("org.lsposed.npatch") -val apiCode by extra(100) +val defaultManagerPackageName by extra("org.gkmspatch") +val apiCode by extra(101) val verCode by extra(commitCount) val verName by extra("0.7.4") val coreVerCode by extra(coreCommitCount) @@ -259,4 +259,4 @@ project(":core") { } } } -} \ No newline at end of file +} diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index 2ee516e..49c0e61 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -83,7 +83,7 @@ afterEvaluate { dependsOn("assemble$variantCapped") from(variant.outputs.map { it.outputFile }) into("${rootProject.projectDir}/out/$variantLowered") - rename(".*.apk", "NPatch-v$verName-$verCode-$variantLowered.apk") + rename(".*.apk", "GKMSPatch-v$verName-$verCode-$variantLowered.apk") } } } @@ -127,4 +127,4 @@ dependencies { debugImplementation(npatch.androidx.compose.ui.tooling) debugImplementation(npatch.androidx.customview) debugImplementation(npatch.androidx.customview.poolingcontainer) -} \ No newline at end of file +} diff --git a/manager/src/main/AndroidManifest.xml b/manager/src/main/AndroidManifest.xml index c07f011..857905d 100644 --- a/manager/src/main/AndroidManifest.xml +++ b/manager/src/main/AndroidManifest.xml @@ -93,9 +93,9 @@ - \ No newline at end of file + diff --git a/manager/src/main/java/nkbe/util/NPackageManager.kt b/manager/src/main/java/nkbe/util/NPackageManager.kt index be7bc53..9b7c193 100644 --- a/manager/src/main/java/nkbe/util/NPackageManager.kt +++ b/manager/src/main/java/nkbe/util/NPackageManager.kt @@ -29,8 +29,11 @@ import org.lsposed.npatch.lspApp import org.lsposed.npatch.share.Constants import java.io.File import java.io.IOException +import java.io.InputStreamReader import java.text.Collator import java.util.* +import java.util.Properties +import java.util.zip.ZipFile import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -42,9 +45,18 @@ object NPackageManager { const val STATUS_USER_CANCELLED = -2 @Parcelize - class AppInfo(val app: ApplicationInfo, val label: String) : Parcelable { + class AppInfo( + val app: ApplicationInfo, + val label: String, + val xposedApi: Int? = null, + val xposedMinApi: Int? = null, + val xposedTargetApi: Int? = null, + val xposedStaticScope: Boolean = false, + val xposedDescription: String = "", + val xposedScope: List = emptyList(), + ) : Parcelable { val isXposedModule: Boolean - get() = app.metaData?.get("xposedminversion") != null + get() = xposedApi != null } var appList by mutableStateOf(listOf()) @@ -76,13 +88,29 @@ object NPackageManager { applicationList.forEach { val label = pm.getApplicationLabel(it) - collection.add(AppInfo(it, label.toString())) + val moduleMeta = resolveModuleMeta(pm, it) + collection.add( + AppInfo( + app = it, + label = label.toString(), + xposedApi = moduleMeta?.api, + xposedMinApi = moduleMeta?.minApi, + xposedTargetApi = moduleMeta?.targetApi, + xposedStaticScope = moduleMeta?.staticScope ?: false, + xposedDescription = moduleMeta?.description.orEmpty(), + xposedScope = moduleMeta?.scope.orEmpty(), + ) + ) appIcon[it.packageName] = iconLoader.loadIcon(it).asImageBitmap() } collection.sortWith(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) val modules = buildMap { - collection.forEach { if (it.isXposedModule) put(it.app.packageName, it.app.sourceDir) } + collection.forEach { + if (it.isXposedModule && !it.app.sourceDir.isNullOrBlank()) { + put(it.app.packageName, it.app.sourceDir) + } + } } ConfigManager.updateModules(modules) appList = collection @@ -203,7 +231,17 @@ object NPackageManager { } if (primary == null) primary = appInfo val label = lspApp.packageManager.getApplicationLabel(appInfo).toString() - AppInfo(appInfo, label) + val moduleMeta = resolveModuleMeta(lspApp.packageManager, appInfo) + AppInfo( + app = appInfo, + label = label, + xposedApi = moduleMeta?.api, + xposedMinApi = moduleMeta?.minApi, + xposedTargetApi = moduleMeta?.targetApi, + xposedStaticScope = moduleMeta?.staticScope ?: false, + xposedDescription = moduleMeta?.description.orEmpty(), + xposedScope = moduleMeta?.scope.orEmpty(), + ) } // TODO: Check selected apks are from the same app primary?.splitSourceDirs = splits.toTypedArray() @@ -255,4 +293,74 @@ object NPackageManager { ris[0].activityInfo.name ) } + + private data class ModuleMeta( + val api: Int?, + val minApi: Int?, + val targetApi: Int?, + val staticScope: Boolean, + val description: String, + val scope: List, + ) + + private fun resolveModuleMeta(pm: PackageManager, appInfo: ApplicationInfo): ModuleMeta? { + val legacyApi = readLegacyApiFromMeta(appInfo) + val legacyDescription = appInfo.metaData?.getString("xposeddescription").orEmpty() + val sourceDir = appInfo.sourceDir ?: return if (legacyApi != null) { + ModuleMeta(legacyApi, legacyApi, legacyApi, false, legacyDescription, emptyList()) + } else { + null + } + return runCatching { + ZipFile(sourceDir).use { apk -> + val modernEntry = apk.getEntry("META-INF/xposed/java_init.list") + val legacyEntry = apk.getEntry("assets/xposed_init") + if (modernEntry == null && legacyEntry == null && legacyApi == null) { + null + } else if (modernEntry != null) { + val prop = Properties() + apk.getEntry("META-INF/xposed/module.prop")?.let { propEntry -> + apk.getInputStream(propEntry).use { prop.load(it) } + } + val minApi = extractIntPart(prop.getProperty("minApiVersion")) + val targetApi = extractIntPart(prop.getProperty("targetApiVersion")) + val api = targetApi ?: minApi ?: 100 + val staticScope = prop.getProperty("staticScope") == "true" + + val scope = + apk.getEntry("META-INF/xposed/scope.list")?.let { scopeEntry -> + apk.getInputStream(scopeEntry).use { input -> + InputStreamReader(input).readLines().map { it.trim() } + .filter { it.isNotEmpty() && !it.startsWith("#") } + } + } ?: emptyList() + + val description = appInfo.loadDescription(pm)?.toString().orEmpty() + ModuleMeta(api, minApi, targetApi, staticScope, description, scope) + } else { + ModuleMeta(legacyApi ?: 0, legacyApi, legacyApi, false, legacyDescription, emptyList()) + } + } + }.getOrElse { + if (legacyApi != null) { + ModuleMeta(legacyApi, legacyApi, legacyApi, false, legacyDescription, emptyList()) + } else { + null + } + } + } + + private fun readLegacyApiFromMeta(appInfo: ApplicationInfo): Int? { + val raw = appInfo.metaData?.get("xposedminversion") ?: return null + return when (raw) { + is Int -> raw + is String -> extractIntPart(raw) + else -> null + } + } + + private fun extractIntPart(value: String?): Int? { + if (value.isNullOrBlank()) return null + return value.takeWhile { it.isDigit() }.takeIf { it.isNotEmpty() }?.toIntOrNull() + } } diff --git a/manager/src/main/java/nkbe/util/ShizukuApi.kt b/manager/src/main/java/nkbe/util/ShizukuApi.kt index c328fa8..0fe7c3d 100644 --- a/manager/src/main/java/nkbe/util/ShizukuApi.kt +++ b/manager/src/main/java/nkbe/util/ShizukuApi.kt @@ -7,6 +7,7 @@ import android.os.IBinder import android.os.IInterface import android.os.Process import android.os.SystemProperties +import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -16,6 +17,7 @@ import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.SystemServiceHelper object ShizukuApi { + private const val TAG = "ShizukuApi" private fun IBinder.wrap() = ShizukuBinderWrapper(this) private fun IInterface.asShizukuBinder() = this.asBinder().wrap() @@ -82,10 +84,79 @@ object ShizukuApi { } fun performDexOptMode(packageName: String): Boolean { - return iPackageManager.performDexOptMode( - packageName, - SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false), - "verify", true, true, null - ) + val checkProfiles = SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false) + val compilerFilter = SystemProperties.get("pm.dexopt.install", "speed-profile") + + // Try framework method first, but do it reflectively so signature changes + // across Android versions won't crash manager process. + runCatching { + val methods = iPackageManager.javaClass.methods + .filter { it.name == "performDexOptMode" } + for (method in methods) { + val args = buildDexOptArgs(method.parameterTypes, packageName, checkProfiles, compilerFilter) + if (args == null) continue + val ret = method.invoke(iPackageManager, *args) + if (ret is Boolean) { + Log.i(TAG, "performDexOptMode via IPackageManager#${method.name}${method.parameterTypes.contentToString()} => $ret") + return ret + } + } + }.onFailure { + Log.w(TAG, "performDexOptMode by reflection failed: ${it.message}", it) + } + + // Fallback for newer systems where hidden API signature changed/removed. + return runCmdCompile(packageName) + } + + private fun buildDexOptArgs( + types: Array>, + packageName: String, + checkProfiles: Boolean, + compilerFilter: String + ): Array? { + var boolIdx = 0 + var stringIdx = 0 + val args = arrayOfNulls(types.size) + for (i in types.indices) { + val t = types[i] + args[i] = when { + t == String::class.java -> { + when (stringIdx++) { + 0 -> packageName + 1 -> compilerFilter + else -> null // splitName/optional string + } + } + t == Boolean::class.javaPrimitiveType || t == java.lang.Boolean::class.java -> { + when (boolIdx++) { + 0 -> checkProfiles + 1 -> true // force + 2 -> true // bootComplete + else -> false + } + } + t == Int::class.javaPrimitiveType || t == java.lang.Integer::class.java -> 0 + t == Long::class.javaPrimitiveType || t == java.lang.Long::class.java -> 0L + else -> null + } + } + // At minimum we expect first arg is packageName for compatibility. + return if (types.isNotEmpty() && types[0] == String::class.java) args else null + } + + private fun runCmdCompile(packageName: String): Boolean { + return runCatching { + val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", "cmd package compile -m speed-profile -f $packageName")) + val output = process.inputStream.bufferedReader().use { it.readText() } + val err = process.errorStream.bufferedReader().use { it.readText() } + val code = process.waitFor() + val ok = code == 0 && (output.contains("Success", ignoreCase = true) || err.isBlank()) + Log.i(TAG, "dexopt fallback exit=$code, out=$output, err=$err") + ok + }.getOrElse { + Log.e(TAG, "dexopt fallback failed: ${it.message}", it) + false + } } } diff --git a/manager/src/main/java/org/lsposed/npatch/LSPApplication.kt b/manager/src/main/java/org/lsposed/npatch/LSPApplication.kt index 873142f..01e0e76 100644 --- a/manager/src/main/java/org/lsposed/npatch/LSPApplication.kt +++ b/manager/src/main/java/org/lsposed/npatch/LSPApplication.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.SharedPreferences import android.content.pm.PackageManager import android.os.Process +import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -17,6 +18,10 @@ import java.io.File lateinit var lspApp: LSPApplication class LSPApplication : Application() { + companion object { + private const val TAG = "LSPApplication" + private const val OFFICIAL_MANAGER_PACKAGE = "org.lsposed.npatch" + } lateinit var prefs: SharedPreferences lateinit var tmpApkDir: File @@ -46,6 +51,10 @@ class LSPApplication : Application() { } private fun verifySignature() { + // Keep upstream anti-tamper behavior for official package only. + // Forked/rebranded packages should not be killed silently at launch. + if (packageName != OFFICIAL_MANAGER_PACKAGE) return + try { val flags = PackageManager.GET_SIGNING_CERTIFICATES val packageInfo = packageManager.getPackageInfo(packageName, flags) @@ -56,12 +65,15 @@ class LSPApplication : Application() { val currentHash = signatures[0].hashCode() val targetHash = 0x0293FA43 if (currentHash != targetHash) { + Log.e(TAG, "Signature mismatch, killing process") killApp() } } else { + Log.e(TAG, "No signatures found, killing process") killApp() } } catch (e: Exception) { + Log.e(TAG, "Signature verification failed, killing process", e) killApp() } } diff --git a/manager/src/main/java/org/lsposed/npatch/Patcher.kt b/manager/src/main/java/org/lsposed/npatch/Patcher.kt index d36d2f8..3ce2a40 100644 --- a/manager/src/main/java/org/lsposed/npatch/Patcher.kt +++ b/manager/src/main/java/org/lsposed/npatch/Patcher.kt @@ -58,10 +58,11 @@ object Patcher { } lspApp.targetApkFiles?.clear() val apkFileList = arrayListOf() + val cacheRoot = lspApp.externalCacheDir ?: lspApp.cacheDir lspApp.tmpApkDir.walk() .filter { it.isFile && it.name.endsWith(Constants.PATCH_FILE_SUFFIX) } .forEach { tempApkFile -> - val cachedApkFile = File(lspApp.externalCacheDir, tempApkFile.name) + val cachedApkFile = File(cacheRoot, tempApkFile.name) if (tempApkFile.renameTo(cachedApkFile).not()) { tempApkFile.copyTo(cachedApkFile, overwrite = true) tempApkFile.delete() diff --git a/manager/src/main/java/org/lsposed/npatch/config/ConfigManager.kt b/manager/src/main/java/org/lsposed/npatch/config/ConfigManager.kt index 0b0e3f4..b8d2006 100644 --- a/manager/src/main/java/org/lsposed/npatch/config/ConfigManager.kt +++ b/manager/src/main/java/org/lsposed/npatch/config/ConfigManager.kt @@ -80,11 +80,25 @@ object ConfigManager { Log.i(TAG, "Module apk path updated: ${it.pkgName}") } loadedModules.getOrPut(it) { + val appInfo = try { + lspApp.packageManager.getApplicationInfo(it.pkgName, PackageManager.GET_META_DATA) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Cannot resolve applicationInfo for module: ${it.pkgName}", e) + null + } org.lsposed.lspd.models.Module().apply { packageName = it.pkgName apkPath = it.apkPath + applicationInfo = appInfo + appId = appInfo?.uid ?: 0 file = ModuleLoader.loadModule(it.apkPath) } + }.takeIf { loaded -> + loaded.file != null + } ?: run { + Log.w(TAG, "Module has no valid entry list: ${it.pkgName}") + loadedModules.remove(it) + null } } } diff --git a/manager/src/main/java/org/lsposed/npatch/manager/ConfigProvider.kt b/manager/src/main/java/org/lsposed/npatch/manager/ConfigProvider.kt index 3b9a232..5f0cd32 100644 --- a/manager/src/main/java/org/lsposed/npatch/manager/ConfigProvider.kt +++ b/manager/src/main/java/org/lsposed/npatch/manager/ConfigProvider.kt @@ -12,7 +12,7 @@ import org.lsposed.npatch.config.ConfigManager class ConfigProvider : ContentProvider() { companion object { - const val AUTHORITY = "org.lsposed.npatch.manager.provider.config" + const val AUTHORITY = "org.gkmspatch.manager.provider.config" const val TAG = "ConfigProvider" } diff --git a/manager/src/main/java/org/lsposed/npatch/manager/ManagerService.kt b/manager/src/main/java/org/lsposed/npatch/manager/ManagerService.kt index 39dbf72..f31c4e2 100644 --- a/manager/src/main/java/org/lsposed/npatch/manager/ManagerService.kt +++ b/manager/src/main/java/org/lsposed/npatch/manager/ManagerService.kt @@ -22,13 +22,18 @@ object ManagerService : ILSPApplicationService.Stub() { val app = lspApp.packageManager.getNameForUid(Binder.getCallingUid()) val list = app?.let { runBlocking { ConfigManager.getModuleFilesForApp(it) } - }.orEmpty() + }.orEmpty().filter { it.file?.legacy == true } Log.d(TAG, "$app calls getLegacyModulesList: $list") return list } override fun getModulesList(): List { - return emptyList() + val app = lspApp.packageManager.getNameForUid(Binder.getCallingUid()) + val list = app?.let { + runBlocking { ConfigManager.getModuleFilesForApp(it) } + }.orEmpty().filter { it.file?.legacy == false } + Log.d(TAG, "$app calls getModulesList: $list") + return list } override fun getPrefsPath(packageName: String): String { diff --git a/manager/src/main/java/org/lsposed/npatch/ui/page/manage/ModuleManagePage.kt b/manager/src/main/java/org/lsposed/npatch/ui/page/manage/ModuleManagePage.kt index 24cab22..12217b3 100644 --- a/manager/src/main/java/org/lsposed/npatch/ui/page/manage/ModuleManagePage.kt +++ b/manager/src/main/java/org/lsposed/npatch/ui/page/manage/ModuleManagePage.kt @@ -85,6 +85,20 @@ fun ModuleManageBody() { fontFamily = FontFamily.Serif, style = MaterialTheme.typography.bodySmall ) + val minApiText = it.second.minApi?.toString() ?: "-" + val targetApiText = it.second.targetApi?.toString() ?: "-" + Text( + text = "min/target $minApiText / $targetApiText", + fontWeight = FontWeight.SemiBold, + fontFamily = FontFamily.Serif, + style = MaterialTheme.typography.bodySmall + ) + Text( + text = "staticScope ${if (it.second.staticScope) "true" else "false"}", + fontWeight = FontWeight.SemiBold, + fontFamily = FontFamily.Serif, + style = MaterialTheme.typography.bodySmall + ) } ) } diff --git a/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/ModuleManageViewModel.kt b/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/ModuleManageViewModel.kt index 05110ff..71ed2dc 100644 --- a/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/ModuleManageViewModel.kt +++ b/manager/src/main/java/org/lsposed/npatch/ui/viewmodel/manage/ModuleManageViewModel.kt @@ -23,17 +23,22 @@ class ModuleManageViewModel : ViewModel() { class XposedInfo( val api: Int, + val minApi: Int?, + val targetApi: Int?, + val staticScope: Boolean, val description: String, val scope: List ) val appList: List> by derivedStateOf { NPackageManager.appList.mapNotNull { appInfo -> - val metaData = appInfo.app.metaData ?: return@mapNotNull null appInfo to XposedInfo( - metaData.getInt("xposedminversion", -1).also { if (it == -1) return@mapNotNull null }, - metaData.getString("xposeddescription") ?: "", - emptyList() // TODO: scope + appInfo.xposedApi ?: return@mapNotNull null, + appInfo.xposedMinApi, + appInfo.xposedTargetApi, + appInfo.xposedStaticScope, + appInfo.xposedDescription, + appInfo.xposedScope ) }.also { Log.d(TAG, "Loaded ${it.size} Xposed modules") diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index 13f7a9f..e1a3f81 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -27,7 +27,7 @@ 系统架构 已复制到剪贴板 支持 - NPatch 是一款免费且迭代于 LSPatch 的,基于 LSPosed 核心的免 Root 的 Xposed 框架。 + GKMSPatch 是一款基于 NPatch 且迭代于 LSPatch 的,基于 LSPosed 核心的免 Root 的 Xposed 框架。仅针对 GKMS-localify 的 API 101 版本进行适配优化。 加入我们的 %2$s 频道]]> 管理 diff --git a/manager/src/main/res/values-zh-rHK/strings.xml b/manager/src/main/res/values-zh-rHK/strings.xml index aff2c8f..27709fb 100644 --- a/manager/src/main/res/values-zh-rHK/strings.xml +++ b/manager/src/main/res/values-zh-rHK/strings.xml @@ -24,7 +24,7 @@ 系統架构 已複製到剪贴板 支持 - NPatch 是一款免费的迭代于 LSPatch 的,基于 LSPosed 核心的免 Root 的 Xposed 框架。 + GKMSPatch 是一款基於 NPatch 且迭代於 LSPatch 的,基於 LSPosed 核心的免 Root 的 Xposed 框架。僅針對 GKMS-localify 的 API 101 版本進行適配優化。 查看源代码 %1$s<br/>加入我们的 %2$s 频道 管理 @@ -91,4 +91,4 @@ 启用 install activity 注入載入器 Dex 对那些需要孤立服务进程的應用程式程序,譬如说浏览器的渲染引擎,请勾选此选项以确保他们正常运行 - \ No newline at end of file + diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index 7dfe9f5..009c732 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -27,7 +27,7 @@ 系統架構 已複製到剪貼簿 支援 - NPatch 是一款免費且迭代自 LSPatch 的,基於 LSPosed 核心的免 Root 的 Xposed 框架。 + GKMSPatch 是一款基於 NPatch 且迭代於 LSPatch 的,基於 LSPosed 核心的免 Root 的 Xposed 框架。僅針對 GKMS-localify 的 API 101 版本進行適配優化。 加入我們的 %2$s 頻道]]> 管理 diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index ce476eb..6b7f7ee 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -18,7 +18,7 @@ Some functions unavailable API Version - NPatch Version + GKMSPatch Version Framework Version System Version Device @@ -27,7 +27,7 @@ System ABI Copied to clipboard Support - NPatch is a free non-root Xposed framework based on LSPosed core. + GKMSPatch is a non-root Xposed framework based on NPatch and iterated from LSPatch, built on LSPosed core. It is only adapted and optimized for the API 101 version of GKMS-localify. Join our %2$s channel]]> @@ -57,9 +57,9 @@ Select an installed app Patch Mode Local - Patches apps without embedding modules.\nThe Xposed scope can be dynamically changed without needing to re-patch.\nThis mode can only be used locally with the NPatch Manager. + Patches apps without embedding modules.\nThe Xposed scope can be dynamically changed without needing to re-patch.\nThis mode can only be used locally with the GKMSPatch Manager. Integrated - Patches apps with built-in modules.\nThe patched app can run independently without the Manager, but it cannot dynamically manage configurations.\nThis mode is suitable for apps that need to run on devices without the NPatch Manager installed. + Patches apps with built-in modules.\nThe patched app can run independently without the Manager, but it cannot dynamically manage configurations.\nThis mode is suitable for apps that need to run on devices without the GKMSPatch Manager installed. Embed modules Debuggable Signature bypass diff --git a/manager/src/main/res/values/strings_untranslatable.xml b/manager/src/main/res/values/strings_untranslatable.xml index d9e5ef3..2aae8d9 100644 --- a/manager/src/main/res/values/strings_untranslatable.xml +++ b/manager/src/main/res/values/strings_untranslatable.xml @@ -1,4 +1,4 @@ - NPatch + GKMSPatch diff --git a/patch-loader/build.gradle.kts b/patch-loader/build.gradle.kts index 7c8ca59..a0b8928 100644 --- a/patch-loader/build.gradle.kts +++ b/patch-loader/build.gradle.kts @@ -32,6 +32,11 @@ android { androidComponents.onVariants { variant -> val variantCapped = variant.name.replaceFirstChar { it.uppercase() } + tasks.matching { it.name == "create${variantCapped}ApkListingFileRedirect" } + .configureEach { + dependsOn(":meta-loader:package$variantCapped") + } + val copyDexTask = tasks.register("copyDex$variantCapped") { dependsOn("assemble$variantCapped") from(layout.buildDirectory.file("intermediates/dex/${variant.name}/mergeDex$variantCapped/classes.dex")) 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 61c2cf8..f1973d5 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 @@ -10,6 +10,8 @@ import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.os.RemoteException; import android.os.Process; import android.system.Os; @@ -46,6 +48,7 @@ import java.util.Map; import java.util.function.BiConsumer; import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedInit; import de.robv.android.xposed.XposedHelpers; import hidden.HiddenApiBridge; @@ -65,6 +68,7 @@ public class LSPApplication { private static ActivityThread activityThread; private static LoadedApk stubLoadedApk; private static LoadedApk appLoadedApk; + private static boolean modernModulesInitialized = false; private static PatchConfig config; @@ -99,10 +103,15 @@ public class LSPApplication { if (config.useManager) { try { service = new RemoteApplicationService(context); - List m = service.getLegacyModulesList(); + List m = new ArrayList<>(); + m.addAll(service.getLegacyModulesList()); + m.addAll(service.getModulesList()); JSONArray moduleArr = new JSONArray(); if (m != null) { for (Module module : m) { + if (module == null || module.apkPath == null || module.packageName == null) { + continue; + } JSONObject moduleObj = new JSONObject(); moduleObj.put("path", module.apkPath); moduleObj.put("packageName", module.packageName); @@ -127,10 +136,12 @@ public class LSPApplication { service = new NeoLocalApplicationService(context); } } + logServiceModuleStats(service); disableProfile(context); Startup.initXposed(false, ActivityThread.currentProcessName(), context.getApplicationInfo().dataDir, service); Startup.bootstrapXposed(); + ensureModernModulesInitialized(service); // WARN: Since it uses `XResource`, the following class should not be initialized // before forkPostCommon is invoke. Otherwise, you will get failure of XResources @@ -138,9 +149,28 @@ public class LSPApplication { if (config.outputLog) { XposedBridge.setLogPrinter(new XposedLogPrinter(0, "NPatch")); } - Log.i(TAG, "Load modules"); - LSPLoader.initModules(appLoadedApk); - Log.i(TAG, "Modules initialized"); + try { + Log.i(TAG, "Load modules"); + LSPLoader.initModules(appLoadedApk); + boolean modernDispatched = LSPLoader.dispatchModernCallbacksNow(appLoadedApk); + if (!modernDispatched) { + Log.w(TAG, "Modern callbacks not ready in sync stage, fallback to async retries"); + LSPLoader.dispatchModernCallbacksAsync(appLoadedApk); + } + Log.i(TAG, "Modules initialized"); + } catch (Throwable t) { + Log.e(TAG, "Failed to initialize modules", t); + Log.i(TAG, "Fallback: schedule module init on main looper"); + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> { + try { + LSPLoader.initModules(appLoadedApk); + LSPLoader.dispatchModernCallbacksAsync(appLoadedApk); + } catch (Throwable tt) { + Log.e(TAG, "Fallback async init failed", tt); + } + }); + } switchAllClassLoader(); SigBypass.doSigBypass(context, config.sigBypassLevel); @@ -153,6 +183,44 @@ public class LSPApplication { Log.i(TAG, "NPatch bootstrap completed"); } + private static void ensureModernModulesInitialized(ILSPApplicationService service) { + if (modernModulesInitialized) return; + try { + List requested = service.getModulesList(); + List requestedPkgs = new ArrayList<>(); + if (requested != null) { + for (Module module : requested) { + if (module != null && module.packageName != null) { + requestedPkgs.add(module.packageName); + } + } + } + XposedInit.loadModules(activityThread); + modernModulesInitialized = true; + List failedPkgs = new ArrayList<>(); + for (String pkg : requestedPkgs) { + if (!XposedInit.getLoadedModules().containsKey(pkg)) { + failedPkgs.add(pkg); + } + } + Log.i(TAG, "Modern modules initialized explicitly, requested=" + requestedPkgs + ", failed=" + failedPkgs); + } catch (Throwable t) { + Log.e(TAG, "Failed to initialize modern modules explicitly", t); + } + } + + private static void logServiceModuleStats(ILSPApplicationService service) { + try { + List legacy = service.getLegacyModulesList(); + List modern = service.getModulesList(); + int legacyCount = legacy == null ? 0 : legacy.size(); + int modernCount = modern == null ? 0 : modern.size(); + Log.i(TAG, "Service module stats: legacy=" + legacyCount + ", modern=" + modernCount); + } catch (Throwable t) { + Log.w(TAG, "Failed to read service module stats", t); + } + } + private static Context createLoadedApkWithContext() { try { var timeStart = System.currentTimeMillis(); @@ -302,8 +370,27 @@ public class LSPApplication { for (Field field : fields) { if (field.getType() == ClassLoader.class) { var obj = XposedHelpers.getObjectField(appLoadedApk, field.getName()); + if (obj == null) continue; + // Android 14+ may throw when addNative() sees a non-PathClassLoader here. + // Keep old value unless the replacement is a PathClassLoader-compatible one. + if (!isPathClassLoader(obj)) { + Log.w(TAG, "Skip replacing classloader field " + field.getName() + " with " + obj.getClass().getName()); + continue; + } XposedHelpers.setObjectField(stubLoadedApk, field.getName(), obj); } } } + + private static boolean isPathClassLoader(Object loader) { + if (!(loader instanceof ClassLoader)) return false; + Class clz = loader.getClass(); + while (clz != null) { + if ("dalvik.system.PathClassLoader".equals(clz.getName())) { + return true; + } + clz = clz.getSuperclass(); + } + return false; + } } diff --git a/patch-loader/src/main/java/org/lsposed/npatch/loader/LSPLoader.java b/patch-loader/src/main/java/org/lsposed/npatch/loader/LSPLoader.java index 88ba2ac..365718a 100644 --- a/patch-loader/src/main/java/org/lsposed/npatch/loader/LSPLoader.java +++ b/patch-loader/src/main/java/org/lsposed/npatch/loader/LSPLoader.java @@ -1,19 +1,34 @@ package org.lsposed.npatch.loader; import android.app.ActivityThread; +import android.app.AppComponentFactory; import android.app.LoadedApk; +import android.content.pm.ApplicationInfo; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.util.Log; +import org.lsposed.lspd.impl.LSPosedContext; + import java.lang.reflect.Method; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedInit; +import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.callbacks.XC_LoadPackage; +import io.github.libxposed.api.XposedModuleInterface; public class LSPLoader { private static final String TAG = "NPatch"; + private static final int MODERN_DISPATCH_MAX_RETRY = 8; + private static final long MODERN_DISPATCH_RETRY_DELAY_MS = 400L; public static void initModules(LoadedApk loadedApk) { + if (loadedApk == null) { + Log.w(TAG, "Skip initModules: loadedApk is null"); + return; + } XposedInit.loadedPackagesInProcess.add(loadedApk.getPackageName()); setPackageNameForResDir(loadedApk.getPackageName(), loadedApk.getResDir()); XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam( @@ -26,6 +41,117 @@ public class LSPLoader { XC_LoadPackage.callAll(lpparam); } + public static void dispatchModernCallbacksAsync(LoadedApk loadedApk) { + dispatchModernCallbacksAsync(loadedApk, 0); + } + + public static boolean dispatchModernCallbacksNow(LoadedApk loadedApk) { + return tryDispatchModernCallbacks(loadedApk, 0); + } + + private static void dispatchModernCallbacksAsync(LoadedApk loadedApk, int retry) { + if (loadedApk == null) return; + Handler handler = new Handler(Looper.getMainLooper()); + handler.postDelayed(() -> { + if (tryDispatchModernCallbacks(loadedApk, retry)) { + return; + } + if (retry < MODERN_DISPATCH_MAX_RETRY) { + dispatchModernCallbacksAsync(loadedApk, retry + 1); + } else { + Log.w(TAG, "Modern callback dispatch exhausted retries for " + loadedApk.getPackageName()); + } + }, retry == 0 ? 600L : MODERN_DISPATCH_RETRY_DELAY_MS); + } + + private static boolean tryDispatchModernCallbacks(LoadedApk loadedApk, int retry) { + final var packageName = loadedApk.getPackageName(); + final var appInfo = loadedApk.getApplicationInfo(); + final var classLoader = loadedApk.getClassLoader(); + if (appInfo == null || classLoader == null) { + Log.w(TAG, "Modern dispatch retry " + retry + ": appInfo/classLoader not ready"); + return false; + } + final var defaultClassLoader = resolveDefaultClassLoader(loadedApk, classLoader); + + // Check one typical app class path availability before dispatching callbacks. + // This reduces chances of firing too early in patched process bootstrap. + try { + classLoader.loadClass("android.app.Activity"); + } catch (Throwable t) { + Log.w(TAG, "Modern dispatch retry " + retry + ": classloader not stable yet"); + return false; + } + + try { + LSPosedContext.callOnPackageLoaded(new XposedModuleInterface.PackageLoadedParam() { + @Override + public String getPackageName() { + return packageName; + } + + @Override + public ApplicationInfo getApplicationInfo() { + return appInfo; + } + + @Override + public ClassLoader getDefaultClassLoader() { + return defaultClassLoader; + } + + @Override + public ClassLoader getClassLoader() { + return classLoader; + } + + @Override + public boolean isFirstPackage() { + return true; + } + }); + } catch (Throwable t) { + Log.e(TAG, "callOnPackageLoaded failed for " + packageName + " retry=" + retry, t); + } + + Object appComponentFactory = null; + try { + appComponentFactory = XposedHelpers.getObjectField(loadedApk, "mAppComponentFactory"); + } catch (Throwable ignored) { + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !(appComponentFactory instanceof AppComponentFactory)) { + appComponentFactory = new AppComponentFactory(); + } + try { + LSPosedContext.callOnPackageReady( + packageName, + appInfo, + true, + defaultClassLoader, + classLoader, + appComponentFactory + ); + Log.i(TAG, "Modern callbacks dispatched for " + packageName + " on retry=" + retry); + return true; + } catch (Throwable t) { + Log.e(TAG, "callOnPackageReady failed for " + packageName + " retry=" + retry, t); + return false; + } + } + + private static ClassLoader resolveDefaultClassLoader(LoadedApk loadedApk, ClassLoader fallback) { + try { + var field = LoadedApk.class.getDeclaredField("mDefaultClassLoader"); + field.setAccessible(true); + var value = field.get(loadedApk); + if (value instanceof ClassLoader) { + return (ClassLoader) value; + } + } catch (Throwable ignored) { + } + return fallback; + } + private static void setPackageNameForResDir(String packageName, String resDir) { try { // Use reflection to avoid direct type reference to android.content.res.XResources @@ -36,7 +162,8 @@ public class LSPLoader { 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); + // Resource hooks are optional in the patch-loader path; avoid noisy stack traces. + Log.w(TAG, "XResources.setPackageNameForResDir not available, skipping"); } } -} \ No newline at end of file +} diff --git a/patch-loader/src/main/java/org/lsposed/npatch/service/IntegrApplicationService.java b/patch-loader/src/main/java/org/lsposed/npatch/service/IntegrApplicationService.java index 61f485a..a0738d2 100644 --- a/patch-loader/src/main/java/org/lsposed/npatch/service/IntegrApplicationService.java +++ b/patch-loader/src/main/java/org/lsposed/npatch/service/IntegrApplicationService.java @@ -17,6 +17,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -62,7 +63,11 @@ public class IntegrApplicationService extends ILSPApplicationService.Stub { module.apkPath = cacheApkPath; module.packageName = packageName; module.file = ModuleLoader.loadModule(cacheApkPath); - modules.add(module); + if (module.file != null) { + modules.add(module); + } else { + Log.w(TAG, "Skip invalid embedded module: " + packageName); + } } } catch (IOException e) { Log.e(TAG, "Error when initializing IntegrApplicationServiceClient", e); @@ -71,12 +76,12 @@ public class IntegrApplicationService extends ILSPApplicationService.Stub { @Override public List getLegacyModulesList() throws RemoteException { - return modules; + return modules.stream().filter(m -> m.file != null && m.file.legacy).collect(Collectors.toList()); } @Override public List getModulesList() throws RemoteException { - return new ArrayList<>(); + return modules.stream().filter(m -> m.file != null && !m.file.legacy).collect(Collectors.toList()); } @Override @@ -99,4 +104,4 @@ public class IntegrApplicationService extends ILSPApplicationService.Stub { return false; } -} \ No newline at end of file +} diff --git a/patch-loader/src/main/java/org/lsposed/npatch/service/NeoLocalApplicationService.java b/patch-loader/src/main/java/org/lsposed/npatch/service/NeoLocalApplicationService.java index fef5089..7feeee8 100644 --- a/patch-loader/src/main/java/org/lsposed/npatch/service/NeoLocalApplicationService.java +++ b/patch-loader/src/main/java/org/lsposed/npatch/service/NeoLocalApplicationService.java @@ -21,10 +21,11 @@ import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; public class NeoLocalApplicationService extends ILSPApplicationService.Stub { private static final String TAG = "NPatch"; - private static final String AUTHORITY = "org.lsposed.npatch.manager.provider.config"; + private static final String AUTHORITY = "org.gkmspatch.manager.provider.config"; private static final Uri PROVIDER_URI = Uri.parse("content://" + AUTHORITY + "/config"); private final List cachedModule; @@ -70,8 +71,12 @@ public class NeoLocalApplicationService extends ILSPApplicationService.Stub { m.packageName = pkgName; m.apkPath = path; m.file = ModuleLoader.loadModule(m.apkPath); - cachedModule.add(m); - Log.i(TAG, "Loaded cached module " + pkgName); + if (m.file != null) { + cachedModule.add(m); + Log.i(TAG, "Loaded cached module " + pkgName); + } else { + Log.w(TAG, "Skip cached module without valid entry list: " + pkgName); + } } catch (Throwable e) { Log.e(TAG, "Failed to load cached module " + pkgName, e); } @@ -111,8 +116,12 @@ public class NeoLocalApplicationService extends ILSPApplicationService.Stub { if (m.apkPath != null && new File(m.apkPath).exists()) { m.file = ModuleLoader.loadModule(m.apkPath); - cachedModule.add(m); - Log.i(TAG, "NeoLocal: Loaded module " + pkgName); + if (m.file != null) { + cachedModule.add(m); + Log.i(TAG, "NeoLocal: Loaded module " + pkgName); + } else { + Log.w(TAG, "NeoLocal: Skip invalid module package " + pkgName); + } } } catch (Throwable e) { Log.e(TAG, "NeoLocal: Failed to load " + pkgName, e); @@ -121,12 +130,12 @@ public class NeoLocalApplicationService extends ILSPApplicationService.Stub { @Override public List getLegacyModulesList() throws RemoteException { - return cachedModule; + return cachedModule.stream().filter(m -> m.file != null && m.file.legacy).collect(Collectors.toList()); } @Override public List getModulesList() throws RemoteException { - return new ArrayList<>(); + return cachedModule.stream().filter(m -> m.file != null && !m.file.legacy).collect(Collectors.toList()); } @Override diff --git a/share/android/src/main/java/org/lsposed/npatch/util/ModuleLoader.java b/share/android/src/main/java/org/lsposed/npatch/util/ModuleLoader.java index 3f98fa5..14e9641 100644 --- a/share/android/src/main/java/org/lsposed/npatch/util/ModuleLoader.java +++ b/share/android/src/main/java/org/lsposed/npatch/util/ModuleLoader.java @@ -60,8 +60,16 @@ public class ModuleLoader { var moduleLibraryNames = new ArrayList(1); try (var apkFile = new ZipFile(path)) { readDexes(apkFile, preLoadedDexes); - readName(apkFile, "assets/xposed_init", moduleClassNames); - readName(apkFile, "assets/native_init", moduleLibraryNames); + // Prefer modern metadata first. + readName(apkFile, "META-INF/xposed/java_init.list", moduleClassNames); + if (moduleClassNames.isEmpty()) { + file.legacy = true; + readName(apkFile, "assets/xposed_init", moduleClassNames); + readName(apkFile, "assets/native_init", moduleLibraryNames); + } else { + file.legacy = false; + readName(apkFile, "META-INF/xposed/native_init.list", moduleLibraryNames); + } } catch (IOException e) { Log.e(TAG, "Can not open " + path, e); return null; 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 e50a34f..45b0bdf 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 @@ -11,7 +11,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 MANAGER_PACKAGE_NAME = "org.gkmspatch"; final static public String REAL_GMS_PACKAGE_NAME = "com.google.android.gms"; final static public int MIN_ROLLING_VERSION_CODE = 400;