├── example ├── libs │ ├── libxposed-api.aar │ └── libxposed-service.aar ├── src │ └── main │ │ ├── kotlin │ │ └── com │ │ │ └── xzakota │ │ │ └── hook │ │ │ └── example │ │ │ ├── utils │ │ │ └── HookUtils.kt │ │ │ ├── ui │ │ │ └── MainActivity.kt │ │ │ └── startup │ │ │ ├── XPInitEntry.kt │ │ │ └── LSPInitEntry.kt │ │ ├── AndroidManifest.xml │ │ └── res │ │ └── layout │ │ └── layout_activity_main.xml ├── proguard-rules.pro └── build.gradle.kts ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── constant ├── build.gradle.kts └── src │ └── main │ └── java │ └── com │ └── xzakota │ └── android │ └── xposed │ ├── XposedAPIVersion.java │ └── XposedFramework.java ├── annotation ├── build.gradle.kts └── src │ └── main │ └── java │ └── com │ └── xzakota │ └── xposed │ └── annotation │ └── ModuleEntry.java ├── gradle-plugin ├── src │ └── main │ │ └── kotlin │ │ └── com │ │ └── xzakota │ │ ├── extension │ │ ├── Document.kt │ │ ├── String.kt │ │ ├── Project.kt │ │ ├── Element.kt │ │ ├── File.kt │ │ └── JarOutputStream.kt │ │ ├── gradle │ │ └── plugin │ │ │ └── xposed │ │ │ ├── DataProvider.kt │ │ │ ├── BaseModuleGenerateTask.kt │ │ │ ├── GenerateModuleConfigClassTask.kt │ │ │ ├── GenerateModulePropertiesTask.kt │ │ │ ├── GenerateModuleResTask.kt │ │ │ ├── XposedModuleExtension.kt │ │ │ ├── GenerateModuleManifestTask.kt │ │ │ ├── GenerateModuleEntryTask.kt │ │ │ └── GradlePluginForXposed.kt │ │ ├── android │ │ ├── AndroidXML.kt │ │ └── AndroidResource.kt │ │ └── LangCode.kt └── build.gradle.kts ├── .gitignore ├── settings.gradle.kts ├── gradle.properties ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /example/libs/libxposed-api.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xzakota/XposedModuleMaker/HEAD/example/libs/libxposed-api.aar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xzakota/XposedModuleMaker/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/libs/libxposed-service.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xzakota/XposedModuleMaker/HEAD/example/libs/libxposed-service.aar -------------------------------------------------------------------------------- /constant/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // noinspection JavaPluginLanguageLevel 3 | id("java-library") 4 | alias(libs.plugins.vanniktech.maven.publish) 5 | } 6 | -------------------------------------------------------------------------------- /annotation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // noinspection JavaPluginLanguageLevel 3 | id("java-library") 4 | alias(libs.plugins.vanniktech.maven.publish) 5 | } 6 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/extension/Document.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.extension 2 | 3 | import org.dom4j.Document 4 | import org.dom4j.Element 5 | 6 | internal fun Document.addElement(name : String, block : Element.() -> Unit) = addElement(name).apply(block) 7 | -------------------------------------------------------------------------------- /example/src/main/kotlin/com/xzakota/hook/example/utils/HookUtils.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.hook.example.utils 2 | 3 | import androidx.annotation.Keep 4 | 5 | object HookUtils { 6 | @Keep 7 | var isSelfModuleActivated = false 8 | 9 | @Keep 10 | var xposedAPIVersion = -1 11 | } -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/extension/String.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.extension 2 | 3 | import java.util.Locale 4 | 5 | internal fun String.capitalizeFirstChar() = replaceFirstChar { 6 | if (it.isLowerCase()) { 7 | it.titlecase(Locale.ROOT) 8 | } else { 9 | it.toString() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/extension/Project.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.extension 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.plugins.JavaPlugin 5 | 6 | internal fun Project.addDependencies(module : String, version : String) { 7 | dependencies.add(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME, "$module:$version") 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Tue Feb 20 22:01:31 CST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://mirrors.aliyun.com/macports/distfiles/gradle/gradle-9.0.0-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/extension/Element.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.extension 2 | 3 | import org.dom4j.Element 4 | import org.dom4j.Namespace 5 | 6 | internal fun Element.addNamespace(namespace : Namespace) = addNamespace(namespace.prefix, namespace.uri) 7 | 8 | internal fun Element.addElement(name : String, block : Element.() -> Unit) = addElement(name).apply(block) 9 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/extension/File.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.extension 2 | 3 | import java.io.File 4 | 5 | internal fun File.safeMkdirs() { 6 | if (!exists()) { 7 | mkdirs() 8 | } 9 | } 10 | 11 | internal fun File.safeCreateNewFile() { 12 | if (!exists()) { 13 | parentFile.mkdirs() 14 | createNewFile() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /constant/src/main/java/com/xzakota/android/xposed/XposedAPIVersion.java: -------------------------------------------------------------------------------- 1 | package com.xzakota.android.xposed; 2 | 3 | @SuppressWarnings("unused") 4 | public class XposedAPIVersion { 5 | public static final int XP_API_82 = 82; 6 | public static final int XP_API_89 = 89; 7 | public static final int XP_API_93 = 93; 8 | public static final int XP_API_100 = 100; 9 | } 10 | -------------------------------------------------------------------------------- /constant/src/main/java/com/xzakota/android/xposed/XposedFramework.java: -------------------------------------------------------------------------------- 1 | package com.xzakota.android.xposed; 2 | 3 | @SuppressWarnings("unused") 4 | public enum XposedFramework { 5 | XPOSED("Xposed"), LSPOSED("LSPosed"); 6 | 7 | private final String name; 8 | 9 | XposedFramework(String name) { 10 | this.name = name; 11 | } 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /annotation/src/main/java/com/xzakota/xposed/annotation/ModuleEntry.java: -------------------------------------------------------------------------------- 1 | package com.xzakota.xposed.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @SuppressWarnings("unused") 9 | @Retention(RetentionPolicy.CLASS) 10 | @Target(ElementType.TYPE) 11 | public @interface ModuleEntry {} 12 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/extension/JarOutputStream.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.extension 2 | 3 | import java.io.File 4 | import java.util.jar.JarEntry 5 | import java.util.jar.JarOutputStream 6 | 7 | internal fun JarOutputStream.createFile(name : String, data : ByteArray) = runCatching { 8 | putNextEntry(JarEntry(name.replace(File.separatorChar, '/'))) 9 | write(data) 10 | closeEntry() 11 | } 12 | 13 | internal fun JarOutputStream.createDirectory(name : String) = runCatching { 14 | putNextEntry(JarEntry(name.replace(File.separatorChar, '/'))) 15 | closeEntry() 16 | } 17 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/gradle/plugin/xposed/DataProvider.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.gradle.plugin.xposed 2 | 3 | import com.xzakota.android.xposed.XposedFramework 4 | 5 | internal object DataProvider { 6 | val moduleConfig = hashMapOf() 7 | 8 | lateinit var xposedEntryClassName : String 9 | lateinit var lsposedEntryClassName : String 10 | 11 | fun isSupportXposed(config : XposedModuleExtension) : Boolean = config.framework has XposedFramework.XPOSED 12 | fun isSupportLSPosed(config : XposedModuleExtension) : Boolean = config.framework has XposedFramework.LSPOSED 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # kotlin 6 | .kotlin 7 | 8 | # Local configuration file (sdk path, etc) 9 | local.properties 10 | 11 | # Log/OS Files 12 | *.log 13 | 14 | # Android Studio generated files and folders 15 | captures/ 16 | .externalNativeBuild/ 17 | .cxx/ 18 | *.apk 19 | output.json 20 | 21 | # IntelliJ 22 | *.iws 23 | *.iml 24 | *.ipr 25 | .idea/ 26 | misc.xml 27 | deploymentTargetDropDown.xml 28 | render.experimental.xml 29 | 30 | # Keystore files 31 | *.jks 32 | *.keystore 33 | 34 | # Mac OS 35 | .DS_Store 36 | 37 | # Google Services (e.g. APIs or Firebase) 38 | google-services.json 39 | 40 | # Android Profiling 41 | *.hprof -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/android/AndroidXML.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.android 2 | 3 | import org.dom4j.Namespace 4 | import org.dom4j.QName 5 | 6 | internal object AndroidXML { 7 | @JvmField 8 | val NAMESPACE_ANDROID = Namespace("android", "http://schemas.android.com/apk/res/android") 9 | 10 | @JvmField 11 | val QUALIFIED_NAME_NAME : QName = QName.get("name", NAMESPACE_ANDROID) 12 | 13 | @JvmField 14 | val QUALIFIED_NAME_VALUE : QName = QName.get("value", NAMESPACE_ANDROID) 15 | 16 | @JvmField 17 | val QUALIFIED_NAME_RESOURCE : QName = QName.get("resource", NAMESPACE_ANDROID) 18 | 19 | @JvmField 20 | val QUALIFIED_NAME_DESCRIPTION : QName = QName.get("description", NAMESPACE_ANDROID) 21 | } 22 | -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | maven("https://maven.aliyun.com/repository/gradle-plugin/") 6 | maven("https://maven.aliyun.com/repository/public/") 7 | maven("https://maven.aliyun.com/repository/google/") 8 | 9 | mavenLocal() 10 | google() 11 | mavenCentral() 12 | gradlePluginPortal() 13 | } 14 | } 15 | 16 | dependencyResolutionManagement { 17 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 18 | repositories { 19 | maven("https://maven.aliyun.com/repository/public/") 20 | maven("https://maven.aliyun.com/repository/google/") 21 | 22 | mavenLocal() 23 | google() 24 | mavenCentral() 25 | } 26 | } 27 | 28 | rootProject.name = "XposedModuleMaker" 29 | include( 30 | ":example", 31 | ":annotation", 32 | ":constant", 33 | ":gradle-plugin" 34 | ) 35 | -------------------------------------------------------------------------------- /example/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Xposed 2 | -adaptresourcefilecontents META-INF/xposed/java_init.list 3 | -keepattributes RuntimeVisibleAnnotations 4 | -keep,allowobfuscation,allowoptimization public class * extends io.github.libxposed.api.XposedModule { 5 | public (...); 6 | public void onPackageLoaded(...); 7 | public void onSystemServerLoaded(...); 8 | } 9 | -keep,allowoptimization,allowobfuscation @io.github.libxposed.api.annotations.* class * { 10 | @io.github.libxposed.api.annotations.BeforeInvocation ; 11 | @io.github.libxposed.api.annotations.AfterInvocation ; 12 | } 13 | 14 | # Kotlin 15 | -assumenosideeffects class kotlin.jvm.internal.Intrinsics { 16 | public static void check*(...); 17 | public static void throw*(...); 18 | } 19 | -assumenosideeffects class java.util.Objects { 20 | public static ** requireNonNull(...); 21 | } 22 | 23 | # Strip debug log 24 | -assumenosideeffects class android.util.Log { 25 | public static int v(...); 26 | public static int d(...); 27 | } 28 | 29 | # Obfuscation 30 | -repackageclasses 31 | -allowaccessmodification -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.2.20" 3 | agp = "8.13.0" 4 | 5 | xposed-module-maker = "0.1.4" 6 | 7 | [libraries] 8 | android-tools = { module = "com.android.tools.build:gradle", version.ref = "agp" } 9 | 10 | xposed-api = { module = "de.robv.android.xposed:api", version = "82" } 11 | xposed-module-maker-annotation = { module = "com.xzakota.xposed:annotation", version.ref = "xposed-module-maker" } 12 | xposed-module-maker-constant = { module = "com.xzakota.xposed:constant", version.ref = "xposed-module-maker" } 13 | 14 | dom4j = { module = "org.dom4j:dom4j", version = "2.2.0" } 15 | grip = { module = "com.joom.grip:grip", version = "0.9.1" } 16 | 17 | [plugins] 18 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 19 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 20 | android-application = { id = "com.android.application", version.ref = "agp" } 21 | gradle-plugin-publish = { id = "com.gradle.plugin-publish", version = "1.3.1" } 22 | vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version = "0.34.0" } 23 | 24 | xposed-module-maker = { id = "com.xzakota.xposed", version.ref = "xposed-module-maker" } 25 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/gradle/plugin/xposed/BaseModuleGenerateTask.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.gradle.plugin.xposed 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.tasks.Input 5 | import org.gradle.api.tasks.Internal 6 | import org.gradle.api.tasks.TaskAction 7 | 8 | abstract class BaseModuleGenerateTask : DefaultTask() { 9 | init { 10 | group = "xposed" 11 | } 12 | 13 | @Internal 14 | protected val currentProjectName = project.name 15 | 16 | @Internal 17 | protected val xposedModuleConfig = DataProvider.moduleConfig[currentProjectName]!! 18 | 19 | @get:Input 20 | protected val moduleMinAPIVersion = xposedModuleConfig.minAPIVersion 21 | 22 | @get:Input 23 | protected val moduleDescription = xposedModuleConfig.description 24 | 25 | @get:Input 26 | protected val moduleDescriptionRes = xposedModuleConfig.descriptionRes 27 | 28 | @get:Input 29 | protected val moduleScope = xposedModuleConfig.scope 30 | 31 | @get:Input 32 | protected val moduleFrameworkSupportList = xposedModuleConfig.framework.supportList 33 | 34 | @Internal 35 | protected val moduleResGenerator = xposedModuleConfig.resGenerator 36 | 37 | @TaskAction 38 | fun run() { 39 | onAction() 40 | } 41 | 42 | protected abstract fun onAction() 43 | } 44 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/gradle/plugin/xposed/GenerateModuleConfigClassTask.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.gradle.plugin.xposed 2 | 3 | import com.xzakota.android.xposed.XposedFramework 4 | import com.xzakota.extension.safeCreateNewFile 5 | import org.gradle.api.tasks.Internal 6 | import java.io.File 7 | 8 | abstract class GenerateModuleConfigClassTask : BaseModuleGenerateTask() { 9 | @Internal 10 | lateinit var packageName : String 11 | 12 | init { 13 | description = "Generate module config class" 14 | } 15 | 16 | override fun onAction() { 17 | File(outputs.files.firstOrNull() ?: return, "${packageName.replace(".", "/")}/ModuleConfig.kt").apply { 18 | safeCreateNewFile() 19 | writeText( 20 | """ 21 | package $packageName 22 | 23 | import ${XposedFramework::class.java.name} 24 | 25 | object ModuleConfig { 26 | @JvmField 27 | val supportFramework = arrayOf(${moduleFrameworkSupportList.joinToString(", ") { "XposedFramework.$it" }}) 28 | 29 | @JvmField 30 | val scope = arrayOf(${moduleScope.joinToString(", ") { "\"$it\"" }}) 31 | } 32 | """.trimIndent() 33 | ) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/src/main/res/layout/layout_activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 13 | 14 | 18 | 19 | 24 | 25 | 26 | 29 | 30 | 34 | 35 | 40 | 41 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/LangCode.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota 2 | 3 | @Suppress("unused") 4 | object LangCode { 5 | const val LANG_CODE_DEFAULT = "default" 6 | 7 | // 中文 8 | const val LANG_CODE_ZH = "zh" 9 | const val LANG_CODE_ZH_CN = "zh-rCN" 10 | const val LANG_CODE_ZH_HK = "zh-rHK" 11 | const val LANG_CODE_ZH_TW = "zh-rTW" 12 | 13 | // 英文 14 | const val LANG_CODE_EN = "en" 15 | const val LANG_CODE_EN_US = "en-rUS" 16 | const val LANG_CODE_EN_GB = "en-rGB" 17 | 18 | // 俄文 19 | const val LANG_CODE_RU = "ru" 20 | const val LANG_CODE_RU_RU = "ru-rRU" 21 | 22 | // 法文 23 | const val LANG_CODE_FR = "fr" 24 | const val LANG_CODE_FR_FR = "fr-rFR" 25 | 26 | // 西班牙文 27 | const val LANG_CODE_ES = "es" 28 | const val LANG_CODE_ES_ES = "es-rES" 29 | 30 | // 德文 31 | const val LANG_CODE_DE = "de" 32 | const val LANG_CODE_DE_DE = "de-rDE" 33 | 34 | // 日文 35 | const val LANG_CODE_JA = "ja" 36 | const val LANG_CODE_JA_JP = "ja-rJP" 37 | 38 | // 韩文 39 | const val LANG_CODE_KO = "ko" 40 | const val LANG_CODE_KO_KR = "ko-rKR" 41 | 42 | // 阿拉伯文 43 | const val LANG_CODE_AR = "ar" 44 | const val LANG_CODE_AR_SA = "ar-rSA" 45 | 46 | // 意大利文 47 | const val LANG_CODE_IT = "it" 48 | const val LANG_CODE_IT_IT = "it-rIT" 49 | 50 | // 葡萄牙文 51 | const val LANG_CODE_PT = "pt" 52 | const val LANG_CODE_PT_BR = "pt-rBR" 53 | 54 | // 荷兰文 55 | const val LANG_CODE_NL = "nl" 56 | const val LANG_CODE_NL_NL = "nl-rNL" 57 | } 58 | -------------------------------------------------------------------------------- /example/src/main/kotlin/com/xzakota/hook/example/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.hook.example.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.os.Bundle 6 | import android.widget.TextView 7 | import com.xzakota.hook.example.databinding.LayoutActivityMainBinding 8 | import com.xzakota.hook.example.utils.HookUtils 9 | import io.github.libxposed.service.XposedService 10 | import io.github.libxposed.service.XposedServiceHelper 11 | 12 | @SuppressLint("SetTextI18n") 13 | class MainActivity : Activity() { 14 | private lateinit var isActivatedTextView : TextView 15 | private lateinit var xposedApiVersionTextView : TextView 16 | 17 | override fun onCreate(savedInstanceState : Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | 20 | val binding = LayoutActivityMainBinding.inflate(layoutInflater) 21 | setContentView(binding.root) 22 | isActivatedTextView = binding.isActivatedText 23 | xposedApiVersionTextView = binding.xposedApiVersionText 24 | 25 | XposedServiceHelper.registerListener(object : XposedServiceHelper.OnServiceListener { 26 | override fun onServiceBind(service : XposedService) { 27 | HookUtils.isSelfModuleActivated = true 28 | HookUtils.xposedAPIVersion = service.apiVersion 29 | 30 | syncTextView() 31 | } 32 | 33 | override fun onServiceDied(p0 : XposedService) {} 34 | }) 35 | 36 | syncTextView() 37 | } 38 | 39 | private fun syncTextView() { 40 | isActivatedTextView.text = HookUtils.isSelfModuleActivated.toString() 41 | xposedApiVersionTextView.text = HookUtils.xposedAPIVersion.toString() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/gradle/plugin/xposed/GenerateModulePropertiesTask.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused", "DEPRECATION") 2 | 3 | package com.xzakota.gradle.plugin.xposed 4 | 5 | import com.android.build.gradle.api.AndroidSourceSet 6 | import com.xzakota.extension.safeMkdirs 7 | import org.gradle.api.NamedDomainObjectContainer 8 | import org.gradle.api.file.DirectoryProperty 9 | import org.gradle.api.tasks.Input 10 | import org.gradle.api.tasks.Internal 11 | import org.gradle.api.tasks.OutputDirectory 12 | import java.io.File 13 | 14 | abstract class GenerateModulePropertiesTask : BaseModuleGenerateTask() { 15 | @get:OutputDirectory 16 | abstract val outputDir : DirectoryProperty 17 | 18 | @Internal 19 | lateinit var sourceSets : NamedDomainObjectContainer 20 | 21 | @Internal 22 | protected val lsposedAPIConfig = xposedModuleConfig.lsposed 23 | 24 | @Input 25 | protected val moduleTargetAPIVersion = lsposedAPIConfig.targetAPIVersion 26 | 27 | @get:Input 28 | protected val isModuleStaticScope = lsposedAPIConfig.isStaticScope 29 | 30 | init { 31 | description = "Generate lsposed properties" 32 | } 33 | 34 | override fun onAction() { 35 | val modulePropertiesDir = File(outputDir.get().asFile, "META-INF/xposed") 36 | modulePropertiesDir.safeMkdirs() 37 | 38 | File(modulePropertiesDir, "module.prop").writeText( 39 | """ 40 | minApiVersion=$moduleMinAPIVersion 41 | targetApiVersion=$moduleTargetAPIVersion 42 | staticScope=$isModuleStaticScope 43 | """.trimIndent() 44 | ) 45 | 46 | File(modulePropertiesDir, "scope.list").writeText(moduleScope.joinToString("\n")) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.xzakota.android.xposed.XposedAPIVersion 2 | import com.xzakota.android.xposed.XposedFramework 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | 5 | plugins { 6 | id("com.android.application") 7 | kotlin("android") 8 | alias(libs.plugins.xposed.module.maker) 9 | } 10 | 11 | android { 12 | namespace = "com.xzakota.hook.example" 13 | compileSdk = 35 14 | 15 | defaultConfig { 16 | applicationId = namespace 17 | minSdk = 24 18 | targetSdk = 35 19 | versionCode = 1 20 | versionName = "1.0" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | isMinifyEnabled = true 26 | isShrinkResources = true 27 | isDebuggable = false 28 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 29 | } 30 | } 31 | 32 | buildFeatures { 33 | buildConfig = true 34 | viewBinding = true 35 | } 36 | 37 | compileOptions { 38 | sourceCompatibility = JavaVersion.VERSION_17 39 | targetCompatibility = JavaVersion.VERSION_17 40 | } 41 | } 42 | 43 | kotlin { 44 | compilerOptions.jvmTarget.set(JvmTarget.JVM_17) 45 | } 46 | 47 | xposedModule { 48 | minAPIVersion = XposedAPIVersion.XP_API_100 49 | description = "Xposed Example" 50 | 51 | framework { 52 | add(XposedFramework.LSPOSED) 53 | } 54 | 55 | lsposed { 56 | targetAPIVersion = minAPIVersion 57 | isStaticScope = false 58 | } 59 | 60 | scope += listOf( 61 | "com.android.settings" 62 | ) 63 | 64 | isIncludeDependencies = false 65 | isGenerateConfigClass = true 66 | } 67 | 68 | dependencies { 69 | // https://api.xposed.info 70 | compileOnly(libs.xposed.api) 71 | 72 | // https://github.com/libxposed 73 | compileOnly(files("libs/libxposed-api.aar")) 74 | implementation(files("libs/libxposed-service.aar")) 75 | 76 | // https://github.com/xzakota/XposedModuleMaker 77 | compileOnly(libs.xposed.module.maker.annotation) 78 | } 79 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | 25 | # publish 26 | POM_NAME=XposedModuleMaker 27 | POM_DESCRIPTION=XposedModuleMaker 28 | POM_INCEPTION_YEAR=2025 29 | POM_URL=https://github.com/xzakota/XposedModuleMaker 30 | 31 | POM_LICENSE_NAME=The Apache Software License, Version 2.0 32 | POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt 33 | POM_LICENSE_DIST=repo 34 | 35 | POM_SCM_URL=https://github.com/xzakota/XposedModuleMaker 36 | POM_SCM_CONNECTION=scm:git:git://github.com/xzakota/XposedModuleMaker.git 37 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/xzakota/XposedModuleMaker.git 38 | 39 | POM_DEVELOPER_ID=xzakota 40 | POM_DEVELOPER_NAME=xzakota 41 | POM_DEVELOPER_URL=https://github.com/xzakota/ 42 | POM_DEVELOPER_EMAIL=xzakota@qq.com 43 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/gradle/plugin/xposed/GenerateModuleResTask.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.gradle.plugin.xposed 2 | 3 | import com.xzakota.LangCode 4 | import com.xzakota.android.AndroidResource 5 | import com.xzakota.extension.safeCreateNewFile 6 | import org.gradle.api.file.DirectoryProperty 7 | import org.gradle.api.tasks.OutputDirectory 8 | import java.io.File 9 | 10 | abstract class GenerateModuleResTask : BaseModuleGenerateTask() { 11 | @get:OutputDirectory 12 | abstract val outputDir : DirectoryProperty 13 | 14 | init { 15 | description = "Generate module res values" 16 | } 17 | 18 | override fun onAction() { 19 | val resDir = outputDir.get().asFile 20 | 21 | val generateStringAction : (String, String) -> Unit = action@{ langCode, value -> 22 | if (langCode.isEmpty()) { 23 | return@action 24 | } 25 | 26 | val stringRes = "values" + if (langCode == LangCode.LANG_CODE_DEFAULT) { 27 | "" 28 | } else { 29 | "-${langCode}" 30 | } + "/strings.xml" 31 | 32 | File(resDir, stringRes).apply { 33 | safeCreateNewFile() 34 | AndroidResource.generateStrings(this, moduleResGenerator.resID.descriptionResID, value) 35 | } 36 | } 37 | 38 | if (moduleDescriptionRes.isNotEmpty()) { 39 | resDir.listFiles()?.forEach { 40 | if (it.isDirectory) { 41 | it.deleteRecursively() 42 | } 43 | } 44 | moduleDescriptionRes.forEach(generateStringAction) 45 | } else { 46 | generateStringAction(LangCode.LANG_CODE_DEFAULT, moduleDescription) 47 | resDir.listFiles()?.forEach { 48 | if (it.isDirectory && it.name != "values") { 49 | it.deleteRecursively() 50 | } 51 | } 52 | } 53 | 54 | File(resDir, "values/array.xml").apply { 55 | safeCreateNewFile() 56 | AndroidResource.generateArray(this, moduleResGenerator.resID.scopeResID, moduleScope) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /gradle-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | `java-gradle-plugin` 6 | alias(libs.plugins.gradle.plugin.publish) 7 | alias(libs.plugins.vanniktech.maven.publish) 8 | } 9 | 10 | val projectGroup: String by extra 11 | 12 | gradlePlugin { 13 | plugins { 14 | create(rootProject.name) { 15 | id = projectGroup 16 | implementationClass = "com.xzakota.gradle.plugin.xposed.GradlePluginForXposed" 17 | } 18 | } 19 | } 20 | 21 | val generatedDir = File(projectDir, "build/generated") 22 | val generatedJavaSourcesDir = File(generatedDir, "main/java") 23 | 24 | tasks { 25 | val task = register("generateBuildConfigClass") { 26 | inputs.property("version", version) 27 | outputs.dir(generatedDir) 28 | 29 | doLast { 30 | val buildClassFile = File(generatedJavaSourcesDir, "${projectGroup.replace(".", "/")}/BuildConfig.java") 31 | buildClassFile.parentFile.mkdirs() 32 | buildClassFile.writeText( 33 | """ 34 | package $projectGroup; 35 | 36 | /** 37 | * By Gradle 38 | */ 39 | public class BuildConfig { 40 | private BuildConfig() {} 41 | 42 | /** 43 | * Project Version 44 | */ 45 | public static final String VERSION = "$version"; 46 | } 47 | """.trimIndent() 48 | ) 49 | } 50 | } 51 | 52 | withType(KotlinCompile::class.java) { 53 | dependsOn(task) 54 | } 55 | 56 | withType(Jar::class.java) { 57 | dependsOn(task) 58 | } 59 | } 60 | 61 | sourceSets { 62 | main { 63 | java { 64 | srcDir(generatedJavaSourcesDir) 65 | } 66 | } 67 | } 68 | 69 | dependencies { 70 | compileOnly(libs.android.tools) 71 | 72 | implementation(libs.dom4j) 73 | implementation(libs.grip) 74 | 75 | implementation(libs.xposed.module.maker.annotation) 76 | api(libs.xposed.module.maker.constant) 77 | } 78 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/android/AndroidResource.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.android 2 | 3 | import com.xzakota.extension.addElement 4 | import org.dom4j.DocumentHelper 5 | import org.dom4j.Element 6 | import org.dom4j.io.OutputFormat 7 | import org.dom4j.io.XMLWriter 8 | import java.io.File 9 | 10 | @Suppress("unused") 11 | internal object AndroidResource { 12 | private const val TYPE_RES_STRING = "string" 13 | private const val TYPE_RES_STRING_ARRAY = "string-array" 14 | 15 | fun generateStrings( 16 | baseFile : File, 17 | name : String, 18 | vararg initValue : String 19 | ) = generate(baseFile, TYPE_RES_STRING, name, *initValue) 20 | 21 | fun generateArray( 22 | baseFile : File, 23 | name : String, 24 | vararg initValue : String 25 | ) = generate(baseFile, TYPE_RES_STRING_ARRAY, name, *initValue) 26 | 27 | fun generateArray( 28 | baseFile : File, 29 | name : String, 30 | initValue : List 31 | ) = generate(baseFile, TYPE_RES_STRING_ARRAY, name, *initValue.toTypedArray()) 32 | 33 | fun generate(baseFile : File, initTag : String, name : String, vararg initValue : String) = generate(baseFile) { 34 | if (initValue.isNotEmpty()) { 35 | if (initTag == TYPE_RES_STRING_ARRAY) { 36 | addElement(initTag) { 37 | addAttribute("name", name) 38 | 39 | initValue.forEach { 40 | addElement("item") { 41 | text = it 42 | } 43 | } 44 | } 45 | } else { 46 | initValue.forEach { 47 | addElement(initTag) { 48 | addAttribute("name", name) 49 | text = it 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | private fun generate(baseFile : File, block : Element.() -> Unit) { 57 | val document = DocumentHelper.createDocument() 58 | document.addElement("resources", block) 59 | 60 | XMLWriter( 61 | baseFile.writer(), 62 | OutputFormat.createPrettyPrint().apply { 63 | encoding = Charsets.UTF_8.name() 64 | } 65 | ).apply { 66 | write(document) 67 | close() 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /example/src/main/kotlin/com/xzakota/hook/example/startup/XPInitEntry.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.hook.example.startup 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.content.Context 6 | import com.xzakota.hook.example.BuildConfig 7 | import com.xzakota.hook.example.utils.HookUtils 8 | import com.xzakota.xposed.annotation.ModuleEntry 9 | import de.robv.android.xposed.IXposedHookLoadPackage 10 | import de.robv.android.xposed.IXposedHookZygoteInit 11 | import de.robv.android.xposed.XC_MethodHook 12 | import de.robv.android.xposed.XposedBridge 13 | import de.robv.android.xposed.XposedHelpers 14 | import de.robv.android.xposed.callbacks.XC_LoadPackage 15 | 16 | @ModuleEntry 17 | class XPInitEntry : IXposedHookZygoteInit, IXposedHookLoadPackage { 18 | override fun initZygote(startupParam : IXposedHookZygoteInit.StartupParam) { 19 | log("OnCreate XPModuleMainEntry") 20 | } 21 | 22 | override fun handleLoadPackage(lpparam : XC_LoadPackage.LoadPackageParam) { 23 | log("OnPackageLoaded: " + lpparam.packageName) 24 | log("PackageClassLoader: " + lpparam.classLoader) 25 | log("HostPath: " + lpparam.appInfo.sourceDir) 26 | log("----------") 27 | 28 | if (!lpparam.isFirstApplication) { 29 | return 30 | } 31 | 32 | if (lpparam.packageName == BuildConfig.APPLICATION_ID) { 33 | val utils = XposedHelpers.findClassIfExists(HookUtils::class.java.name, lpparam.classLoader) 34 | XposedHelpers.setStaticBooleanField(utils, "isSelfModuleActivated", true) 35 | XposedHelpers.setStaticIntField(utils, "xposedAPIVersion", XposedBridge.getXposedVersion()) 36 | 37 | return 38 | } 39 | 40 | @SuppressLint("DiscouragedPrivateApi") 41 | val applicationAttachMethod = Application::class.java.getDeclaredMethod("attach", Context::class.java) 42 | XposedBridge.hookMethod(applicationAttachMethod, object : XC_MethodHook() { 43 | override fun beforeHookedMethod(param : MethodHookParam) { 44 | val appContext = param.args[0] 45 | 46 | log("beforeHookedMethod") 47 | log("app context: $appContext") 48 | } 49 | 50 | override fun afterHookedMethod(param : MethodHookParam) { 51 | log("afterHookedMethod") 52 | } 53 | }) 54 | } 55 | 56 | private fun log(text : String) = XposedBridge.log(text) 57 | } 58 | -------------------------------------------------------------------------------- /example/src/main/kotlin/com/xzakota/hook/example/startup/LSPInitEntry.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.hook.example.startup 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.content.Context 6 | import com.xzakota.hook.example.BuildConfig 7 | import com.xzakota.xposed.annotation.ModuleEntry 8 | import io.github.libxposed.api.XposedInterface 9 | import io.github.libxposed.api.XposedModule 10 | import io.github.libxposed.api.XposedModuleInterface 11 | import io.github.libxposed.api.annotations.AfterInvocation 12 | import io.github.libxposed.api.annotations.BeforeInvocation 13 | import io.github.libxposed.api.annotations.XposedHooker 14 | import kotlin.random.Random 15 | 16 | private lateinit var module : XposedModule 17 | 18 | @ModuleEntry 19 | class LSPInitEntry( 20 | base : XposedInterface, 21 | param : XposedModuleInterface.ModuleLoadedParam 22 | ) : XposedModule(base, param) { 23 | init { 24 | module = this 25 | log("OnCreate LSPModuleMainEntry") 26 | } 27 | 28 | override fun onPackageLoaded(param : XposedModuleInterface.PackageLoadedParam) { 29 | log("OnPackageLoaded: " + param.packageName) 30 | log("PackageClassLoader: " + param.classLoader) 31 | log("HostPath: " + applicationInfo.sourceDir) 32 | log("----------") 33 | 34 | if (!param.isFirstPackage) { 35 | return 36 | } 37 | 38 | if (param.packageName == BuildConfig.APPLICATION_ID) { 39 | return 40 | } 41 | 42 | @SuppressLint("DiscouragedPrivateApi") 43 | val applicationAttachMethod = Application::class.java.getDeclaredMethod("attach", Context::class.java) 44 | hook(applicationAttachMethod, MyHooker::class.java) 45 | } 46 | 47 | @XposedHooker 48 | class MyHooker(private val magic : Int) : XposedInterface.Hooker { 49 | companion object { 50 | @JvmStatic 51 | @BeforeInvocation 52 | fun beforeInvocation(callback : XposedInterface.BeforeHookCallback) : MyHooker { 53 | val key = Random.nextInt() 54 | val appContext = callback.args[0] 55 | module.log("beforeInvocation: key = $key") 56 | module.log("app context: $appContext") 57 | return MyHooker(key) 58 | } 59 | 60 | @JvmStatic 61 | @AfterInvocation 62 | fun afterInvocation(callback : XposedInterface.AfterHookCallback, context : MyHooker) { 63 | module.log("afterInvocation: key = ${context.magic}") 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xposed Module Maker 2 | [![GitHub License](https://img.shields.io/github/license/xzakota/XposedModuleMaker?color=blue)](https://github.com/xzakota/XposedModuleMaker/blob/main/LICENSE) 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.xzakota.xposed/annotation?color=green)](https://search.maven.org/search?q=g:com.xzakota.xposed) 4 | 5 | 快速配置 [Xposed](https://api.xposed.info) 模块**元信息**。利用 Gradle 插件实现入口类的检测写入和其他资源的创建合并。 6 | > 仅针对元信息配置,不包含 HOOK 代码实现 7 | 8 | # 支持 9 | - Legacy API 元信息 10 | - [LSP Modern API](https://github.com/LSPosed/LSPosed/wiki/Develop-Xposed-Modules-Using-Modern-Xposed-API) 元信息 11 | 12 | # 使用 13 | 在 app 目录下的 `build.gradle.kts` 添加 14 | 15 | ## 插件及依赖 16 | ``` 17 | plugins { 18 | id("com.xzakota.xposed") version "${version}" 19 | } 20 | 21 | dependencies { 22 | compileOnly("com.xzakota.xposed:annotation:${version}") 23 | } 24 | ``` 25 | 26 | ## 配置一(入口类) 27 | `Legacy API` 28 | 29 | ```Kotlin 30 | import com.xzakota.xposed.annotation.ModuleEntry 31 | import de.robv.android.xposed.IXposedHookLoadPackage 32 | 33 | // 添加注解 34 | @ModuleEntry 35 | class XPInitEntry : IXposedHookLoadPackage 36 | ``` 37 | 38 | `LSP Modern API` 39 | 40 | ```Kotlin 41 | import com.xzakota.xposed.annotation.ModuleEntry 42 | import io.github.libxposed.api.XposedInterface 43 | import io.github.libxposed.api.XposedModule 44 | import io.github.libxposed.api.XposedModuleInterface 45 | 46 | // 添加注解 47 | @ModuleEntry 48 | class LSPInitEntry(base : XposedInterface, param : XposedModuleInterface.ModuleLoadedParam) : XposedModule(base, param) 49 | ``` 50 | 51 | ## 配置二 52 | | 字段 | 作用 | 53 | |:---------------------:|:-----------:| 54 | | isXposedModule | 控制插件的启用/禁用 | 55 | | minAPIVersion | 最低 API 版本 | 56 | | description | 模块介绍 | 57 | | scope | 作用域(需管理器支持) | 58 | | isIncludeDependencies | 是否检索依赖库 | 59 | | isGenerateConfigClass | 是否生成信息类 | 60 | 61 | | 扩展方法 | 作用 | 62 | |:------------:|:---------------------:| 63 | | description | 多国语言的模块介绍(会覆盖字段值) | 64 | | framework | 想支持的框架 | 65 | | lsposed | 针对 LSP Modern API 的配置 | 66 | | scope | 作用域(需管理器支持) | 67 | | resGenerator | 针对资源生成的配置 | 68 | 69 | 具体值按需填写,提供一份详细举例 70 | ``` 71 | xposedModule { 72 | isXposedModule = true 73 | minAPIVersion = XposedAPIVersion.XP_API_82 74 | 75 | // description = "Xposed Example" 76 | description { 77 | // 默认介绍 78 | resString("Xposed Example") 79 | // 中文介绍 80 | resString("一个 Xposed 模块样例", LangCode.LANG_CODE_ZH_CN) 81 | } 82 | 83 | // 默认 XposedFramework.XPOSED 84 | framework { 85 | // 添加 86 | add(XposedFramework.LSPOSED) 87 | // 删除 88 | // remove(XposedFramework.XPOSED) 89 | // 只支持 90 | // only(XposedFramework.LSPOSED) 91 | } 92 | 93 | lsposed { 94 | // 新共享首选项(将在 LSPosed-2.1.0 停止支持) 95 | isNewXSharedPreferences = false 96 | // 目标 API 版本 97 | targetAPIVersion = XposedAPIVersion.XP_API_100 98 | // 静态作用域 99 | isStaticScope = true 100 | } 101 | 102 | resGenerator { 103 | // 资源 ID 名称 104 | resID { 105 | descriptionResID = "xposed_module_description" 106 | scopeResID = "xposed_module_scope" 107 | } 108 | } 109 | 110 | scope += listOf( 111 | "com.android.settings" 112 | ) 113 | 114 | isIncludeDependencies = false 115 | isGenerateConfigClass = true 116 | } 117 | ``` 118 | 另可参考 [example](https://github.com/xzakota/XposedModuleMaker/tree/main/example) 模块 119 | 120 | # TODO 121 | - [ ] Native 入口文件 122 | - [ ] 配套使用的 HOOK 工具库 -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/gradle/plugin/xposed/XposedModuleExtension.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.gradle.plugin.xposed 2 | 3 | import com.xzakota.LangCode 4 | import com.xzakota.android.xposed.XposedAPIVersion 5 | import com.xzakota.android.xposed.XposedFramework 6 | import org.gradle.api.Action 7 | 8 | internal typealias StringList = ArrayList 9 | internal typealias StringRes = HashMap 10 | 11 | @Suppress("unused") 12 | open class XposedModuleExtension { 13 | // 是否为 Xposed 模块 14 | var isXposedModule = true 15 | 16 | // 最低 API 17 | var minAPIVersion = XposedAPIVersion.XP_API_82 18 | 19 | // 模块介绍 20 | var description = "" 21 | internal val descriptionRes = Description() 22 | 23 | // 模块作用域(非原生 Xposed 管理器支持) 24 | val scope = StringList() 25 | 26 | // 支持框架 27 | internal val framework = Framework() 28 | 29 | // LSPosed API 30 | internal val lsposed = LSPosedAPI() 31 | 32 | // R 资源生成控制 33 | internal val resGenerator = ResGenerator() 34 | 35 | // 是否检索依赖库 36 | var isIncludeDependencies = false 37 | 38 | // 是否检索依赖库 39 | var isGenerateConfigClass = false 40 | 41 | fun description(action : Action) { 42 | action.execute(descriptionRes) 43 | } 44 | 45 | fun framework(action : Action) { 46 | action.execute(framework) 47 | } 48 | 49 | fun scope(action : Action) { 50 | action.execute(scope) 51 | } 52 | 53 | fun lsposed(action : Action) { 54 | action.execute(lsposed) 55 | } 56 | 57 | fun resGenerator(action : Action) { 58 | action.execute(resGenerator) 59 | } 60 | 61 | override fun toString() : String = """ 62 | XposedModuleExtension { 63 | isXposedModule => $isXposedModule 64 | minAPIVersion => $minAPIVersion 65 | framework => $framework 66 | scope => $scope 67 | } 68 | """.trimIndent() 69 | 70 | open class Description internal constructor() : StringRes() { 71 | @JvmOverloads 72 | fun resString(value : String, langCode : String = LangCode.LANG_CODE_DEFAULT) { 73 | put(langCode, value) 74 | } 75 | } 76 | 77 | open class Framework internal constructor() { 78 | internal val supportList = mutableListOf(XposedFramework.XPOSED) 79 | 80 | fun add(element : XposedFramework) { 81 | supportList.add(element) 82 | } 83 | 84 | fun remove(element : XposedFramework) { 85 | supportList.remove(element) 86 | } 87 | 88 | fun only(element : XposedFramework) { 89 | supportList.clear() 90 | add(element) 91 | } 92 | 93 | fun isEmpty() : Boolean = supportList.isEmpty() 94 | 95 | internal infix fun has(element : XposedFramework) : Boolean = supportList.contains(element) 96 | 97 | override fun toString() : String = supportList.toString() 98 | } 99 | 100 | open class LSPosedAPI internal constructor() { 101 | /** 102 | * 新 XSharedPreferences 103 | * 104 | * 将在 LSPosed-2.1.0 停止支持 105 | */ 106 | var isNewXSharedPreferences = false 107 | 108 | // 目标 API 109 | var targetAPIVersion = XposedAPIVersion.XP_API_82 110 | 111 | // 静态作用域 112 | var isStaticScope = false 113 | } 114 | 115 | open class ResGenerator internal constructor() { 116 | // 资源 ID 117 | internal val resID = ResID(ResID.ID_DESCRIPTION, ResID.ID_SCOPE) 118 | 119 | fun resID(action : Action) { 120 | action.execute(resID) 121 | } 122 | 123 | open class ResID internal constructor(var descriptionResID : String, var scopeResID : String) { 124 | companion object { 125 | const val ID_DESCRIPTION = "__xposed_module_description" 126 | const val ID_SCOPE = "__xposed_module_scope" 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/gradle/plugin/xposed/GenerateModuleManifestTask.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.gradle.plugin.xposed 2 | 3 | import com.xzakota.android.AndroidXML.NAMESPACE_ANDROID 4 | import com.xzakota.android.AndroidXML.QUALIFIED_NAME_DESCRIPTION 5 | import com.xzakota.android.AndroidXML.QUALIFIED_NAME_NAME 6 | import com.xzakota.android.AndroidXML.QUALIFIED_NAME_RESOURCE 7 | import com.xzakota.android.AndroidXML.QUALIFIED_NAME_VALUE 8 | import com.xzakota.extension.addElement 9 | import com.xzakota.extension.addNamespace 10 | import com.xzakota.gradle.plugin.xposed.DataProvider.isSupportLSPosed 11 | import com.xzakota.gradle.plugin.xposed.DataProvider.isSupportXposed 12 | import org.dom4j.DocumentHelper 13 | import org.dom4j.Element 14 | import org.dom4j.io.OutputFormat 15 | import org.dom4j.io.XMLWriter 16 | import org.gradle.api.file.RegularFileProperty 17 | import org.gradle.api.tasks.OutputFile 18 | 19 | abstract class GenerateModuleManifestTask : BaseModuleGenerateTask() { 20 | @get:OutputFile 21 | abstract val outputFile : RegularFileProperty 22 | 23 | init { 24 | description = "Generate module manifest" 25 | } 26 | 27 | override fun onAction() { 28 | val resID = moduleResGenerator.resID 29 | val document = DocumentHelper.createDocument() 30 | 31 | document.addElement("manifest") { 32 | addNamespace(NAMESPACE_ANDROID) 33 | 34 | addElement("application") { 35 | val isSupportXposed = isSupportXposed(xposedModuleConfig) 36 | val isSupportLSPosed = isSupportLSPosed(xposedModuleConfig) 37 | 38 | if (isSupportLSPosed) { 39 | addAttribute(QUALIFIED_NAME_DESCRIPTION, "@string/${resID.descriptionResID}") 40 | } 41 | 42 | addMetaDataElementWithValue("xposedmodule", "true", isSupportXposed) 43 | 44 | addMetaDataElement("xposeddescription", isSupportXposed) { 45 | if (moduleDescriptionRes.isNotEmpty()) { 46 | it.addAttribute(QUALIFIED_NAME_RESOURCE, "@string/${resID.descriptionResID}") 47 | } else { 48 | it.addAttribute(QUALIFIED_NAME_VALUE, moduleDescription) 49 | } 50 | } 51 | 52 | addMetaDataElementWithValue("xposedminversion", moduleMinAPIVersion, isSupportXposed) 53 | 54 | addMetaDataElementWithResource( 55 | "xposedscope", 56 | "@array/${resID.scopeResID}", 57 | isSupportXposed && moduleScope.isNotEmpty() 58 | ) 59 | 60 | addMetaDataElementWithResource( 61 | "xposedsharedprefs", 62 | "true", 63 | isSupportXposed && isSupportLSPosed && xposedModuleConfig.lsposed.isNewXSharedPreferences 64 | ) 65 | } 66 | } 67 | 68 | XMLWriter( 69 | outputFile.get().asFile.writer(), 70 | OutputFormat.createPrettyPrint().apply { 71 | encoding = Charsets.UTF_8.name() 72 | } 73 | ).apply { 74 | write(document) 75 | close() 76 | } 77 | } 78 | 79 | private fun Element.addMetaDataElementWithValue( 80 | name : String, 81 | value : Any, 82 | isInvokeAction : Boolean = true 83 | ) = addMetaDataElement(name, isInvokeAction) { 84 | it.addAttribute(QUALIFIED_NAME_VALUE, value.toString()) 85 | } 86 | 87 | private fun Element.addMetaDataElementWithResource( 88 | name : String, 89 | resource : String, 90 | isInvokeAction : Boolean = true 91 | ) = addMetaDataElement(name, isInvokeAction) { 92 | it.addAttribute(QUALIFIED_NAME_RESOURCE, resource) 93 | } 94 | 95 | private fun Element.addMetaDataElement( 96 | name : String, 97 | isInvokeAction : Boolean = true, 98 | action : (Element) -> Unit 99 | ) { 100 | if (!isInvokeAction) { 101 | return 102 | } 103 | 104 | val element = addElement("meta-data") { 105 | addAttribute(QUALIFIED_NAME_NAME, name) 106 | } 107 | 108 | action(element) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/gradle/plugin/xposed/GenerateModuleEntryTask.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.gradle.plugin.xposed 2 | 3 | import com.joom.grip.ClassRegistry 4 | import com.joom.grip.GripFactory 5 | import com.joom.grip.io.FileSource 6 | import com.joom.grip.io.IoFactory 7 | import com.joom.grip.mirrors.ClassMirror 8 | import com.joom.grip.mirrors.getObjectType 9 | import com.joom.grip.mirrors.getObjectTypeByInternalName 10 | import com.xzakota.extension.createDirectory 11 | import com.xzakota.extension.createFile 12 | import com.xzakota.extension.safeMkdirs 13 | import com.xzakota.xposed.annotation.ModuleEntry 14 | import org.gradle.api.file.Directory 15 | import org.gradle.api.file.DirectoryProperty 16 | import org.gradle.api.file.RegularFile 17 | import org.gradle.api.file.RegularFileProperty 18 | import org.gradle.api.provider.ListProperty 19 | import org.gradle.api.tasks.InputFiles 20 | import org.gradle.api.tasks.OutputDirectory 21 | import org.gradle.api.tasks.OutputFile 22 | import org.gradle.api.tasks.PathSensitive 23 | import org.gradle.api.tasks.PathSensitivity 24 | import org.objectweb.asm.Opcodes 25 | import java.io.BufferedOutputStream 26 | import java.io.File 27 | import java.io.FileOutputStream 28 | import java.util.jar.JarOutputStream 29 | 30 | abstract class GenerateModuleEntryTask : BaseModuleGenerateTask() { 31 | @get:InputFiles 32 | @get:PathSensitive(PathSensitivity.RELATIVE) 33 | abstract val allJars : ListProperty 34 | 35 | @get:InputFiles 36 | @get:PathSensitive(PathSensitivity.RELATIVE) 37 | abstract val allDirectories : ListProperty 38 | 39 | @get:OutputFile 40 | abstract val classOutput : RegularFileProperty 41 | 42 | @get:OutputDirectory 43 | abstract val entryOutput : DirectoryProperty 44 | 45 | init { 46 | description = "Generate module entry file" 47 | } 48 | 49 | override fun onAction() { 50 | val inputs = (allJars.get() + allDirectories.get()).map { it.asFile.toPath() } 51 | val grip = GripFactory.newInstance(Opcodes.ASM9).create(inputs) 52 | 53 | JarOutputStream(BufferedOutputStream(FileOutputStream(classOutput.get().asFile))).use { jarOutput -> 54 | val sources = inputs.asSequence().map { input -> 55 | IoFactory.createFileSource(input) 56 | } 57 | 58 | try { 59 | sources.forEach { source -> 60 | source.listFiles { name, type -> 61 | when (type) { 62 | FileSource.EntryType.CLASS -> processClass(grip.classRegistry, source, jarOutput, name) 63 | FileSource.EntryType.FILE -> jarOutput.createFile(name, source.readFile(name)) 64 | FileSource.EntryType.DIRECTORY -> jarOutput.createDirectory(name) 65 | } 66 | } 67 | } 68 | } finally { 69 | sources.forEach { 70 | it.close() 71 | } 72 | } 73 | } 74 | 75 | var entryDir = File(entryOutput.get().asFile, "assets") 76 | var entryFile = File(entryDir, "xposed_init") 77 | if (DataProvider.isSupportXposed(xposedModuleConfig)) { 78 | entryDir.safeMkdirs() 79 | entryFile.writeText(DataProvider.xposedEntryClassName) 80 | } else { 81 | entryFile.delete() 82 | } 83 | 84 | entryDir = File(entryOutput.get().asFile, "META-INF/xposed") 85 | entryFile = File(entryDir, "java_init.list") 86 | if (DataProvider.isSupportLSPosed(xposedModuleConfig)) { 87 | entryDir.safeMkdirs() 88 | entryFile.writeText(DataProvider.lsposedEntryClassName) 89 | } else { 90 | entryFile.delete() 91 | } 92 | } 93 | 94 | private fun processClass( 95 | classRegistry : ClassRegistry, 96 | source : FileSource, 97 | jarOutput : JarOutputStream, 98 | name : String 99 | ) { 100 | if (!name.endsWith(".class")) { 101 | return 102 | } 103 | 104 | val pathName = name.substringBeforeLast(".class").replace('\\', '/') 105 | val type = getObjectTypeByInternalName(pathName) 106 | val classMirror = classRegistry.getClassMirror(type) 107 | 108 | if (ANNOTATION_XPOSED_MODULE_ENTRY in classMirror.annotations) { 109 | if (isLSPosedModuleEntry(classRegistry, classMirror)) { 110 | DataProvider.lsposedEntryClassName = pathName.replace("/", ".") 111 | } else { 112 | DataProvider.xposedEntryClassName = pathName.replace("/", ".") 113 | } 114 | } 115 | 116 | jarOutput.createFile(name, source.readFile(name)) 117 | } 118 | 119 | private fun isLSPosedModuleEntry(classRegistry : ClassRegistry, classMirror : ClassMirror) : Boolean { 120 | var type = classMirror.superType 121 | while (type != null && type != TYPE_ANY) { 122 | if (type == TYPE_XPOSED_MODULE) { 123 | return true 124 | } 125 | 126 | type = classRegistry.getClassMirror(type).superType 127 | } 128 | 129 | return false 130 | } 131 | 132 | private companion object { 133 | val TYPE_ANY = getObjectType(Any::class.java) 134 | val TYPE_XPOSED_MODULE = getObjectType("Lio/github/libxposed/api/XposedModule;") 135 | val ANNOTATION_XPOSED_MODULE_ENTRY = getObjectType(ModuleEntry::class.java) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/com/xzakota/gradle/plugin/xposed/GradlePluginForXposed.kt: -------------------------------------------------------------------------------- 1 | package com.xzakota.gradle.plugin.xposed 2 | 3 | import com.android.build.api.artifact.ScopedArtifact 4 | import com.android.build.api.variant.AndroidComponentsExtension 5 | import com.android.build.api.variant.ScopedArtifacts.Scope 6 | import com.android.build.api.variant.Variant 7 | import com.android.build.gradle.AppExtension 8 | import com.android.build.gradle.AppPlugin 9 | import com.xzakota.android.xposed.XposedAPIVersion 10 | import com.xzakota.extension.addDependencies 11 | import com.xzakota.extension.capitalizeFirstChar 12 | import com.xzakota.xposed.BuildConfig 13 | import org.gradle.api.Plugin 14 | import org.gradle.api.Project 15 | import org.gradle.kotlin.dsl.get 16 | 17 | @Suppress("unused") 18 | class GradlePluginForXposed : Plugin { 19 | override fun apply(project : Project) { 20 | val isAndroidAppProject = project.plugins.hasPlugin(AppPlugin::class.java) 21 | if (!isAndroidAppProject) { 22 | return 23 | } 24 | 25 | registerTask(project, project.extensions.create("xposedModule", XposedModuleExtension::class.java)) 26 | } 27 | 28 | private fun registerTask(project : Project, moduleConfig : XposedModuleExtension) { 29 | project.extensions.getByType(AndroidComponentsExtension::class.java).onVariants { variant : Variant -> 30 | if (!moduleConfig.isXposedModule) { 31 | return@onVariants 32 | } 33 | 34 | val tasks = project.tasks 35 | val variantTitleName = variant.name.capitalizeFirstChar() 36 | 37 | var taskName = "generate${variantTitleName}XposedModuleManifest" 38 | if (tasks.findByPath(taskName) == null) { 39 | val task = tasks.register(taskName, GenerateModuleManifestTask::class.java) 40 | variant.sources.manifests.addGeneratedManifestFile( 41 | task, GenerateModuleManifestTask::outputFile 42 | ) 43 | } 44 | 45 | if (DataProvider.isSupportLSPosed(moduleConfig)) { 46 | taskName = "generate${variantTitleName}XposedModuleProperties" 47 | if (tasks.findByPath(taskName) == null) { 48 | val task = tasks.register(taskName, GenerateModulePropertiesTask::class.java) 49 | variant.sources.resources?.addGeneratedSourceDirectory( 50 | task, GenerateModulePropertiesTask::outputDir 51 | ) 52 | } 53 | 54 | if (moduleConfig.lsposed.targetAPIVersion >= XposedAPIVersion.XP_API_100) { 55 | variant.packaging.resources.merges.add("META-INF/xposed/*") 56 | } 57 | } 58 | 59 | taskName = "generate${variantTitleName}XposedEntryClass" 60 | if (tasks.findByPath(taskName) == null) { 61 | val task = tasks.register(taskName, GenerateModuleEntryTask::class.java) 62 | variant.artifacts.forScope( 63 | if (moduleConfig.isIncludeDependencies) { 64 | Scope.ALL 65 | } else { 66 | Scope.PROJECT 67 | } 68 | ).use(task).toTransform( 69 | ScopedArtifact.CLASSES, 70 | GenerateModuleEntryTask::allJars, 71 | GenerateModuleEntryTask::allDirectories, 72 | GenerateModuleEntryTask::classOutput 73 | ) 74 | 75 | variant.sources.resources?.addGeneratedSourceDirectory( 76 | task, GenerateModuleEntryTask::entryOutput 77 | ) 78 | } 79 | } 80 | 81 | val androidExtensions = project.extensions.getByType(AppExtension::class.java) 82 | project.afterEvaluate { 83 | if (!moduleConfig.isXposedModule) { 84 | return@afterEvaluate 85 | } 86 | 87 | DataProvider.moduleConfig[project.name] = moduleConfig 88 | 89 | val projectGeneratedDir = project.layout.buildDirectory.dir("generated") 90 | val generatedBaseDir = projectGeneratedDir.get().dir("xposed") 91 | 92 | androidExtensions.applicationVariants.forEach { variant -> 93 | val variantDirName = variant.dirName 94 | val variantTitleName = variant.name.capitalizeFirstChar() 95 | 96 | var taskName = "generate${variantTitleName}XposedModuleResource" 97 | if (tasks.findByPath(taskName) == null) { 98 | val generatedResDir = generatedBaseDir.dir("resValues/$variantDirName") 99 | val task = tasks.register(taskName, GenerateModuleResTask::class.java) { 100 | outputDir.set(generatedResDir) 101 | } 102 | variant.registerGeneratedResFolders(project.files(generatedResDir).builtBy(task)) 103 | } 104 | 105 | if (moduleConfig.isGenerateConfigClass) { 106 | taskName = "generate${variantTitleName}XposedModuleConfigClass" 107 | if (tasks.findByPath(taskName) == null) { 108 | val generatedJavaDir = generatedBaseDir.dir("source/$variantDirName").asFile 109 | val task = tasks.register(taskName, GenerateModuleConfigClassTask::class.java) { 110 | packageName = variant.applicationId 111 | outputs.dir(generatedJavaDir) 112 | } 113 | 114 | val kspTask = tasks.findByPath("ksp${variantTitleName}Kotlin") 115 | if (kspTask != null) { 116 | task.get().mustRunAfter(kspTask) 117 | } 118 | 119 | val compileKotlinTask = tasks.findByPath("compile${variantTitleName}Kotlin") 120 | if (compileKotlinTask != null) { 121 | compileKotlinTask.dependsOn(task) 122 | } else { 123 | tasks.getByPath("compile${variantTitleName}JavaWithJavac").dependsOn(task) 124 | } 125 | 126 | variant.registerJavaGeneratingTask(task, generatedJavaDir) 127 | androidExtensions.sourceSets[variant.name].apply { 128 | java.setSrcDirs(java.srcDirs + generatedJavaDir) 129 | } 130 | } 131 | 132 | project.addDependencies("com.xzakota.xposed:constant", BuildConfig.VERSION) 133 | } 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. --------------------------------------------------------------------------------