treewide: Upgrade API level to 101 and change name to GKMSPatch

This commit is contained in:
WeiguangTWK 2026-04-02 20:14:53 +08:00
parent 41872d8261
commit 98cd89f9d0
25 changed files with 529 additions and 54 deletions

4
.gitignore vendored
View File

@ -18,3 +18,7 @@ libxposed
.cxx .cxx
/out /out
/.idea /.idea
# JVM/Android local build artifacts
**/bin/
**/release/

View File

@ -46,8 +46,8 @@ val (coreCommitCount, coreLatestTag) = FileRepositoryBuilder().setGitDir(rootPro
}.getOrNull() ?: (1145 to "1.0") }.getOrNull() ?: (1145 to "1.0")
// sync from https://github.com/JingMartix/LSPosed/blob/master/build.gradle.kts // sync from https://github.com/JingMartix/LSPosed/blob/master/build.gradle.kts
val defaultManagerPackageName by extra("org.lsposed.npatch") val defaultManagerPackageName by extra("org.gkmspatch")
val apiCode by extra(100) val apiCode by extra(101)
val verCode by extra(commitCount) val verCode by extra(commitCount)
val verName by extra("0.7.4") val verName by extra("0.7.4")
val coreVerCode by extra(coreCommitCount) val coreVerCode by extra(coreCommitCount)

View File

@ -83,7 +83,7 @@ afterEvaluate {
dependsOn("assemble$variantCapped") dependsOn("assemble$variantCapped")
from(variant.outputs.map { it.outputFile }) from(variant.outputs.map { it.outputFile })
into("${rootProject.projectDir}/out/$variantLowered") into("${rootProject.projectDir}/out/$variantLowered")
rename(".*.apk", "NPatch-v$verName-$verCode-$variantLowered.apk") rename(".*.apk", "GKMSPatch-v$verName-$verCode-$variantLowered.apk")
} }
} }
} }

View File

@ -93,7 +93,7 @@
<provider <provider
android:name=".manager.ConfigProvider" android:name=".manager.ConfigProvider"
android:authorities="org.lsposed.npatch.manager.provider.config" android:authorities="org.gkmspatch.manager.provider.config"
android:exported="true" android:exported="true"
android:enabled="true" /> android:enabled="true" />

View File

@ -29,8 +29,11 @@ import org.lsposed.npatch.lspApp
import org.lsposed.npatch.share.Constants import org.lsposed.npatch.share.Constants
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStreamReader
import java.text.Collator import java.text.Collator
import java.util.* import java.util.*
import java.util.Properties
import java.util.zip.ZipFile
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@ -42,9 +45,18 @@ object NPackageManager {
const val STATUS_USER_CANCELLED = -2 const val STATUS_USER_CANCELLED = -2
@Parcelize @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<String> = emptyList(),
) : Parcelable {
val isXposedModule: Boolean val isXposedModule: Boolean
get() = app.metaData?.get("xposedminversion") != null get() = xposedApi != null
} }
var appList by mutableStateOf(listOf<AppInfo>()) var appList by mutableStateOf(listOf<AppInfo>())
@ -76,13 +88,29 @@ object NPackageManager {
applicationList.forEach { applicationList.forEach {
val label = pm.getApplicationLabel(it) 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() appIcon[it.packageName] = iconLoader.loadIcon(it).asImageBitmap()
} }
collection.sortWith(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) collection.sortWith(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label))
val modules = buildMap { 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) ConfigManager.updateModules(modules)
appList = collection appList = collection
@ -203,7 +231,17 @@ object NPackageManager {
} }
if (primary == null) primary = appInfo if (primary == null) primary = appInfo
val label = lspApp.packageManager.getApplicationLabel(appInfo).toString() 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 // TODO: Check selected apks are from the same app
primary?.splitSourceDirs = splits.toTypedArray() primary?.splitSourceDirs = splits.toTypedArray()
@ -255,4 +293,74 @@ object NPackageManager {
ris[0].activityInfo.name 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<String>,
)
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()
}
} }

View File

@ -7,6 +7,7 @@ import android.os.IBinder
import android.os.IInterface import android.os.IInterface
import android.os.Process import android.os.Process
import android.os.SystemProperties import android.os.SystemProperties
import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -16,6 +17,7 @@ import rikka.shizuku.ShizukuBinderWrapper
import rikka.shizuku.SystemServiceHelper import rikka.shizuku.SystemServiceHelper
object ShizukuApi { object ShizukuApi {
private const val TAG = "ShizukuApi"
private fun IBinder.wrap() = ShizukuBinderWrapper(this) private fun IBinder.wrap() = ShizukuBinderWrapper(this)
private fun IInterface.asShizukuBinder() = this.asBinder().wrap() private fun IInterface.asShizukuBinder() = this.asBinder().wrap()
@ -82,10 +84,79 @@ object ShizukuApi {
} }
fun performDexOptMode(packageName: String): Boolean { fun performDexOptMode(packageName: String): Boolean {
return iPackageManager.performDexOptMode( val checkProfiles = SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false)
packageName, val compilerFilter = SystemProperties.get("pm.dexopt.install", "speed-profile")
SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false),
"verify", true, true, null // 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<Class<*>>,
packageName: String,
checkProfiles: Boolean,
compilerFilter: String
): Array<Any?>? {
var boolIdx = 0
var stringIdx = 0
val args = arrayOfNulls<Any>(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
}
} }
} }

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Process import android.os.Process
import android.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -17,6 +18,10 @@ import java.io.File
lateinit var lspApp: LSPApplication lateinit var lspApp: LSPApplication
class LSPApplication : Application() { 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 prefs: SharedPreferences
lateinit var tmpApkDir: File lateinit var tmpApkDir: File
@ -46,6 +51,10 @@ class LSPApplication : Application() {
} }
private fun verifySignature() { 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 { try {
val flags = PackageManager.GET_SIGNING_CERTIFICATES val flags = PackageManager.GET_SIGNING_CERTIFICATES
val packageInfo = packageManager.getPackageInfo(packageName, flags) val packageInfo = packageManager.getPackageInfo(packageName, flags)
@ -56,12 +65,15 @@ class LSPApplication : Application() {
val currentHash = signatures[0].hashCode() val currentHash = signatures[0].hashCode()
val targetHash = 0x0293FA43 val targetHash = 0x0293FA43
if (currentHash != targetHash) { if (currentHash != targetHash) {
Log.e(TAG, "Signature mismatch, killing process")
killApp() killApp()
} }
} else { } else {
Log.e(TAG, "No signatures found, killing process")
killApp() killApp()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Signature verification failed, killing process", e)
killApp() killApp()
} }
} }

View File

@ -58,10 +58,11 @@ object Patcher {
} }
lspApp.targetApkFiles?.clear() lspApp.targetApkFiles?.clear()
val apkFileList = arrayListOf<File>() val apkFileList = arrayListOf<File>()
val cacheRoot = lspApp.externalCacheDir ?: lspApp.cacheDir
lspApp.tmpApkDir.walk() lspApp.tmpApkDir.walk()
.filter { it.isFile && it.name.endsWith(Constants.PATCH_FILE_SUFFIX) } .filter { it.isFile && it.name.endsWith(Constants.PATCH_FILE_SUFFIX) }
.forEach { tempApkFile -> .forEach { tempApkFile ->
val cachedApkFile = File(lspApp.externalCacheDir, tempApkFile.name) val cachedApkFile = File(cacheRoot, tempApkFile.name)
if (tempApkFile.renameTo(cachedApkFile).not()) { if (tempApkFile.renameTo(cachedApkFile).not()) {
tempApkFile.copyTo(cachedApkFile, overwrite = true) tempApkFile.copyTo(cachedApkFile, overwrite = true)
tempApkFile.delete() tempApkFile.delete()

View File

@ -80,11 +80,25 @@ object ConfigManager {
Log.i(TAG, "Module apk path updated: ${it.pkgName}") Log.i(TAG, "Module apk path updated: ${it.pkgName}")
} }
loadedModules.getOrPut(it) { 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 { org.lsposed.lspd.models.Module().apply {
packageName = it.pkgName packageName = it.pkgName
apkPath = it.apkPath apkPath = it.apkPath
applicationInfo = appInfo
appId = appInfo?.uid ?: 0
file = ModuleLoader.loadModule(it.apkPath) 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
} }
} }
} }

View File

@ -12,7 +12,7 @@ import org.lsposed.npatch.config.ConfigManager
class ConfigProvider : ContentProvider() { class ConfigProvider : ContentProvider() {
companion object { companion object {
const val AUTHORITY = "org.lsposed.npatch.manager.provider.config" const val AUTHORITY = "org.gkmspatch.manager.provider.config"
const val TAG = "ConfigProvider" const val TAG = "ConfigProvider"
} }

View File

@ -22,13 +22,18 @@ object ManagerService : ILSPApplicationService.Stub() {
val app = lspApp.packageManager.getNameForUid(Binder.getCallingUid()) val app = lspApp.packageManager.getNameForUid(Binder.getCallingUid())
val list = app?.let { val list = app?.let {
runBlocking { ConfigManager.getModuleFilesForApp(it) } runBlocking { ConfigManager.getModuleFilesForApp(it) }
}.orEmpty() }.orEmpty().filter { it.file?.legacy == true }
Log.d(TAG, "$app calls getLegacyModulesList: $list") Log.d(TAG, "$app calls getLegacyModulesList: $list")
return list return list
} }
override fun getModulesList(): List<Module> { override fun getModulesList(): List<Module> {
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 { override fun getPrefsPath(packageName: String): String {

View File

@ -85,6 +85,20 @@ fun ModuleManageBody() {
fontFamily = FontFamily.Serif, fontFamily = FontFamily.Serif,
style = MaterialTheme.typography.bodySmall 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
)
} }
) )
} }

View File

@ -23,17 +23,22 @@ class ModuleManageViewModel : ViewModel() {
class XposedInfo( class XposedInfo(
val api: Int, val api: Int,
val minApi: Int?,
val targetApi: Int?,
val staticScope: Boolean,
val description: String, val description: String,
val scope: List<String> val scope: List<String>
) )
val appList: List<Pair<NPackageManager.AppInfo, XposedInfo>> by derivedStateOf { val appList: List<Pair<NPackageManager.AppInfo, XposedInfo>> by derivedStateOf {
NPackageManager.appList.mapNotNull { appInfo -> NPackageManager.appList.mapNotNull { appInfo ->
val metaData = appInfo.app.metaData ?: return@mapNotNull null
appInfo to XposedInfo( appInfo to XposedInfo(
metaData.getInt("xposedminversion", -1).also { if (it == -1) return@mapNotNull null }, appInfo.xposedApi ?: return@mapNotNull null,
metaData.getString("xposeddescription") ?: "", appInfo.xposedMinApi,
emptyList() // TODO: scope appInfo.xposedTargetApi,
appInfo.xposedStaticScope,
appInfo.xposedDescription,
appInfo.xposedScope
) )
}.also { }.also {
Log.d(TAG, "Loaded ${it.size} Xposed modules") Log.d(TAG, "Loaded ${it.size} Xposed modules")

View File

@ -27,7 +27,7 @@
<string name="home_system_abi">系统架构</string> <string name="home_system_abi">系统架构</string>
<string name="home_info_copied">已复制到剪贴板</string> <string name="home_info_copied">已复制到剪贴板</string>
<string name="home_support">支持</string> <string name="home_support">支持</string>
<string name="home_description">NPatch 是一款免费且迭代于 LSPatch 的,基于 LSPosed 核心的免 Root 的 Xposed 框架。</string> <string name="home_description">GKMSPatch 是一款基于 NPatch 且迭代于 LSPatch 的,基于 LSPosed 核心的免 Root 的 Xposed 框架。仅针对 GKMS-localify 的 API 101 版本进行适配优化</string>
<string name="home_view_source_code"><![CDATA[查看源代码 %1$s<br/>加入我们的 %2$s 频道]]></string> <string name="home_view_source_code"><![CDATA[查看源代码 %1$s<br/>加入我们的 %2$s 频道]]></string>
<!-- Manage Screen --> <!-- Manage Screen -->
<string name="screen_manage">管理</string> <string name="screen_manage">管理</string>

View File

@ -24,7 +24,7 @@
<string name="home_system_abi">系統架构</string> <string name="home_system_abi">系統架构</string>
<string name="home_info_copied">已複製到剪贴板</string> <string name="home_info_copied">已複製到剪贴板</string>
<string name="home_support">支持</string> <string name="home_support">支持</string>
<string name="home_description">NPatch 是一款免费的迭代于 LSPatch 的,基于 LSPosed 核心的免 Root 的 Xposed 框架</string> <string name="home_description">GKMSPatch 是一款基於 NPatch 且迭代於 LSPatch 的,基於 LSPosed 核心的免 Root 的 Xposed 框架。僅針對 GKMS-localify 的 API 101 版本進行適配優化</string>
<string name="home_view_source_code">查看源代码 %1$s&lt;br/&gt;加入我们的 %2$s 频道</string> <string name="home_view_source_code">查看源代码 %1$s&lt;br/&gt;加入我们的 %2$s 频道</string>
<!-- Manage Screen --> <!-- Manage Screen -->
<string name="screen_manage">管理</string> <string name="screen_manage">管理</string>

View File

@ -27,7 +27,7 @@
<string name="home_system_abi">系統架構</string> <string name="home_system_abi">系統架構</string>
<string name="home_info_copied">已複製到剪貼簿</string> <string name="home_info_copied">已複製到剪貼簿</string>
<string name="home_support">支援</string> <string name="home_support">支援</string>
<string name="home_description">NPatch 是一款免費且迭代自 LSPatch 的,基於 LSPosed 核心的免 Root 的 Xposed 框架。</string> <string name="home_description">GKMSPatch 是一款基於 NPatch 且迭代於 LSPatch 的,基於 LSPosed 核心的免 Root 的 Xposed 框架。僅針對 GKMS-localify 的 API 101 版本進行適配優化</string>
<string name="home_view_source_code"><![CDATA[查看原始碼 %1$s<br/>加入我們的 %2$s 頻道]]></string> <string name="home_view_source_code"><![CDATA[查看原始碼 %1$s<br/>加入我們的 %2$s 頻道]]></string>
<!-- Manage Screen --> <!-- Manage Screen -->
<string name="screen_manage">管理</string> <string name="screen_manage">管理</string>

View File

@ -18,7 +18,7 @@
<!-- Home Screen --> <!-- Home Screen -->
<string name="home_shizuku_warning">Some functions unavailable</string> <string name="home_shizuku_warning">Some functions unavailable</string>
<string name="home_api_version">API Version</string> <string name="home_api_version">API Version</string>
<string name="home_npatch_version">NPatch Version</string> <string name="home_npatch_version">GKMSPatch Version</string>
<string name="home_framework_version">Framework Version</string> <string name="home_framework_version">Framework Version</string>
<string name="home_system_version">System Version</string> <string name="home_system_version">System Version</string>
<string name="home_device">Device</string> <string name="home_device">Device</string>
@ -27,7 +27,7 @@
<string name="home_system_abi">System ABI</string> <string name="home_system_abi">System ABI</string>
<string name="home_info_copied">Copied to clipboard</string> <string name="home_info_copied">Copied to clipboard</string>
<string name="home_support">Support</string> <string name="home_support">Support</string>
<string name="home_description">NPatch is a free non-root Xposed framework based on LSPosed core.</string> <string name="home_description">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.</string>
<string name="home_view_source_code"><![CDATA[View source code at %1$s<br/>Join our %2$s channel]]></string> <string name="home_view_source_code"><![CDATA[View source code at %1$s<br/>Join our %2$s channel]]></string>
<!-- Manage Screen --> <!-- Manage Screen -->
@ -57,9 +57,9 @@
<string name="patch_from_applist">Select an installed app</string> <string name="patch_from_applist">Select an installed app</string>
<string name="patch_mode">Patch Mode</string> <string name="patch_mode">Patch Mode</string>
<string name="patch_local">Local</string> <string name="patch_local">Local</string>
<string name="patch_local_desc">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.</string> <string name="patch_local_desc">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.</string>
<string name="patch_integrated">Integrated</string> <string name="patch_integrated">Integrated</string>
<string name="patch_integrated_desc">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.</string> <string name="patch_integrated_desc">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.</string>
<string name="patch_embed_modules">Embed modules</string> <string name="patch_embed_modules">Embed modules</string>
<string name="patch_debuggable">Debuggable</string> <string name="patch_debuggable">Debuggable</string>
<string name="patch_sigbypass">Signature bypass</string> <string name="patch_sigbypass">Signature bypass</string>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name" translatable="false">NPatch</string> <string name="app_name" translatable="false">GKMSPatch</string>
</resources> </resources>

View File

@ -32,6 +32,11 @@ android {
androidComponents.onVariants { variant -> androidComponents.onVariants { variant ->
val variantCapped = variant.name.replaceFirstChar { it.uppercase() } val variantCapped = variant.name.replaceFirstChar { it.uppercase() }
tasks.matching { it.name == "create${variantCapped}ApkListingFileRedirect" }
.configureEach {
dependsOn(":meta-loader:package$variantCapped")
}
val copyDexTask = tasks.register<Copy>("copyDex$variantCapped") { val copyDexTask = tasks.register<Copy>("copyDex$variantCapped") {
dependsOn("assemble$variantCapped") dependsOn("assemble$variantCapped")
from(layout.buildDirectory.file("intermediates/dex/${variant.name}/mergeDex$variantCapped/classes.dex")) from(layout.buildDirectory.file("intermediates/dex/${variant.name}/mergeDex$variantCapped/classes.dex"))

View File

@ -10,6 +10,8 @@ import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo; 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.Handler;
import android.os.Looper;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.Process; import android.os.Process;
import android.system.Os; import android.system.Os;
@ -46,6 +48,7 @@ import java.util.Map;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedInit;
import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.XposedHelpers;
import hidden.HiddenApiBridge; import hidden.HiddenApiBridge;
@ -65,6 +68,7 @@ public class LSPApplication {
private static ActivityThread activityThread; private static ActivityThread activityThread;
private static LoadedApk stubLoadedApk; private static LoadedApk stubLoadedApk;
private static LoadedApk appLoadedApk; private static LoadedApk appLoadedApk;
private static boolean modernModulesInitialized = false;
private static PatchConfig config; private static PatchConfig config;
@ -99,10 +103,15 @@ public class LSPApplication {
if (config.useManager) { if (config.useManager) {
try { try {
service = new RemoteApplicationService(context); service = new RemoteApplicationService(context);
List<Module> m = service.getLegacyModulesList(); List<Module> m = new ArrayList<>();
m.addAll(service.getLegacyModulesList());
m.addAll(service.getModulesList());
JSONArray moduleArr = new JSONArray(); JSONArray moduleArr = new JSONArray();
if (m != null) { if (m != null) {
for (Module module : m) { for (Module module : m) {
if (module == null || module.apkPath == null || module.packageName == null) {
continue;
}
JSONObject moduleObj = new JSONObject(); JSONObject moduleObj = new JSONObject();
moduleObj.put("path", module.apkPath); moduleObj.put("path", module.apkPath);
moduleObj.put("packageName", module.packageName); moduleObj.put("packageName", module.packageName);
@ -127,10 +136,12 @@ public class LSPApplication {
service = new NeoLocalApplicationService(context); service = new NeoLocalApplicationService(context);
} }
} }
logServiceModuleStats(service);
disableProfile(context); disableProfile(context);
Startup.initXposed(false, ActivityThread.currentProcessName(), context.getApplicationInfo().dataDir, service); Startup.initXposed(false, ActivityThread.currentProcessName(), context.getApplicationInfo().dataDir, service);
Startup.bootstrapXposed(); Startup.bootstrapXposed();
ensureModernModulesInitialized(service);
// WARN: Since it uses `XResource`, the following class should not be initialized // WARN: Since it uses `XResource`, the following class should not be initialized
// before forkPostCommon is invoke. Otherwise, you will get failure of XResources // before forkPostCommon is invoke. Otherwise, you will get failure of XResources
@ -138,9 +149,28 @@ public class LSPApplication {
if (config.outputLog) { if (config.outputLog) {
XposedBridge.setLogPrinter(new XposedLogPrinter(0, "NPatch")); XposedBridge.setLogPrinter(new XposedLogPrinter(0, "NPatch"));
} }
Log.i(TAG, "Load modules"); try {
LSPLoader.initModules(appLoadedApk); Log.i(TAG, "Load modules");
Log.i(TAG, "Modules initialized"); 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(); switchAllClassLoader();
SigBypass.doSigBypass(context, config.sigBypassLevel); SigBypass.doSigBypass(context, config.sigBypassLevel);
@ -153,6 +183,44 @@ public class LSPApplication {
Log.i(TAG, "NPatch bootstrap completed"); Log.i(TAG, "NPatch bootstrap completed");
} }
private static void ensureModernModulesInitialized(ILSPApplicationService service) {
if (modernModulesInitialized) return;
try {
List<Module> requested = service.getModulesList();
List<String> 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<String> 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<Module> legacy = service.getLegacyModulesList();
List<Module> 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() { private static Context createLoadedApkWithContext() {
try { try {
var timeStart = System.currentTimeMillis(); var timeStart = System.currentTimeMillis();
@ -302,8 +370,27 @@ public class LSPApplication {
for (Field field : fields) { for (Field field : fields) {
if (field.getType() == ClassLoader.class) { if (field.getType() == ClassLoader.class) {
var obj = XposedHelpers.getObjectField(appLoadedApk, field.getName()); 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); 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;
}
} }

View File

@ -1,19 +1,34 @@
package org.lsposed.npatch.loader; package org.lsposed.npatch.loader;
import android.app.ActivityThread; import android.app.ActivityThread;
import android.app.AppComponentFactory;
import android.app.LoadedApk; 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 android.util.Log;
import org.lsposed.lspd.impl.LSPosedContext;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedInit; import de.robv.android.xposed.XposedInit;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage; import de.robv.android.xposed.callbacks.XC_LoadPackage;
import io.github.libxposed.api.XposedModuleInterface;
public class LSPLoader { public class LSPLoader {
private static final String TAG = "NPatch"; 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) { public static void initModules(LoadedApk loadedApk) {
if (loadedApk == null) {
Log.w(TAG, "Skip initModules: loadedApk is null");
return;
}
XposedInit.loadedPackagesInProcess.add(loadedApk.getPackageName()); XposedInit.loadedPackagesInProcess.add(loadedApk.getPackageName());
setPackageNameForResDir(loadedApk.getPackageName(), loadedApk.getResDir()); setPackageNameForResDir(loadedApk.getPackageName(), loadedApk.getResDir());
XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam( XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(
@ -26,6 +41,117 @@ public class LSPLoader {
XC_LoadPackage.callAll(lpparam); 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) { private static void setPackageNameForResDir(String packageName, String resDir) {
try { try {
// Use reflection to avoid direct type reference to android.content.res.XResources // 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); Method setMethod = xResourcesClass.getMethod("setPackageNameForResDir", String.class, String.class);
setMethod.invoke(null, packageName, resDir); setMethod.invoke(null, packageName, resDir);
} catch (Throwable e) { } 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");
} }
} }
} }

View File

@ -17,6 +17,7 @@ import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
@ -62,7 +63,11 @@ public class IntegrApplicationService extends ILSPApplicationService.Stub {
module.apkPath = cacheApkPath; module.apkPath = cacheApkPath;
module.packageName = packageName; module.packageName = packageName;
module.file = ModuleLoader.loadModule(cacheApkPath); 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) { } catch (IOException e) {
Log.e(TAG, "Error when initializing IntegrApplicationServiceClient", e); Log.e(TAG, "Error when initializing IntegrApplicationServiceClient", e);
@ -71,12 +76,12 @@ public class IntegrApplicationService extends ILSPApplicationService.Stub {
@Override @Override
public List<Module> getLegacyModulesList() throws RemoteException { public List<Module> getLegacyModulesList() throws RemoteException {
return modules; return modules.stream().filter(m -> m.file != null && m.file.legacy).collect(Collectors.toList());
} }
@Override @Override
public List<Module> getModulesList() throws RemoteException { public List<Module> getModulesList() throws RemoteException {
return new ArrayList<>(); return modules.stream().filter(m -> m.file != null && !m.file.legacy).collect(Collectors.toList());
} }
@Override @Override

View File

@ -21,10 +21,11 @@ import java.io.File;
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.stream.Collectors;
public class NeoLocalApplicationService extends ILSPApplicationService.Stub { public class NeoLocalApplicationService extends ILSPApplicationService.Stub {
private static final String TAG = "NPatch"; 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 static final Uri PROVIDER_URI = Uri.parse("content://" + AUTHORITY + "/config");
private final List<Module> cachedModule; private final List<Module> cachedModule;
@ -70,8 +71,12 @@ public class NeoLocalApplicationService extends ILSPApplicationService.Stub {
m.packageName = pkgName; m.packageName = pkgName;
m.apkPath = path; m.apkPath = path;
m.file = ModuleLoader.loadModule(m.apkPath); m.file = ModuleLoader.loadModule(m.apkPath);
cachedModule.add(m); if (m.file != null) {
Log.i(TAG, "Loaded cached module " + pkgName); 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) { } catch (Throwable e) {
Log.e(TAG, "Failed to load cached module " + pkgName, 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()) { if (m.apkPath != null && new File(m.apkPath).exists()) {
m.file = ModuleLoader.loadModule(m.apkPath); m.file = ModuleLoader.loadModule(m.apkPath);
cachedModule.add(m); if (m.file != null) {
Log.i(TAG, "NeoLocal: Loaded module " + pkgName); cachedModule.add(m);
Log.i(TAG, "NeoLocal: Loaded module " + pkgName);
} else {
Log.w(TAG, "NeoLocal: Skip invalid module package " + pkgName);
}
} }
} catch (Throwable e) { } catch (Throwable e) {
Log.e(TAG, "NeoLocal: Failed to load " + pkgName, e); Log.e(TAG, "NeoLocal: Failed to load " + pkgName, e);
@ -121,12 +130,12 @@ public class NeoLocalApplicationService extends ILSPApplicationService.Stub {
@Override @Override
public List<Module> getLegacyModulesList() throws RemoteException { public List<Module> getLegacyModulesList() throws RemoteException {
return cachedModule; return cachedModule.stream().filter(m -> m.file != null && m.file.legacy).collect(Collectors.toList());
} }
@Override @Override
public List<Module> getModulesList() throws RemoteException { public List<Module> getModulesList() throws RemoteException {
return new ArrayList<>(); return cachedModule.stream().filter(m -> m.file != null && !m.file.legacy).collect(Collectors.toList());
} }
@Override @Override

View File

@ -60,8 +60,16 @@ public class ModuleLoader {
var moduleLibraryNames = new ArrayList<String>(1); var moduleLibraryNames = new ArrayList<String>(1);
try (var apkFile = new ZipFile(path)) { try (var apkFile = new ZipFile(path)) {
readDexes(apkFile, preLoadedDexes); readDexes(apkFile, preLoadedDexes);
readName(apkFile, "assets/xposed_init", moduleClassNames); // Prefer modern metadata first.
readName(apkFile, "assets/native_init", moduleLibraryNames); 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) { } catch (IOException e) {
Log.e(TAG, "Can not open " + path, e); Log.e(TAG, "Can not open " + path, e);
return null; return null;

View File

@ -11,7 +11,7 @@ public class Constants {
final static public String PATCH_FILE_SUFFIX = "-npatched.apk"; 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 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 String REAL_GMS_PACKAGE_NAME = "com.google.android.gms";
final static public int MIN_ROLLING_VERSION_CODE = 400; final static public int MIN_ROLLING_VERSION_CODE = 400;