From 15a5653d3488173ea3b3754a7765d7f9634df98f Mon Sep 17 00:00:00 2001 From: foolishchow Date: Fri, 21 May 2021 11:16:37 +0800 Subject: [PATCH] navigation-transform --- .gitignore | 1 + app/build.gradle | 2 +- .../foolishchow/autoparamdemo/MainActivity.kt | 1 - app/src/main/res/navigation/main.xml | 2 +- app/src/main/res/raw/discard.xml | 4 - .../navigationprocessor/NavigationPlugin.kt | 83 ++--- .../navigationprocessor/NavigationTask.kt | 138 +++++--- .../android/navigationprocessor/RuleEditor.kt | 111 +++++++ .../navigationprocessor/extensions/Project.kt | 13 +- navigation/.gitignore | 1 + navigation/build.gradle.kts | 23 ++ .../android/plugin/navigation/ClassName.kt | 60 ++++ .../plugin/navigation/NavigationPlugin.kt | 86 +++++ .../plugin/navigation/NavigationTask.kt | 300 ++++++++++++++++++ .../android/plugin/navigation/RuleEditor.kt | 111 +++++++ .../plugin/navigation/extensions/File.kt | 35 ++ .../plugin/navigation/extensions/Project.kt | 32 ++ .../plugin/navigation/extensions/Resource.kt | 36 +++ .../plugin/navigation/extensions/String.kt | 44 +++ .../plugin/navigation/extensions/XmlParser.kt | 108 +++++++ .../navigation-transform.properties | 1 + settings.gradle | 2 +- 22 files changed, 1077 insertions(+), 117 deletions(-) delete mode 100644 app/src/main/res/raw/discard.xml create mode 100644 buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/RuleEditor.kt create mode 100644 navigation/.gitignore create mode 100644 navigation/build.gradle.kts create mode 100644 navigation/src/main/java/me/foolishchow/android/plugin/navigation/ClassName.kt create mode 100644 navigation/src/main/java/me/foolishchow/android/plugin/navigation/NavigationPlugin.kt create mode 100644 navigation/src/main/java/me/foolishchow/android/plugin/navigation/NavigationTask.kt create mode 100644 navigation/src/main/java/me/foolishchow/android/plugin/navigation/RuleEditor.kt create mode 100644 navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/File.kt create mode 100644 navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/Project.kt create mode 100644 navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/Resource.kt create mode 100644 navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/String.kt create mode 100644 navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/XmlParser.kt create mode 100644 navigation/src/main/resources/META-INF/gradle-plugins/navigation-transform.properties diff --git a/.gitignore b/.gitignore index 42ef4b2..41e812b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ backup/ .idea /multi-module/build/ /view-binding/build/ +/buildSrc/build/ diff --git a/app/build.gradle b/app/build.gradle index 62ba7ae..4c8a6f9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,7 +46,7 @@ android { } debug { - shrinkResources true + //shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } diff --git a/app/src/main/java/me/foolishchow/autoparamdemo/MainActivity.kt b/app/src/main/java/me/foolishchow/autoparamdemo/MainActivity.kt index ecb0490..a14f1e2 100644 --- a/app/src/main/java/me/foolishchow/autoparamdemo/MainActivity.kt +++ b/app/src/main/java/me/foolishchow/autoparamdemo/MainActivity.kt @@ -16,7 +16,6 @@ open class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) val controller = navController NavigationManager.attach(controller, R.navigation.main) - println("") //val navDestinations = MainActivity(controller) //val inflate = controller!!.navInflater.inflate(R.navigation.main) //val displayName = inflate.navigatorName diff --git a/app/src/main/res/navigation/main.xml b/app/src/main/res/navigation/main.xml index 6c270e6..906202f 100644 --- a/app/src/main/res/navigation/main.xml +++ b/app/src/main/res/navigation/main.xml @@ -15,5 +15,5 @@ + android:label="ChildFragment1" /> \ No newline at end of file diff --git a/app/src/main/res/raw/discard.xml b/app/src/main/res/raw/discard.xml deleted file mode 100644 index 1a26efb..0000000 --- a/app/src/main/res/raw/discard.xml +++ /dev/null @@ -1,4 +0,0 @@ - - \ No newline at end of file diff --git a/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/NavigationPlugin.kt b/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/NavigationPlugin.kt index 8c18aa1..de34e65 100644 --- a/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/NavigationPlugin.kt +++ b/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/NavigationPlugin.kt @@ -2,22 +2,11 @@ package me.foolishchow.android.navigationprocessor import com.android.build.gradle.api.ApplicationVariant import com.android.build.gradle.internal.dsl.BaseAppModuleExtension -import me.foolishchow.android.navigationprocessor.extensions.AaptRules -import me.foolishchow.android.navigationprocessor.extensions.NavigationTaskName -import me.foolishchow.android.navigationprocessor.extensions.navigationDir -import org.gradle.api.DefaultTask +import me.foolishchow.android.navigationprocessor.extensions.* import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.OutputFiles -import org.gradle.api.tasks.TaskAction import org.gradle.kotlin.dsl.closureOf -import java.io.BufferedReader -import java.io.File -import java.io.FileReader +import java.io.* /** * Description: @@ -34,98 +23,64 @@ class NavigationPlugin : Plugin { val android: BaseAppModuleExtension = project.extensions.getByName("android") as BaseAppModuleExtension android.applicationVariants.configureEach { - postProcessProguardRules(this, project) + registerPostProcessProguardRules(this, project) registerNavigationTask(this, project) } } - private fun registerNavigationTask(variant: ApplicationVariant, project: Project) { - val android: BaseAppModuleExtension = project.extensions + private fun registerNavigationTask(variant: ApplicationVariant, _project: Project) { + val android: BaseAppModuleExtension = _project.extensions .getByName("android") as BaseAppModuleExtension val sourceSet = variant.sourceSets.find { it.name == variant.name } ?: return android.sourceSets.forEach { source -> if (source.name == sourceSet.name) { - source.java.srcDir(project.navigationDir(variant)) + source.java.srcDir(_project.navJavaDir(variant)) + source.res.srcDir(_project.navResDir(variant)) } } - val task = project.tasks.create(mapOf( + val task = _project.tasks.create(mapOf( "name" to variant.NavigationTaskName, "group" to "auto-param", "type" to NavigationTask::class.java ), closureOf { val files = mutableListOf() + var lastModified = -1L variant.sourceSets.forEach { sourceSet -> sourceSet.resDirectories.forEach { res -> project.fileTree(File(res, "navigation")).forEach { file -> files.add(file) + lastModified = file.lastModified().coerceAtLeast(lastModified) } } } - this.inputs.property("variant", variant.name) - this.inputs.files(files.toTypedArray()) - this.outputs.dir("${project.buildDir}/generated/source/navigation/${variant.name}") - .withPropertyName("outputDir") + inputs.property("lastModified",lastModified) + inputs.files(files.toTypedArray()) + resDir = project.file(project.navResDir(variant)) + javaDir = project.file(project.navJavaDir(variant)) + ruleFile = project.file(project.navRuleFile(variant)) }) val preBuild = "pre${variant.name.capitalize()}Build" - project.tasks.findByName(preBuild)?.dependsOn(task) + _project.tasks.findByName(preBuild)?.dependsOn(task) } - private fun postProcessProguardRules(variant: ApplicationVariant, project: Project) { + private fun registerPostProcessProguardRules(variant: ApplicationVariant, _project: Project) { variant.outputs.forEach { output -> val taskName = "process${variant.name.capitalize()}Resources" - val task = project.tasks.findByName(taskName) + val task = _project.tasks.findByName(taskName) task?.let { output.processResourcesProvider.orNull?.doLast { - val rulesPath = project.AaptRules(variant) - val rules = Rules() - - val stream = BufferedReader(FileReader(rulesPath)) - var str: String? - var line = 0 - while (stream.readLine().also { str = it } != null) { - rules.add(str, line) - line++ - } - rules.add(str, line) - rules.rules.forEach { rule -> - println(rule.className) - } + editAaptRule(project, variant) } } } } -} -class Rules { - val rules = mutableListOf() - var rule = Rule() - - init { - rules.add(rule) - } - - fun add(str: String?, line: Int) { - if (str.isNullOrBlank()) { - rule = Rule() - rules.add(rule) - } else if (str.startsWith("# Referenced at")) { - rule.references.add(str) - } else if (str.startsWith("-keep class ")) { - rule.className = str - rule.line = line - } - } } -class Rule { - var line = -1 - var className: String? = null - val references = mutableListOf() -} diff --git a/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/NavigationTask.kt b/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/NavigationTask.kt index 25cd04f..3d70f12 100644 --- a/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/NavigationTask.kt +++ b/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/NavigationTask.kt @@ -7,74 +7,85 @@ import com.squareup.javapoet.MethodSpec import com.squareup.javapoet.TypeSpec import groovy.util.Node import groovy.util.XmlParser -import me.foolishchow.android.navigationprocessor.extensions.navigationDir import me.foolishchow.android.navigationprocessor.extensions.resId import me.foolishchow.android.navigationprocessor.extensions.resourceSymbol import me.foolishchow.android.navigationprocessor.extensions.snake2camel +import org.apache.tools.ant.taskdefs.condition.IsLastModified import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.transform.InputArtifact import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.* +import java.io.BufferedWriter import java.io.File +import java.io.FileWriter import javax.lang.model.element.Modifier +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult abstract class NavigationTask : DefaultTask() { + + @InputFiles abstract fun getNavFiles(): ConfigurableFileCollection - private var outputDir:File? = null + @get:OutputDirectory + lateinit var javaDir: File - @OutputDirectory - open fun getOutputDir(): File? { - return outputDir - } + @get:OutputDirectory + lateinit var resDir: File - private lateinit var android: BaseAppModuleExtension - private lateinit var packageName: String - private lateinit var mVariantName: String + @get:OutputFile + lateinit var ruleFile: File - private lateinit var mDir: File private lateinit var mClass: TypeSpec.Builder - private val mMaps = mutableMapOf() + + /** + * 缓存当前所有的文件名和方法名 + */ + private val mFileNameWithMethodName = mutableMapOf() private lateinit var mResource: ClassName @TaskAction open fun perform() { - android = project.extensions.getByName("android") as BaseAppModuleExtension - packageName = android.defaultConfig.applicationId + val android = project.extensions.getByName("android") as BaseAppModuleExtension + val packageName = android.defaultConfig.applicationId - mVariantName = inputs.properties["variant"] as String + project.delete(javaDir.listFiles()) + project.delete(resDir.listFiles()) + project.delete(ruleFile) - android.applicationVariants.forEach{ variant-> - val file = File(project.navigationDir(variant)) - if (variant.name.equals(mVariantName)) { - mDir = file - } - } + parseFiles(packageName) + + createDiscard() + + crateFileNames() + + } + + + private fun parseFiles(packageName: String?) { mResource = ClassName.get(packageName, "R") mClass = TypeSpec.classBuilder("NavigationManager") - //.superclass(NavGraph) .addModifiers(Modifier.PUBLIC) inputs.files.forEach { file -> parseAndGenerate(file) } - val method = MethodSpec.methodBuilder("attach") .addParameter(NavController, "controller") .addParameter(ClassName.INT, "navigationId") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .beginControlFlow("switch(navigationId)") - - - mMaps.forEach { item -> - method.addStatement("case \$T.navigation.${item.key}:", mResource) - method.addStatement("${item.value}(controller)") + mFileNameWithMethodName.forEach { item -> + method.addStatement("case \$T.navigation.${item.key.xmlName}:", mResource) + method.addStatement(" ${item.value}(controller)") method.addStatement("break") } method.endControlFlow() @@ -82,17 +93,17 @@ abstract class NavigationTask : DefaultTask() { val javaFile = JavaFile.builder("$packageName.navigation", mClass.build()) .build() - if (!mDir.exists()) { - mDir.mkdirs() + if (!javaDir.exists()) { + javaDir.mkdirs() } - javaFile.writeTo(mDir) + javaFile.writeTo(javaDir) } private fun parseAndGenerate(file: File) { val name = file.xmlName.snake2camel() val methodName = "attachNavigation$name" - mMaps[file.xmlName] = methodName + mFileNameWithMethodName[file] = methodName val method = MethodSpec.methodBuilder(methodName) .addParameter(NavController, "controller") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) @@ -118,7 +129,6 @@ abstract class NavigationTask : DefaultTask() { if (attr.key.isAndroidLabel) { method.addStatement("graph.setLabel(\"${attr.value}\")", mResource) } - println(attr.value) } @@ -147,7 +157,8 @@ abstract class NavigationTask : DefaultTask() { method.addStatement("fragment.setId(\$T.id.${attr.value.resId})", mResource) } attr.key.isAndroidName -> { - method.addStatement("fragment.setClassName(${attr.value}.class.getName())") + val type = ClassName.bestGuess(attr.value) + method.addStatement("fragment.setClassName(\$T.class.getName())", type) } attr.key.isAndroidLabel -> { method.addStatement("fragment.setLabel(\"${attr.value}\")") @@ -230,15 +241,60 @@ abstract class NavigationTask : DefaultTask() { ) } - method.addStatement("action = new \$T(\$T.id.$destId,builder.build())", - NavAction, mResource) - method.addStatement("fragment.putAction(\$T.id.${actionId},action)", - mResource) + method.addStatement( + "action = new \$T(\$T.id.$destId,builder.build())", + NavAction, mResource + ) + method.addStatement( + "fragment.putAction(\$T.id.${actionId},action)", mResource + ) + } + + + private fun crateFileNames() { + ruleFile.delete() + val fw = FileWriter(ruleFile.absoluteFile) + val bw = BufferedWriter(fw) + mFileNameWithMethodName.forEach { attr -> + bw.write(attr.key.absolutePath) + bw.newLine() + } + bw.close() + } + + private fun createDiscard() { + val document = DocumentBuilderFactory + .newInstance() + .newDocumentBuilder() + .newDocument() + + document.xmlStandalone = false + + val resources = document.createElement("resources") + resources.setAttribute("xmlns:tools", "http://schemas.android.com/tools") + val discards = mutableListOf() + mFileNameWithMethodName.forEach { item -> + discards.add("@navigation/${item.key.xmlName}") + } + resources.setAttribute("tools:discard", discards.joinToString(separator = ",") { it }) + document.appendChild(resources) + + val rawDir = File(resDir, "raw") + if (!rawDir.exists()) { + rawDir.mkdirs() + } + val discardFile = File(rawDir, "nav_discard.xml") + + val transformer = TransformerFactory.newInstance().newTransformer() + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2") + //是否自动换行 + transformer.setOutputProperty(OutputKeys.INDENT, "yes") + transformer.transform(DOMSource(document), StreamResult(discardFile)) } protected fun getIncremental(): Boolean { - return true + return false } } \ No newline at end of file diff --git a/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/RuleEditor.kt b/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/RuleEditor.kt new file mode 100644 index 0000000..0d992ae --- /dev/null +++ b/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/RuleEditor.kt @@ -0,0 +1,111 @@ +package me.foolishchow.android.navigationprocessor + +import com.android.build.gradle.api.ApplicationVariant +import me.foolishchow.android.navigationprocessor.extensions.AaptRules +import me.foolishchow.android.navigationprocessor.extensions.navRuleFile +import org.gradle.api.Project +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.FileReader +import java.io.FileWriter + +/** + * Description: + * Author: foolishchow + * Date: 2021/05/21 10:53 AM + */ +class Rules { + val rules = mutableListOf() + var rule = Rule() + + init { + rules.add(rule) + } + + fun add(str: String?, line: Int) { + if (str.isNullOrBlank()) { + rule = Rule() + rules.add(rule) + } else if (str.startsWith("# Referenced at")) { + rule.references.add(str) + } else if (str.startsWith("-keep class ")) { + rule.className = str + rule.line = line + } + } +} + +class Rule { + var line = -1 + var className: String? = null + val references = mutableListOf() +} + + +fun editAaptRule(project: Project, variant: ApplicationVariant) { + val rulesPath = project.AaptRules(variant) + val rulesWrapper = Rules() + + + //region 读取 aapt_rules.txt + val ruleContent = mutableListOf() + + val stream = BufferedReader(FileReader(rulesPath)) + var str: String? + var line = 1 + while (stream.readLine().also { str = it } != null) { + rulesWrapper.add(str, line) + ruleContent.add(str ?: "") + line++ + } + rulesWrapper.add(str, line) + + val rules = rulesWrapper.rules + //endregion + + + //region 读取 navigation/${variant.name}/navigation.txt + val navNameFile = project.navRuleFile(variant) + val navFiles = mutableListOf() + val reader = BufferedReader(FileReader(navNameFile)) + while (reader.readLine().also { str = it } != null) { + str?.let { navFiles.add(it) } + } + //endregion + + + //region 获取需要被注释的line + rules.forEach { rule -> + val ref = rule.references.filter { it -> + var has = false + for (navFile in navFiles) { + if (it.startsWith("# Referenced at $navFile")) { + has = true + break + } + } + !has + } + rule.references.clear() + rule.references.addAll(ref) + } + + val needToCommentLines = rules.map(fun(it: Rule): Int? { + if (it.className != null && it.references.size == 0) return it.line + return null + }).filterNotNull() + //endregion + + val fw = FileWriter(rulesPath) + val bw = BufferedWriter(fw) + ruleContent.forEachIndexed { index, lineContent -> + if (needToCommentLines.contains(index + 1)) { + bw.write("# $lineContent") + } else { + bw.write(lineContent) + } + bw.newLine() + } + bw.close() + +} \ No newline at end of file diff --git a/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/extensions/Project.kt b/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/extensions/Project.kt index a6a4712..0bfa968 100644 --- a/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/extensions/Project.kt +++ b/buildSrc/src/main/java/me/foolishchow/android/navigationprocessor/extensions/Project.kt @@ -9,12 +9,17 @@ import org.gradle.api.Project * Author: foolishchow * Date: 2021/05/20 9:05 AM */ -fun Project.navigationDir(buildType: BuildType): String { - return "${project.buildDir}/generated/source/navigation/${buildType.name}" + +fun Project.navRuleFile(variant: ApplicationVariant):String{ + return "${project.buildDir}/intermediates/navigation/${variant.name}/navigation.txt" +} + +fun Project.navJavaDir(variant: ApplicationVariant): String { + return "${project.buildDir}/generated/source/navigation/${variant.name}/java" } -fun Project.navigationDir(variant: ApplicationVariant): String { - return "${project.buildDir}/generated/source/navigation/${variant.name}" +fun Project.navResDir(variant: ApplicationVariant): String { + return "${project.buildDir}/generated/source/navigation/${variant.name}/res" } fun Project.AaptRules(variant: ApplicationVariant): String { diff --git a/navigation/.gitignore b/navigation/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/navigation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/navigation/build.gradle.kts b/navigation/build.gradle.kts new file mode 100644 index 0000000..2a96b3b --- /dev/null +++ b/navigation/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `kotlin-dsl` + `maven` + `java` +} + +repositories { + google() + jcenter() +} + +java{ + sourceCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + implementation(gradleApi()) + implementation(localGroovy()) + implementation("com.squareup:javapoet:1.11.1") + implementation("com.android.tools.build:gradle:3.6.3") +} + +group = "com.github.foolishchow" \ No newline at end of file diff --git a/navigation/src/main/java/me/foolishchow/android/plugin/navigation/ClassName.kt b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/ClassName.kt new file mode 100644 index 0000000..d53d2fa --- /dev/null +++ b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/ClassName.kt @@ -0,0 +1,60 @@ +package me.foolishchow.android.plugin.navigation + +import com.squareup.javapoet.ClassName +import com.squareup.javapoet.ParameterizedTypeName +import com.squareup.javapoet.TypeName +import com.squareup.javapoet.WildcardTypeName + +/** + * Description: + * Author: foolishchow + * Date: 2021/05/19 2:26 PM + */ + +val Navigator: ParameterizedTypeName = ParameterizedTypeName.get( + ClassName.get("androidx.navigation", "Navigator"), + WildcardTypeName.subtypeOf(TypeName.OBJECT) +) + +val NavAction: ClassName = ClassName.get( + "androidx.navigation", + "NavAction" +) + + +val NavigationUtil: ClassName = ClassName.get("me.foolishchow.androidplugins.fake", "NavigationUtils") +val NavigatorProvider: ClassName = ClassName.get("androidx.navigation", "NavigatorProvider") + + +val NavController: ClassName = ClassName.get( + "androidx.navigation", + "NavController") + +val FragmentNavigator: ClassName = ClassName.get( + "androidx.navigation.fragment", + "FragmentNavigator" +) +val FragmentDestination: ClassName = ClassName.get( + "androidx.navigation.fragment", + "FragmentNavigator.Destination" +) + +val NavGraphNavigator: ClassName = ClassName.get( + "androidx.navigation", + "NavGraphNavigator" +) +val NavGraph: ClassName = ClassName.get( + "androidx.navigation", + "NavGraph" +) + +val NavOptionsBuilder: ClassName = ClassName.get( + "androidx.navigation", + "NavOptions" +) + + + + + + diff --git a/navigation/src/main/java/me/foolishchow/android/plugin/navigation/NavigationPlugin.kt b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/NavigationPlugin.kt new file mode 100644 index 0000000..7aeb3be --- /dev/null +++ b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/NavigationPlugin.kt @@ -0,0 +1,86 @@ +package me.foolishchow.android.plugin.navigation + +import com.android.build.gradle.api.ApplicationVariant +import com.android.build.gradle.internal.dsl.BaseAppModuleExtension +import me.foolishchow.android.navigationprocessor.extensions.* +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.closureOf +import java.io.* + +/** + * Description: + * Author: foolishchow + * Date: 2021/05/19 1:51 PM + */ + + +@Suppress("DefaultLocale", "UnstableApiUsage") +class NavigationPlugin : Plugin { + + override fun apply(project: Project) { + + val android: BaseAppModuleExtension = project.extensions.getByName("android") as BaseAppModuleExtension + + android.applicationVariants.configureEach { + registerPostProcessProguardRules(this, project) + registerNavigationTask(this, project) + } + } + + + private fun registerNavigationTask(variant: ApplicationVariant, _project: Project) { + val android: BaseAppModuleExtension = _project.extensions + .getByName("android") as BaseAppModuleExtension + + val sourceSet = variant.sourceSets.find { it.name == variant.name } ?: return + + android.sourceSets.forEach { source -> + if (source.name == sourceSet.name) { + source.java.srcDir(_project.navJavaDir(variant)) + source.res.srcDir(_project.navResDir(variant)) + } + } + + val task = _project.tasks.create(mapOf( + "name" to variant.NavigationTaskName, + "group" to "auto-param", + "type" to NavigationTask::class.java + ), closureOf { + val files = mutableListOf() + var lastModified = -1L + variant.sourceSets.forEach { sourceSet -> + sourceSet.resDirectories.forEach { res -> + project.fileTree(File(res, "navigation")).forEach { file -> + files.add(file) + lastModified = file.lastModified().coerceAtLeast(lastModified) + } + } + } + inputs.property("lastModified",lastModified) + inputs.files(files.toTypedArray()) + resDir = project.file(project.navResDir(variant)) + javaDir = project.file(project.navJavaDir(variant)) + ruleFile = project.file(project.navRuleFile(variant)) + }) + + val preBuild = "pre${variant.name.capitalize()}Build" + _project.tasks.findByName(preBuild)?.dependsOn(task) + } + + private fun registerPostProcessProguardRules(variant: ApplicationVariant, _project: Project) { + variant.outputs.forEach { output -> + val taskName = "process${variant.name.capitalize()}Resources" + val task = _project.tasks.findByName(taskName) + + task?.let { + output.processResourcesProvider.orNull?.doLast { + editAaptRule(project, variant) + } + } + } + } + +} + + diff --git a/navigation/src/main/java/me/foolishchow/android/plugin/navigation/NavigationTask.kt b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/NavigationTask.kt new file mode 100644 index 0000000..dfb5483 --- /dev/null +++ b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/NavigationTask.kt @@ -0,0 +1,300 @@ +package me.foolishchow.android.plugin.navigation + +import com.android.build.gradle.internal.dsl.BaseAppModuleExtension +import com.squareup.javapoet.ClassName +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.MethodSpec +import com.squareup.javapoet.TypeSpec +import groovy.util.Node +import groovy.util.XmlParser +import me.foolishchow.android.navigationprocessor.extensions.resId +import me.foolishchow.android.navigationprocessor.extensions.resourceSymbol +import me.foolishchow.android.navigationprocessor.extensions.snake2camel +import org.apache.tools.ant.taskdefs.condition.IsLastModified +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.transform.InputArtifact +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.tasks.* +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter +import javax.lang.model.element.Modifier +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + + +abstract class NavigationTask : DefaultTask() { + + + + @InputFiles + abstract fun getNavFiles(): ConfigurableFileCollection + + @get:OutputDirectory + lateinit var javaDir: File + + @get:OutputDirectory + lateinit var resDir: File + + @get:OutputFile + lateinit var ruleFile: File + + private lateinit var mClass: TypeSpec.Builder + + /** + * 缓存当前所有的文件名和方法名 + */ + private val mFileNameWithMethodName = mutableMapOf() + private lateinit var mResource: ClassName + + @TaskAction + open fun perform() { + val android = project.extensions.getByName("android") as BaseAppModuleExtension + val packageName = android.defaultConfig.applicationId + + project.delete(javaDir.listFiles()) + project.delete(resDir.listFiles()) + project.delete(ruleFile) + + + parseFiles(packageName) + + createDiscard() + + crateFileNames() + + } + + + private fun parseFiles(packageName: String?) { + mResource = ClassName.get(packageName, "R") + mClass = TypeSpec.classBuilder("NavigationManager") + .addModifiers(Modifier.PUBLIC) + inputs.files.forEach { file -> + parseAndGenerate(file) + } + + val method = MethodSpec.methodBuilder("attach") + .addParameter(NavController, "controller") + .addParameter(ClassName.INT, "navigationId") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .beginControlFlow("switch(navigationId)") + + mFileNameWithMethodName.forEach { item -> + method.addStatement("case \$T.navigation.${item.key.xmlName}:", mResource) + method.addStatement(" ${item.value}(controller)") + method.addStatement("break") + } + method.endControlFlow() + mClass.addMethod(method.build()) + + val javaFile = JavaFile.builder("$packageName.navigation", mClass.build()) + .build() + if (!javaDir.exists()) { + javaDir.mkdirs() + } + javaFile.writeTo(javaDir) + } + + + private fun parseAndGenerate(file: File) { + val name = file.xmlName.snake2camel() + val methodName = "attachNavigation$name" + mFileNameWithMethodName[file] = methodName + val method = MethodSpec.methodBuilder(methodName) + .addParameter(NavController, "controller") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + + method.addStatement("\$T fragment", FragmentDestination) + method.addStatement("\$T action", NavAction) + method.addStatement("\$T.Builder builder", NavOptionsBuilder) + method.addStatement( + "\$T graph = controller.getNavigatorProvider().getNavigator(\$T.class).createDestination()", + NavGraph, NavGraphNavigator + ) + + val navigation = XmlParser().parse(file) + navigation.loopAttribute { attr -> + val value = attr.value + if (attr.key.isResId) { + + method.addStatement("graph.setId(\$T.id.${value.resId})", mResource) + } + if (attr.key.isStartDestination) { + method.addStatement("graph.setStartDestination(\$T.id.${value.resId})", mResource) + } + if (attr.key.isAndroidLabel) { + method.addStatement("graph.setLabel(\"${attr.value}\")", mResource) + } + } + + + navigation.children().forEach { node -> + if (node is Node) { + parseFragment(node, method) + } + } + + method.addStatement("controller.setGraph((\$T)graph)", NavGraph) + + mClass.addMethod(method.build()) + + } + + + private fun parseFragment(fragment: Node, method: MethodSpec.Builder) { + if (!fragment.name().equals("fragment")) return + + method.addStatement( + "fragment = controller.getNavigatorProvider().getNavigator(\$T.class).createDestination()", + FragmentNavigator) + fragment.loopAttribute { attr -> + when { + attr.key.isResId -> { + method.addStatement("fragment.setId(\$T.id.${attr.value.resId})", mResource) + } + attr.key.isAndroidName -> { + val type = ClassName.bestGuess(attr.value) + method.addStatement("fragment.setClassName(\$T.class.getName())", type) + } + attr.key.isAndroidLabel -> { + method.addStatement("fragment.setLabel(\"${attr.value}\")") + } + } + } + + fragment.children().forEach { actionNode -> + if (actionNode is Node && actionNode.name().equals("action")) { + val action: Node = actionNode + parseAction(method, action) + } + } + method.addStatement("graph.addDestination(fragment)") + + } + + private fun parseAction(method: MethodSpec.Builder, action: Node) { + method.addStatement("builder = new \$T.Builder()", NavOptionsBuilder) + + + var actionId = "" + var destId = "" + var popUpTo = "" + var popUpToInclusive = "false" + action.loopAttribute { attr -> + when { + attr.key.isResId -> { + actionId = attr.value.resId + } + attr.key.isAppDestination -> { + destId = attr.value.resId + } + attr.key.isPopEnterAnim -> { + method.addStatement( + "builder.setPopEnterAnim(${attr.value.resourceSymbol})", + mResource + ) + } + attr.key.isPopExitAnim -> { + method.addStatement( + "builder.setPopExitAnim(${attr.value.resourceSymbol})", + mResource + ) + } + attr.key.isEnterAnim -> { + method.addStatement( + "builder.setEnterAnim(${attr.value.resourceSymbol})", + mResource + ) + } + attr.key.isExitAnim -> { + method.addStatement( + "builder.setExitAnim(${attr.value.resourceSymbol})", + mResource + ) + } + + attr.key.isLaunchSingleTop -> { + method.addStatement( + "builder.setLaunchSingleTop(${attr.value.resourceSymbol})", + mResource + ) + } + + attr.key.isPopUpTo -> { + popUpTo = attr.value.resourceSymbol + } + + attr.key.isPopUpToInclusive -> { + popUpToInclusive = attr.value.resourceSymbol + } + } + } + + if (popUpTo.isNotEmpty()) { + method.addStatement( + "builder.setPopUpTo(${popUpTo},${popUpToInclusive})", + mResource + ) + } + + method.addStatement( + "action = new \$T(\$T.id.$destId,builder.build())", + NavAction, mResource + ) + method.addStatement( + "fragment.putAction(\$T.id.${actionId},action)", mResource + ) + } + + + private fun crateFileNames() { + ruleFile.delete() + val fw = FileWriter(ruleFile.absoluteFile) + val bw = BufferedWriter(fw) + mFileNameWithMethodName.forEach { attr -> + bw.write(attr.key.absolutePath) + bw.newLine() + } + bw.close() + } + + private fun createDiscard() { + val document = DocumentBuilderFactory + .newInstance() + .newDocumentBuilder() + .newDocument() + + document.xmlStandalone = false + + val resources = document.createElement("resources") + resources.setAttribute("xmlns:tools", "http://schemas.android.com/tools") + val discards = mutableListOf() + mFileNameWithMethodName.forEach { item -> + discards.add("@navigation/${item.key.xmlName}") + } + resources.setAttribute("tools:discard", discards.joinToString(separator = ",") { it }) + document.appendChild(resources) + + val rawDir = File(resDir, "raw") + if (!rawDir.exists()) { + rawDir.mkdirs() + } + val discardFile = File(rawDir, "nav_discard.xml") + + val transformer = TransformerFactory.newInstance().newTransformer() + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2") + //是否自动换行 + transformer.setOutputProperty(OutputKeys.INDENT, "yes") + transformer.transform(DOMSource(document), StreamResult(discardFile)) + } + + + protected fun getIncremental(): Boolean { + return false + } + +} \ No newline at end of file diff --git a/navigation/src/main/java/me/foolishchow/android/plugin/navigation/RuleEditor.kt b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/RuleEditor.kt new file mode 100644 index 0000000..9389a37 --- /dev/null +++ b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/RuleEditor.kt @@ -0,0 +1,111 @@ +package me.foolishchow.android.plugin.navigation + +import com.android.build.gradle.api.ApplicationVariant +import me.foolishchow.android.navigationprocessor.extensions.AaptRules +import me.foolishchow.android.navigationprocessor.extensions.navRuleFile +import org.gradle.api.Project +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.FileReader +import java.io.FileWriter + +/** + * Description: + * Author: foolishchow + * Date: 2021/05/21 10:53 AM + */ +class Rules { + val rules = mutableListOf() + var rule = Rule() + + init { + rules.add(rule) + } + + fun add(str: String?, line: Int) { + if (str.isNullOrBlank()) { + rule = Rule() + rules.add(rule) + } else if (str.startsWith("# Referenced at")) { + rule.references.add(str) + } else if (str.startsWith("-keep class ")) { + rule.className = str + rule.line = line + } + } +} + +class Rule { + var line = -1 + var className: String? = null + val references = mutableListOf() +} + + +fun editAaptRule(project: Project, variant: ApplicationVariant) { + val rulesPath = project.AaptRules(variant) + val rulesWrapper = Rules() + + + //region 读取 aapt_rules.txt + val ruleContent = mutableListOf() + + val stream = BufferedReader(FileReader(rulesPath)) + var str: String? + var line = 1 + while (stream.readLine().also { str = it } != null) { + rulesWrapper.add(str, line) + ruleContent.add(str ?: "") + line++ + } + rulesWrapper.add(str, line) + + val rules = rulesWrapper.rules + //endregion + + + //region 读取 navigation/${variant.name}/navigation.txt + val navNameFile = project.navRuleFile(variant) + val navFiles = mutableListOf() + val reader = BufferedReader(FileReader(navNameFile)) + while (reader.readLine().also { str = it } != null) { + str?.let { navFiles.add(it) } + } + //endregion + + + //region 获取需要被注释的line + rules.forEach { rule -> + val ref = rule.references.filter { it -> + var has = false + for (navFile in navFiles) { + if (it.startsWith("# Referenced at $navFile")) { + has = true + break + } + } + !has + } + rule.references.clear() + rule.references.addAll(ref) + } + + val needToCommentLines = rules.map(fun(it: Rule): Int? { + if (it.className != null && it.references.size == 0) return it.line + return null + }).filterNotNull() + //endregion + + val fw = FileWriter(rulesPath) + val bw = BufferedWriter(fw) + ruleContent.forEachIndexed { index, lineContent -> + if (needToCommentLines.contains(index + 1)) { + bw.write("# $lineContent") + } else { + bw.write(lineContent) + } + bw.newLine() + } + bw.close() + +} \ No newline at end of file diff --git a/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/File.kt b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/File.kt new file mode 100644 index 0000000..5312097 --- /dev/null +++ b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/File.kt @@ -0,0 +1,35 @@ +package me.foolishchow.android.plugin.navigation.extensions + +import java.io.File + +/** + * Description: + * Author: foolishchow + * Date: 2021/05/19 3:17 PM + */ + +fun File.deleteSelf() { + deleteFile(this) +} + +/** + * 先根遍历序递归删除文件夹 + * + * @param dirFile 要被删除的文件或者目录 + * @return 删除成功返回true, 否则返回false + */ +fun deleteFile(dirFile: File): Boolean { + // 如果dir对应的文件不存在,则退出 + if (!dirFile.exists()) { + return false + } + + if (dirFile.isFile) { + return dirFile.delete() + } else { + dirFile.listFiles()?.forEach { file -> + deleteFile(file) + } + } + return dirFile.delete() +} diff --git a/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/Project.kt b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/Project.kt new file mode 100644 index 0000000..3947438 --- /dev/null +++ b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/Project.kt @@ -0,0 +1,32 @@ +package me.foolishchow.android.plugin.navigation.extensions + +import com.android.build.gradle.api.ApplicationVariant +import com.android.build.gradle.internal.dsl.BuildType +import org.gradle.api.Project + +/** + * Description: + * Author: foolishchow + * Date: 2021/05/20 9:05 AM + */ + +fun Project.navRuleFile(variant: ApplicationVariant):String{ + return "${project.buildDir}/intermediates/navigation/${variant.name}/navigation.txt" +} + +fun Project.navJavaDir(variant: ApplicationVariant): String { + return "${project.buildDir}/generated/source/navigation/${variant.name}/java" +} + +fun Project.navResDir(variant: ApplicationVariant): String { + return "${project.buildDir}/generated/source/navigation/${variant.name}/res" +} + +fun Project.AaptRules(variant: ApplicationVariant): String { + return "${project.buildDir.absolutePath}/intermediates/aapt_proguard_file/${variant.name}/aapt_rules.txt" +} + +val ApplicationVariant.NavigationTaskName: String + get() { + return "transfer${this.name.capitalize()}Navigation" + } \ No newline at end of file diff --git a/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/Resource.kt b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/Resource.kt new file mode 100644 index 0000000..290d4ef --- /dev/null +++ b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/Resource.kt @@ -0,0 +1,36 @@ +package me.foolishchow.android.plugin.navigation.extensions + +import java.util.regex.Pattern + +/** + * Description: + * Author: foolishchow + * Date: 2021/05/19 7:07 PM + */ +val String.resId: String + get() { + return this.replace("+", "").replace("@id/", "") + } + + +val IdPattern = Pattern.compile("\\@\\+?id\\/") +val AnimPattern = Pattern.compile("@anim\\/") + +val String.resourceSymbol: String + get() { + var matcher = IdPattern.matcher(this) + if (matcher.find()) { + return this.replace("+", "").replace("@id/", "\$T.id.") + } + + matcher = AnimPattern.matcher(this) + if (matcher.find()) { + return this.replace("+", "").replace("@anim/", "\$T.anim.") + } + + if (equals("true") || equals("false")) { + return this + } + + return "\"${this}\"" + } \ No newline at end of file diff --git a/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/String.kt b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/String.kt new file mode 100644 index 0000000..a26175c --- /dev/null +++ b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/String.kt @@ -0,0 +1,44 @@ +package me.foolishchow.android.plugin.navigation.extensions + +import java.util.regex.Matcher +import java.util.regex.Pattern + +/** + * Description: + * Author: foolishchow + * Date: 2021/05/19 3:12 PM + */ + + +val camelPattern = Pattern.compile("[A-Z]") +val snakePattern = Pattern.compile("_[a-z]") + + +@Suppress("DefaultLocale") +@JvmOverloads +fun String.camel2snake(uppercase: Boolean = false): String { + val str = this + val matcher: Matcher = camelPattern.matcher(str) + val sb = StringBuffer() + while (matcher.find()) { + matcher.appendReplacement(sb, "_" + matcher.group(0).toLowerCase()) + } + matcher.appendTail(sb) + return if (uppercase) { + sb.toString().toUpperCase() + } else sb.toString() +} + + +@Suppress("DefaultLocale") +fun String.snake2camel(): String { + val str: String = this + val matcher: Matcher = snakePattern.matcher(str); + val sb = StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(sb, matcher.group(0).substring(1).toUpperCase()); + } + matcher.appendTail(sb) + sb.setCharAt(0, sb[0].toUpperCase()) + return sb.toString(); +} \ No newline at end of file diff --git a/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/XmlParser.kt b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/XmlParser.kt new file mode 100644 index 0000000..a5149a8 --- /dev/null +++ b/navigation/src/main/java/me/foolishchow/android/plugin/navigation/extensions/XmlParser.kt @@ -0,0 +1,108 @@ +package me.foolishchow.android.plugin.navigation.extensions + +import groovy.util.Node +import groovy.xml.QName +import java.io.File + + +const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android" +const val APP_NAMESPACE = "http://schemas.android.com/apk/res-auto" + +fun Node.loopAttribute(predicate: (Map.Entry) -> Unit) { + val attributes = this.attributes() as Map + attributes.forEach(predicate) +} + +/** + * `app:destination` + */ +val QName.isAppDestination:Boolean + get() { + return namespaceURI.equals(APP_NAMESPACE) && localPart.equals("destination") + } + +/** + * `android:label` + */ +val QName.isAndroidLabel: Boolean + get() { + return namespaceURI.equals(ANDROID_NAMESPACE) && localPart.equals("label") + } + +/** + * `android:name` + */ +val QName.isAndroidName: Boolean + get() { + return namespaceURI.equals(ANDROID_NAMESPACE) && localPart.equals("name") + } + +/** + * `android:id` + */ +val QName.isResId: Boolean + get() { + return namespaceURI.equals(ANDROID_NAMESPACE) && localPart.equals("id") + } + +/** + * `app:startDestination` + */ +val QName.isStartDestination: Boolean + get() { + return namespaceURI.equals(APP_NAMESPACE) && localPart.equals("startDestination") + } + +/** + * `app:popEnterAnim` + */ +val QName.isPopEnterAnim: Boolean + get() { + return namespaceURI.equals(APP_NAMESPACE) && localPart.equals("popEnterAnim") + } + +/** + * `app:popExitAnim` + */ +val QName.isPopExitAnim: Boolean + get() { + return namespaceURI.equals(APP_NAMESPACE) && localPart.equals("popExitAnim") + } + +/** + * `app:enterAnim` + */ +val QName.isEnterAnim: Boolean + get() { + return namespaceURI.equals(APP_NAMESPACE) && localPart.equals("enterAnim") + } + +/** + * `app:exitAnim` + */ +val QName.isExitAnim: Boolean + get() { + return namespaceURI.equals(APP_NAMESPACE) && localPart.equals("exitAnim") + } + +val QName.isLaunchSingleTop:Boolean + get() { + return namespaceURI.equals(APP_NAMESPACE) && localPart.equals("launchSingleTop") + } + +val QName.isPopUpTo:Boolean + get() { + return namespaceURI.equals(APP_NAMESPACE) && localPart.equals("popUpTo") + } + +val QName.isPopUpToInclusive:Boolean + get() { + return namespaceURI.equals(APP_NAMESPACE) && localPart.equals("popUpToInclusive") + } + + +val File.xmlName:String + get() { + return this.name.replace(".xml", "") + } + diff --git a/navigation/src/main/resources/META-INF/gradle-plugins/navigation-transform.properties b/navigation/src/main/resources/META-INF/gradle-plugins/navigation-transform.properties new file mode 100644 index 0000000..36bb4d4 --- /dev/null +++ b/navigation/src/main/resources/META-INF/gradle-plugins/navigation-transform.properties @@ -0,0 +1 @@ +implementation-class=me.foolishchow.android.plugin.navigation.NavigationPlugin \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 4cdc351..3ba189a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ -//include ':navigation-transform' +include ':navigation'//include ':navigation-transform' include ':processor' include ':annotation' include ':utils'