diff --git a/annotations/build.gradle b/annotations/build.gradle index a542500..4eda512 100644 --- a/annotations/build.gradle +++ b/annotations/build.gradle @@ -14,19 +14,19 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } -sourceCompatibility = "7" -targetCompatibility = "7" +sourceCompatibility = "17" +targetCompatibility = "17" repositories { mavenCentral() } compileKotlin { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } } compileTestKotlin { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } } diff --git a/app/build.gradle b/app/build.gradle index 09059de..e8cf407 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,28 +2,26 @@ plugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-kapt' - id 'com.funnydevs.hilt-conductor.plugin' id 'dagger.hilt.android.plugin' + id 'com.funnydevs.hilt-conductor.plugin' } android { - compileSdkVersion 30 - buildToolsVersion "30.0.3" + compileSdk 33 + buildToolsVersion = "33.0.1" defaultConfig { applicationId "com.funnydevs.hilt_conductor.demo" minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 33 versionCode 1 versionName "1.0" + multiDexEnabled = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - lintOptions { - abortOnError false - } buildTypes { release { @@ -32,12 +30,16 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' + } + lint { + abortOnError false } + namespace 'com.funnydevs.hilt_conductor.demo' } dependencies { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 141e15e..da8be70 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> - + diff --git a/app/src/main/java/com/funnydevs/hilt_conductor/demo/App.kt b/app/src/main/java/com/funnydevs/hilt_conductor/demo/App.kt index 50bfd99..8959db0 100644 --- a/app/src/main/java/com/funnydevs/hilt_conductor/demo/App.kt +++ b/app/src/main/java/com/funnydevs/hilt_conductor/demo/App.kt @@ -4,5 +4,4 @@ import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class App : Application() { -} \ No newline at end of file +class App : Application() \ No newline at end of file diff --git a/app/src/main/java/com/funnydevs/hilt_conductor/demo/BaseBaseController.kt b/app/src/main/java/com/funnydevs/hilt_conductor/demo/BaseBaseController.kt new file mode 100644 index 0000000..6b75fe7 --- /dev/null +++ b/app/src/main/java/com/funnydevs/hilt_conductor/demo/BaseBaseController.kt @@ -0,0 +1,13 @@ +package com.funnydevs.hilt_conductor.demo + +import android.os.Bundle +import com.bluelinelabs.conductor.Controller +import javax.inject.Inject +import javax.inject.Named + +abstract class BaseBaseController(args: Bundle?) : Controller(args) { + + @Inject + @Named("second") + lateinit var secondText: String +} \ No newline at end of file diff --git a/app/src/main/java/com/funnydevs/hilt_conductor/demo/BaseController.kt b/app/src/main/java/com/funnydevs/hilt_conductor/demo/BaseController.kt index ddd7073..21b6fa1 100644 --- a/app/src/main/java/com/funnydevs/hilt_conductor/demo/BaseController.kt +++ b/app/src/main/java/com/funnydevs/hilt_conductor/demo/BaseController.kt @@ -1,13 +1,12 @@ package com.funnydevs.hilt_conductor.demo import android.os.Bundle -import com.bluelinelabs.conductor.Controller import javax.inject.Inject import javax.inject.Named -abstract class BaseController(args: Bundle?) : Controller(args) { +abstract class BaseController(args: Bundle?): BaseBaseController(args) { @Inject - @Named("second") - lateinit var testo: String + @Named("first") + lateinit var text: String } \ No newline at end of file diff --git a/app/src/main/java/com/funnydevs/hilt_conductor/demo/HiltModule.kt b/app/src/main/java/com/funnydevs/hilt_conductor/demo/HiltModule.kt index 9812e6a..64975c7 100644 --- a/app/src/main/java/com/funnydevs/hilt_conductor/demo/HiltModule.kt +++ b/app/src/main/java/com/funnydevs/hilt_conductor/demo/HiltModule.kt @@ -6,6 +6,7 @@ import com.funnydevs.hilt_conductor.annotations.ControllerScoped import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.scopes.ActivityScoped import javax.inject.Named @InstallIn(ControllerComponent::class) @@ -20,7 +21,7 @@ object HiltModule { @Provides @ControllerScoped @Named("second") - fun textTwo(controller: Controller): String { + fun textTwo(): String { return "Hello Moon" } } \ No newline at end of file diff --git a/app/src/main/java/com/funnydevs/hilt_conductor/demo/MainController.kt b/app/src/main/java/com/funnydevs/hilt_conductor/demo/MainController.kt index 00b0e52..1759ab0 100644 --- a/app/src/main/java/com/funnydevs/hilt_conductor/demo/MainController.kt +++ b/app/src/main/java/com/funnydevs/hilt_conductor/demo/MainController.kt @@ -1,32 +1,26 @@ package com.funnydevs.hilt_conductor.demo +import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView -import com.bluelinelabs.conductor.Controller -import com.funnydevs.hilt_conductor.ConductorInterface import com.funnydevs.hilt_conductor.annotations.ConductorEntryPoint -import com.funnydevs.hilt_conductor.annotations.ControllerScoped -import dagger.hilt.EntryPoints -import dagger.hilt.android.EntryPointAccessors -import javax.inject.Inject -import javax.inject.Named @ConductorEntryPoint class MainController(args: Bundle?) : BaseController(args) { - - + @SuppressLint("SetTextI18n") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle? ): View { - System.out.println() val view = inflater.inflate(R.layout.controller_main, container, false) - view.findViewById(R.id.tv_test).text = testo + view.findViewById(R.id.tv_test).text = "$text $secondText" return view } + + } \ No newline at end of file diff --git a/app/src/main/java/com/funnydevs/hilt_conductor/demo/SingletonModule.kt b/app/src/main/java/com/funnydevs/hilt_conductor/demo/SingletonModule.kt deleted file mode 100644 index 57dffed..0000000 --- a/app/src/main/java/com/funnydevs/hilt_conductor/demo/SingletonModule.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.funnydevs.hilt_conductor.demo - -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@InstallIn(SingletonComponent::class) -@Module -object SingletonModule { -} \ No newline at end of file diff --git a/build.gradle b/build.gradle index c7ca165..b7451ed 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { mavenLocal() } dependencies { - classpath "com.android.tools.build:gradle:4.1.3" + classpath 'com.android.tools.build:gradle:8.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" classpath "com.google.dagger:hilt-android-gradle-plugin:$versions.hilt" classpath "io.github.funnydevs:hilt-conductor-plugin:$VERSION" diff --git a/dependencies.gradle b/dependencies.gradle index 8d016b3..82b28a9 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,14 +1,14 @@ ext.versions = [ - appcompat : "1.3.0-rc01", - kotlin : "1.4.31", + appcompat : "1.6.1", + kotlin : '1.8.0', conductor : "3.0.0-rc4", timber : "4.7.1", - hilt : "2.35", + hilt : '2.49', navigation : "2.3.4", - material : "1.4.0-alpha02", - javassist : "3.27.0-GA", + material : "1.9.0", + asm : "9.6", auto_service : "1.0-rc4", javapoet : "1.13.0", javax_inject : "1" @@ -24,7 +24,8 @@ ext.libraries = [ hilt_android_compiler : "com.google.dagger:hilt-android-compiler:$versions.hilt", material : "com.google.android.material:material:$versions.material", stdlib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin", - javassist : "org.javassist:javassist:$versions.javassist", + asm : "org.ow2.asm:asm:$versions.asm", + asmCommons : "org.ow2.asm:asm-commons:$versions.asm", auto_service : "com.google.auto.service:auto-service:$versions.auto_service", javapoet : "com.squareup:javapoet:$versions.javapoet", kotlin_reflect : "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin", diff --git a/gradle.properties b/gradle.properties index 9d18707..39d738c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,8 @@ android.enableJetifier=false kotlin.code.style=official GROUP=io.github.funnydevs -VERSION=0.5.0 +VERSION=0.5.1 +android.nonTransitiveRClass=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 056dbec..293101f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Apr 23 14:34:01 CEST 2021 +#Fri Dec 08 11:42:47 CST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/library/build.gradle b/library/build.gradle index 8c2e9a4..67e732c 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -4,14 +4,12 @@ plugins { } android { - compileSdkVersion 30 - buildToolsVersion "30.0.3" + compileSdk 33 + buildToolsVersion "33.0.1" defaultConfig { minSdkVersion 21 - targetSdkVersion 30 - versionCode 1 - versionName VERSION + targetSdkVersion 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -26,8 +24,16 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + namespace 'com.funnydevs.hilt_conductor' + + publishing { + singleVariant('release') { + withSourcesJar() + withJavadocJar() + } } } diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml deleted file mode 100644 index 879a74b..0000000 --- a/library/src/main/AndroidManifest.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/library/src/main/java/com/funnydevs/hilt_conductor/ControllerComponent.java b/library/src/main/java/com/funnydevs/hilt_conductor/ControllerComponent.java index 896417e..92f9c68 100644 --- a/library/src/main/java/com/funnydevs/hilt_conductor/ControllerComponent.java +++ b/library/src/main/java/com/funnydevs/hilt_conductor/ControllerComponent.java @@ -7,10 +7,7 @@ import dagger.BindsInstance; import dagger.hilt.DefineComponent; -import dagger.hilt.android.components.ActivityComponent; import dagger.hilt.android.components.ActivityRetainedComponent; -import dagger.hilt.android.scopes.ActivityRetainedScoped; -import dagger.hilt.components.SingletonComponent; @ControllerScoped @DefineComponent(parent = ActivityRetainedComponent.class) diff --git a/plugin/build.gradle b/plugin/build.gradle index 3afb837..603bab9 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -26,17 +26,18 @@ repositories { } java { - sourceCompatibility = JavaVersion.VERSION_1_7 - targetCompatibility = JavaVersion.VERSION_1_7 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } dependencies { - implementation 'com.android.tools.build:gradle:4.0.0' + implementation 'com.android.tools.build:gradle:8.0.2' + implementation libraries.asm + implementation libraries.asmCommons implementation gradleApi() implementation localGroovy() implementation libraries.stdlib - implementation libraries.javassist } diff --git a/plugin/src/main/java/com/funnydevs/hilt_conductor/HiltConductorPlugin.kt b/plugin/src/main/java/com/funnydevs/hilt_conductor/HiltConductorPlugin.kt deleted file mode 100644 index 0fe3364..0000000 --- a/plugin/src/main/java/com/funnydevs/hilt_conductor/HiltConductorPlugin.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.funnydevs.hilt_conductor.plugin - -import com.android.build.gradle.AppPlugin -import com.android.build.gradle.BaseExtension -import com.android.build.gradle.LibraryPlugin -import org.gradle.api.GradleException -import org.gradle.api.Plugin -import org.gradle.api.Project - - -class HiltConductorPlugin : Plugin { - override fun apply(project: Project) { - - val isAndroid = - project.plugins.hasPlugin(AppPlugin::class.java) || project.plugins.hasPlugin( - LibraryPlugin::class.java - ) - if (!isAndroid) - throw GradleException("'com.android.application' or 'com.android.library' plugin required.") - - val android = project.extensions.findByName("android") as BaseExtension - - android.registerTransform(HiltConductorTransformer(project)) - } - -} - diff --git a/plugin/src/main/java/com/funnydevs/hilt_conductor/HiltConductorTransformer.kt b/plugin/src/main/java/com/funnydevs/hilt_conductor/HiltConductorTransformer.kt deleted file mode 100644 index e3ebb2f..0000000 --- a/plugin/src/main/java/com/funnydevs/hilt_conductor/HiltConductorTransformer.kt +++ /dev/null @@ -1,210 +0,0 @@ -package com.funnydevs.hilt_conductor.plugin - -import com.android.SdkConstants -import com.android.build.api.transform.* -import com.android.build.api.transform.QualifiedContent.DefaultContentType -import com.android.build.api.transform.QualifiedContent.Scope -import com.android.build.gradle.BaseExtension -import javassist.ClassPool -import javassist.CtClass -import javassist.CtField -import javassist.CtMethod -import org.gradle.api.Project -import java.util.* - - -class HiltConductorTransformer(private val project: Project) : Transform() { - - - override fun getName(): String = "ConductorHilt" - - override fun getInputTypes(): MutableSet = - mutableSetOf(DefaultContentType.CLASSES) - - override fun isIncremental(): Boolean = true - - override fun getScopes(): MutableSet = - EnumSet.of(Scope.PROJECT) - - override fun getReferencedScopes(): MutableSet = - EnumSet.of(Scope.EXTERNAL_LIBRARIES, Scope.SUB_PROJECTS, Scope.TESTED_CODE) - - - override fun transform(transformInvocation: TransformInvocation) { - super.transform(transformInvocation) - - /*****************Output directory where files will be saved****/ - val outputDir = transformInvocation.outputProvider.getContentLocation( - name, - outputTypes, - scopes, - Format.DIRECTORY - ) - - /****************************************************************/ - - val ctClasses = getClasses(transformInvocation) - - if (ctClasses.isEmpty()) - return - - val controllerClassFilter = - ctClasses.first().classPool.get("com.bluelinelabs.conductor.Controller") - - - val controllerClasses = getClasses( - controllerClassFilter,ctClasses - ) - - - controllerClasses - .filter { it.hasAnnotation("com.funnydevs.hilt_conductor.annotations.ConductorEntryPoint") } - .forEach { controllerClass -> - - println(controllerClass.name) - - controllerClass.addField( - CtField.make( - "com.funnydevs.hilt_conductor.ConductorComponentLifecycleHandler handler; ", - controllerClass - ) - ) - - - val injectableFields = StringBuilder() - for (field in controllerClass.fields){ - if (field.hasAnnotation("javax.inject.Inject")) - injectableFields - .append("${field.fieldInfo.name} = hiltInterface") - .append(".${controllerClass.simpleName}_${field.fieldInfo.name.capitalize()}();\n") - } - - - var hiltInterface = "${controllerClass.packageName}.${controllerClass.simpleName}HiltInterface" - - controllerClass.declaredMethods.firstOrNull() { it.name == "onCreateView" }?.apply { - this.insertAt(0, - "com.funnydevs.hilt_conductor.ConductorInterface conductorInterface = " + - "dagger.hilt.EntryPoints.get(getActivity()," + - "com.funnydevs.hilt_conductor.ConductorInterface.class);\n" + - "this.handler = conductorInterface.Conductor_LifeCycleHandler();\n" + - "handler.inject(getActivity(),this);\n"+ - "$hiltInterface hiltInterface = dagger.hilt.EntryPoints.get(handler,${hiltInterface}.class);\n" - +injectableFields) - } - - - var onDestroyViewMethod = controllerClass.declaredMethods.firstOrNull { it.name == "onDestroyView" } - if (onDestroyViewMethod == null){ - onDestroyViewMethod = CtMethod.make("protected void onDestroyView(android.view.View view) {super.onDestroyView(view);}" - ,controllerClass) - - controllerClass.addMethod(onDestroyViewMethod) - } - - onDestroyViewMethod?.insertAfter("handler.destroy();") - - - - } - - - - saveJarFiles(transformInvocation) - ctClasses.forEach { it.writeFile(outputDir.canonicalPath) } - } - - - - - private fun getClasses(transformInvocation: TransformInvocation): List { - val classPool = createClassPool(transformInvocation) - val isIncremental = transformInvocation.isIncremental - val classNames = - if (isIncremental) collectClassNamesForIncrementalBuild(transformInvocation) - else collectClassNamesForFullBuild(transformInvocation) - val ctClasses = classNames.map { className -> classPool.get(className) } - return ctClasses - } - - - private fun createClassPool(invocation: TransformInvocation): ClassPool { - val classPool = ClassPool(null) - classPool.appendSystemPath() - project.extensions.findByType(BaseExtension::class.java)?.bootClasspath?.forEach { - classPool.appendClassPath(it.absolutePath) - } - - invocation.inputs.forEach { input -> - input.directoryInputs.forEach { classPool.appendClassPath(it.file.absolutePath) } - input.jarInputs.forEach { classPool.appendClassPath(it.file.absolutePath) } - } - invocation.referencedInputs.forEach { input -> - input.directoryInputs.forEach { classPool.appendClassPath(it.file.absolutePath) } - input.jarInputs.forEach { classPool.appendClassPath(it.file.absolutePath) } - } - - return classPool - } - - private fun collectClassNamesForFullBuild(invocation: TransformInvocation): List = - invocation.inputs - .flatMap { it.directoryInputs } - .flatMap { - it.file.walkTopDown() - .filter { file -> file.isFile } - .map { file -> file.relativeTo(it.file) } - .toList() - } - .map { it.path } - .filter { it.endsWith(SdkConstants.DOT_CLASS) } - .map { pathToClassName(it) } - - private fun collectClassNamesForIncrementalBuild(invocation: TransformInvocation): List = - invocation.inputs - .flatMap { it.directoryInputs } - .flatMap { - it.changedFiles - .filter { (_, status) -> status != Status.NOTCHANGED && status != Status.REMOVED } - .map { (file, _) -> file.relativeTo(it.file) } - } - .map { it.path } - .filter { it.endsWith(SdkConstants.DOT_CLASS) } - .map { pathToClassName(it) } - - private fun pathToClassName(path: String): String { - return path.substring(0, path.length - SdkConstants.DOT_CLASS.length) - .replace("/", ".") - .replace("\\", ".") - } - - - private fun saveJarFiles(transformInvocation: TransformInvocation) { - transformInvocation.inputs.forEach { transformInput -> - // Ensure JARs are copied as well: - transformInput.jarInputs.forEach { - it.file.copyTo( - transformInvocation.outputProvider.getContentLocation( - it.name, - inputTypes, - scopes, - Format.JAR - ), - overwrite = true - ) - } - } - } -} - - -private fun getClasses( - superClass: CtClass, - ctClasses: List): List{ - - return ctClasses.filter { - it.subclassOf(superClass) - } - - -} \ No newline at end of file diff --git a/plugin/src/main/java/com/funnydevs/hilt_conductor/plugin/HiltConductorPlugin.kt b/plugin/src/main/java/com/funnydevs/hilt_conductor/plugin/HiltConductorPlugin.kt new file mode 100644 index 0000000..c84e09f --- /dev/null +++ b/plugin/src/main/java/com/funnydevs/hilt_conductor/plugin/HiltConductorPlugin.kt @@ -0,0 +1,39 @@ +package com.funnydevs.hilt_conductor.plugin + +import com.android.build.api.instrumentation.FramesComputationMode +import com.android.build.api.instrumentation.InstrumentationScope +import com.android.build.gradle.AppPlugin +import com.android.build.gradle.LibraryPlugin +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import com.android.build.api.variant.AndroidComponentsExtension +import com.funnydevs.hilt_conductor.visitors.ConductorEntryPointClassVisitor + +class HiltConductorPlugin : Plugin { + override fun apply(project: Project) { + + val isAndroid = + project.plugins.hasPlugin(AppPlugin::class.java) || project.plugins.hasPlugin( + LibraryPlugin::class.java + ) + if (!isAndroid) + throw GradleException("'com.android.application' or 'com.android.library' plugin required.") + + val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java) + + androidComponents.onVariants { variant -> + variant.instrumentation.apply { + + transformClassesWith( + classVisitorFactoryImplClass = ConductorEntryPointClassVisitor.Factory::class.java, + scope = InstrumentationScope.ALL, + instrumentationParamsConfig = {} + ) + setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_CLASSES) + } + } + + } +} + diff --git a/plugin/src/main/java/com/funnydevs/hilt_conductor/visitors/ConductorEntryPointClassVisitorFactory.kt b/plugin/src/main/java/com/funnydevs/hilt_conductor/visitors/ConductorEntryPointClassVisitorFactory.kt new file mode 100644 index 0000000..a573650 --- /dev/null +++ b/plugin/src/main/java/com/funnydevs/hilt_conductor/visitors/ConductorEntryPointClassVisitorFactory.kt @@ -0,0 +1,183 @@ +package com.funnydevs.hilt_conductor.visitors + +import com.android.build.api.instrumentation.AsmClassVisitorFactory +import com.android.build.api.instrumentation.ClassContext +import com.android.build.api.instrumentation.ClassData +import com.android.build.api.instrumentation.InstrumentationParameters +import org.objectweb.asm.AnnotationVisitor +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.FieldVisitor +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * ASM Adapter that transforms @ConductorEntryPoint-annotated classes to extend the Hilt + * generated android class, including the @HiltAndroidApp application class. + */ +class ConductorEntryPointClassVisitor( + private val apiVersion: Int, + nextClassVisitor: ClassVisitor, +) : ClassVisitor(apiVersion, nextClassVisitor) { + + + abstract class Factory : AsmClassVisitorFactory { + override fun createClassVisitor( + classContext: ClassContext, + nextClassVisitor: ClassVisitor + ): ClassVisitor { + // val writer = PrintWriter(System.out) // or a file writer for output to a file + // val traceVisitor = TraceClassVisitor(nextClassVisitor, writer) + + return ConductorEntryPointClassVisitor( + apiVersion = instrumentationContext.apiVersion.get(), + nextClassVisitor = nextClassVisitor, + ) + } + + /** + * Check if a class should be transformed. + * + * Only classes that are a Conductor entry point should be transformed. + */ + override fun isInstrumentable(classData: ClassData): Boolean { + return classData.classAnnotations.any { CONDUCTOR_ENTRY_POINT_ANNOTATIONS.contains(it) } + } + } + + + private var isConductorEntryPoint = false + private lateinit var controllerName:String + private var controllerSuperName: String? = null + + // where the key is the field name and the value is the field descriptor + private val injectableFields = mutableMapOf() + private var onDestroyViewExists = false + + + override fun visit(version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, + interfaces: Array?) { + super.visit(version, access, name, signature, superName, interfaces) + this.controllerName = name + this.controllerSuperName = superName + + // Add a field + cv.visitField( + /* access = */ Opcodes.ACC_PRIVATE, + /* name = */ "handler", + /* descriptor = */ "Lcom/funnydevs/hilt_conductor/ConductorComponentLifecycleHandler;", + /* signature = */ null, + /* value = */ null + )?.visitEnd() + + } + + override fun visitField(access: Int, + name: String, + desc: String, + signature: String?, + value: Any?): FieldVisitor { + val fv = super.visitField(access, name, desc, signature, value) + println("field name = $name") + return object : FieldVisitor(Opcodes.ASM7, fv) { + override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? { + if (descriptor == "Ljavax/inject/Inject;") { + injectableFields[name] = desc + } + return super.visitAnnotation(descriptor, visible) + } + } + } + + override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? { + println("visitAnnotation name = $descriptor") + if (CONDUCTOR_ENTRY_POINT_ANNOTATIONS.contains(descriptor)) { + isConductorEntryPoint = true + } + return super.visitAnnotation(descriptor, visible) + } + + override fun visitMethod( + access: Int, name: String, desc: String, signature: String?, exceptions: Array? + ): MethodVisitor { + val mv = cv.visitMethod(access, name, desc, signature, exceptions) + println("visitMethod name = $name") + + // Check if this is the 'onCreateView' method. + if (name == "onCreateView") { + return ConductorOnCreateMethodVisitor(mv,desc,access, controllerName) + } + + if (name == "onDestroyView") { + // Modify the onDestroy method + onDestroyViewExists = true + return ConductorOnDestroyMethodVisitor(mv, controllerName) + } + return mv + } + + override fun visitEnd() { + if (!onDestroyViewExists) { + // Create the onDestroyView method if it doesn't exist + createOnDestroyViewMethod() + } + super.visitEnd() + } + + private fun createOnDestroyViewMethod() { + val mv = cv.visitMethod(Opcodes.ACC_PROTECTED, + "onDestroyView", + "(Landroid/view/View;)V", + null, + null + ) + mv.visitCode() + + // Equivalent to: super.onDestroyView(view); + // Load 'this' onto the stack + mv.visitVarInsn(Opcodes.ALOAD, 0) + // Load the first method argument (the View parameter) onto the stack + mv.visitVarInsn(Opcodes.ALOAD, 1) + // Invoke the super class's onDestroyView method + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, + controllerSuperName, + "onDestroyView", + "(Landroid/view/View;)V", + false + ) + + // Equivalent to: this.handler.destroy(); + // Load 'this' onto the stack + mv.visitVarInsn(Opcodes.ALOAD, 0) + // Get the 'handler' field from 'this' + mv.visitFieldInsn(Opcodes.GETFIELD, + controllerName, + "handler", + "Lcom/funnydevs/hilt_conductor/ConductorComponentLifecycleHandler;" + ) + // Invoke the 'destroy' method on 'handler' + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "com/funnydevs/hilt_conductor/ConductorComponentLifecycleHandler", + "destroy", + "()V", + false + ) + + // Return + mv.visitInsn(Opcodes.RETURN) + + // Compute the maximum stack size and local variables automatically + mv.visitMaxs(-1, -1) + mv.visitEnd() + } + + companion object { + val CONDUCTOR_ENTRY_POINT_ANNOTATIONS = setOf( + "com.funnydevs.hilt_conductor.annotations.ConductorEntryPoint", + "Lcom/funnydevs/hilt_conductor/annotations/ConductorEntryPoint" + ) + } +} diff --git a/plugin/src/main/java/com/funnydevs/hilt_conductor/visitors/ConductorOnCreateMethodVisitor.kt b/plugin/src/main/java/com/funnydevs/hilt_conductor/visitors/ConductorOnCreateMethodVisitor.kt new file mode 100644 index 0000000..8194b63 --- /dev/null +++ b/plugin/src/main/java/com/funnydevs/hilt_conductor/visitors/ConductorOnCreateMethodVisitor.kt @@ -0,0 +1,86 @@ +package com.funnydevs.hilt_conductor.visitors + + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Opcodes.INVOKESTATIC +import org.objectweb.asm.Type +import org.objectweb.asm.commons.LocalVariablesSorter + +class ConductorOnCreateMethodVisitor( + mv: MethodVisitor, + descriptor: String, + access: Int, + private val controllerName: String, +) : LocalVariablesSorter(Opcodes.ASM7, access, descriptor, mv) { + + private val componentLifeCycleHandler = "com/funnydevs/hilt_conductor/ConductorComponentLifecycleHandler" + private var conductorInterfaceVarIndex = -1 + override fun visitCode() { + super.visitCode() + + + // Load 'this' and call getActivity() + loadThisAndInvokeMethod("getActivity", "()Landroid/app/Activity;") + + // Get ConductorInterface instance + getHiltEntryPoint() + + // Set 'handler' field + setHandlerField() + + // Invoke 'handler.inject(getActivity(), this)' + invokeHandlerInject() + + // Controller_Injecter.inject(handler, this) + invokeControllerInject() + + } + + private fun invokeControllerInject() { + mv.visitVarInsn(Opcodes.ALOAD, 0); // Load 'this' onto the stack + mv.visitFieldInsn(Opcodes.GETFIELD, + controllerName, + "handler", + "Lcom/funnydevs/hilt_conductor/ConductorComponentLifecycleHandler;" + ) + mv.visitVarInsn(Opcodes.ALOAD, 0) + mv.visitMethodInsn(INVOKESTATIC, + "${controllerName}_Injector", + "inject", + "(Lcom/funnydevs/hilt_conductor/ConductorComponentLifecycleHandler;L$controllerName;)V", + false + ); + } + + private fun loadThisAndInvokeMethod(methodName: String, methodDesc: String) { + mv.visitVarInsn(Opcodes.ALOAD, 0) // Load 'this' + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, controllerName, methodName, methodDesc, false) + } + + private fun getHiltEntryPoint() { + val hiltInterfaceTypeDesc = "Lcom/funnydevs/hilt_conductor/ConductorInterface;" + val hiltInterfaceType = Type.getType(hiltInterfaceTypeDesc) + conductorInterfaceVarIndex = newLocal(hiltInterfaceType) + mv.visitLdcInsn(hiltInterfaceType) + mv.visitMethodInsn(INVOKESTATIC, "dagger/hilt/EntryPoints", "get", "(Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;", false) + mv.visitTypeInsn(Opcodes.CHECKCAST, hiltInterfaceType.internalName) + mv.visitVarInsn(Opcodes.ASTORE, conductorInterfaceVarIndex) + } + + private fun setHandlerField() { + mv.visitVarInsn(Opcodes.ALOAD, 0) // Load 'this' + mv.visitVarInsn(Opcodes.ALOAD, conductorInterfaceVarIndex) + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "com/funnydevs/hilt_conductor/ConductorInterface", "Conductor_LifeCycleHandler", "()L${componentLifeCycleHandler};", true) + mv.visitFieldInsn(Opcodes.PUTFIELD, controllerName, "handler", "L${componentLifeCycleHandler};") + } + + private fun invokeHandlerInject() { + mv.visitVarInsn(Opcodes.ALOAD, 0) // Load 'this' + mv.visitFieldInsn(Opcodes.GETFIELD, controllerName, "handler", "L${componentLifeCycleHandler};") + loadThisAndInvokeMethod("getActivity", "()Landroid/app/Activity;") + mv.visitVarInsn(Opcodes.ALOAD, 0) + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, componentLifeCycleHandler, "inject", "(Landroid/app/Activity;Lcom/bluelinelabs/conductor/Controller;)V", false) + } + +} diff --git a/plugin/src/main/java/com/funnydevs/hilt_conductor/visitors/ConductorOnDestroyMethodVisitor.kt b/plugin/src/main/java/com/funnydevs/hilt_conductor/visitors/ConductorOnDestroyMethodVisitor.kt new file mode 100644 index 0000000..7e15045 --- /dev/null +++ b/plugin/src/main/java/com/funnydevs/hilt_conductor/visitors/ConductorOnDestroyMethodVisitor.kt @@ -0,0 +1,21 @@ +package com.funnydevs.hilt_conductor.visitors + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +class ConductorOnDestroyMethodVisitor(mv: MethodVisitor, + private val controllerName: String) + : MethodVisitor(Opcodes.ASM7, mv) { + + override fun visitInsn(opcode: Int) { + if (opcode == Opcodes.RETURN) { + // Inject the handler.destroy() call just before every RETURN instruction + mv.visitVarInsn(Opcodes.ALOAD, 0) // Load 'this' + mv.visitFieldInsn(Opcodes.GETFIELD, controllerName, "handler", "Lcom/funnydevs/hilt_conductor/ConductorComponentLifecycleHandler;") + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/funnydevs/hilt_conductor/ConductorComponentLifecycleHandler", "destroy", "()V", false) + } + super.visitInsn(opcode) + } + + +} diff --git a/processor/build.gradle b/processor/build.gradle index 29e4b32..e98a516 100644 --- a/processor/build.gradle +++ b/processor/build.gradle @@ -34,8 +34,8 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } -sourceCompatibility = "8" -targetCompatibility = "8" +sourceCompatibility = "17" +targetCompatibility = "17" publishing { publications { @@ -56,11 +56,11 @@ repositories { } compileKotlin { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } } compileTestKotlin { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } } \ No newline at end of file diff --git a/processor/src/main/java/com/funnydevs/hilt_conductor/processor/FileGenerator.kt b/processor/src/main/java/com/funnydevs/hilt_conductor/processor/FileGenerator.kt index 5a6ad9a..d5a3a2a 100644 --- a/processor/src/main/java/com/funnydevs/hilt_conductor/processor/FileGenerator.kt +++ b/processor/src/main/java/com/funnydevs/hilt_conductor/processor/FileGenerator.kt @@ -10,15 +10,18 @@ import javax.inject.Named import javax.lang.model.SourceVersion import javax.lang.model.element.* import javax.lang.model.type.DeclaredType +import javax.lang.model.type.TypeKind +import javax.lang.model.type.TypeMirror +import javax.tools.Diagnostic @AutoService(Processor::class) class FileGenerator : AbstractProcessor() { -// var injectablesFields = mutableMapOf>>() - var whereInjectList = mutableListOf() - var whereNamedList = mutableListOf() - var build = false +// val injectablesFields = mutableMapOf>>() + private val whereInjectList = mutableListOf() + private val whereNamedList = mutableListOf() + private var build = false override fun getSupportedAnnotationTypes(): MutableSet { return mutableSetOf(ControllerScoped::class.java.name, Named::class.java.name, Inject::class.java.name) @@ -38,18 +41,16 @@ class FileGenerator : AbstractProcessor() { val rootClassType = rootElement as TypeElement roundEnv.getElementsAnnotatedWith(Named::class.java)?.forEach { namedElement -> - var classType = namedElement::class.members.let { members -> - members.first { it.name == "owner" }.let { it.call(namedElement) } - } as? TypeElement + val classType = namedElement::class.members.first { it.name == "owner" }.call(namedElement) as? TypeElement if (classType is TypeElement) { - var controllerTypeClass = findIfHasControllerParent(classType) + val controllerTypeClass = findIfHasControllerParent(classType) if (controllerTypeClass!= null && - ( classType.toString() == rootClassType.superclass.toString() || + ( isSuperclassOfType(classType, rootClassType) || classType.toString() == rootClassType.toString() ) ) { - var classTypeName = rootClassType.toString() + val classTypeName = rootClassType.toString() val value = namedElement.annotationMirrors.first { it.annotationType.asElement().simpleName.toString() == Named::class.simpleName.toString() } .elementValues.values.iterator().next().toString() @@ -72,18 +73,16 @@ class FileGenerator : AbstractProcessor() { roundEnv.getElementsAnnotatedWith(Inject::class.java)?.forEach { injectElement -> - var classType = injectElement::class.members.let { members -> - members.first { it.name == "owner" }.let { it.call(injectElement) } - } as? TypeElement + val classType = injectElement::class.members.first { it.name == "owner" }.call(injectElement) as? TypeElement if (classType is TypeElement) { - var controllerTypeClass = findIfHasControllerParent(classType) + val controllerTypeClass = findIfHasControllerParent(classType) if (controllerTypeClass!= null && - ( classType.toString() == rootClassType.superclass.toString() || + ( isSuperclassOfType(classType, rootClassType) || classType.toString() == rootClassType.toString() ) ) { - var classTypeName = rootClassType.toString() + val classTypeName = rootClassType.toString() val fieldClassType = injectElement.asType().toString() if (whereInjectList.firstOrNull { it.className == classTypeName } == null) whereInjectList.add(WhereInject(className = classTypeName)) @@ -100,6 +99,8 @@ class FileGenerator : AbstractProcessor() { generateInterfaces() + generateInjectors() + build = true } catch (t: Throwable) { println(t.message) @@ -109,6 +110,20 @@ class FileGenerator : AbstractProcessor() { return true } + private fun isSuperclassOfType(classType: TypeElement, rootClassType: TypeElement): Boolean { + var currentType: TypeMirror? = rootClassType.superclass + + while (currentType != null && currentType.kind != TypeKind.NONE) { + if (currentType.toString() == classType.toString()) { + return true + } + val currentElement = (currentType as DeclaredType).asElement() as TypeElement + currentType = currentElement.superclass + } + + return false + } + private fun findIfHasControllerParent(typeElement: TypeElement): TypeElement? { @@ -129,8 +144,8 @@ class FileGenerator : AbstractProcessor() { private fun generateInterfaces() { try { for (element in whereInjectList) { - var packageValue = element.className.substringBeforeLast(".") - var className = element.className.substringAfterLast(".") + val packageValue = element.className.substringBeforeLast(".") + val className = element.className.substringAfterLast(".") val hiltInterface: TypeSpec = TypeSpec.interfaceBuilder("${className}HiltInterface").addModifiers(Modifier.PUBLIC) @@ -149,12 +164,12 @@ class FileGenerator : AbstractProcessor() { ) .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT).returns( ClassName.get( - "${field.type.substringBeforeLast(".")}", - "${field.type.substringAfterLast(".")}" + field.type.substringBeforeLast("."), + field.type.substringAfterLast(".") ) ) .apply { - var namedElement = whereNamedList.firstOrNull { it.className == element.className } + val namedElement = whereNamedList.firstOrNull { it.className == element.className } ?.fields?.firstOrNull { it.name == field.name.capitalize() } if (namedElement != null){ addAnnotation(AnnotationSpec.builder(Class.forName ("javax.inject.Named")) @@ -177,6 +192,46 @@ class FileGenerator : AbstractProcessor() { } + private fun generateInjectors() { + whereInjectList.forEach { whereInject -> + println("whereInject = $whereInject") + val className = whereInject.className.substringAfterLast(".") + val packageValue = whereInject.className.substringBeforeLast(".") + + val classBuilder = TypeSpec.classBuilder("${className}_Injector") + .addModifiers(Modifier.PUBLIC) + + val injectMethodBuilder = MethodSpec.methodBuilder("inject") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(Void.TYPE) + .addParameter(ClassName.get(packageValue.substringBeforeLast("."), "ConductorComponentLifecycleHandler"), "handler") + .addParameter(ClassName.bestGuess(whereInject.className), "controller") + + injectMethodBuilder.addStatement("\$T entryPoint = \$T.get(handler, \$T.class)", + ClassName.get("java.lang", "Object"), // For 'Object' + ClassName.get("dagger.hilt", "EntryPoints"), // For 'EntryPoints' + ClassName.get(packageValue, "${className}HiltInterface")) + + + whereInject.fields.forEach { field -> + val methodName = "${className}_${field.name.capitalize()}" + injectMethodBuilder.addStatement("controller.\$N = ((\$T)entryPoint).$methodName()", + field.name, + ClassName.get(packageValue, "${className}HiltInterface")) + } + + + classBuilder.addMethod(injectMethodBuilder.build()) + + val javaFile = JavaFile.builder(packageValue, classBuilder.build()) + try { + javaFile.build().writeTo(processingEnv.filer) + } catch (e: Exception) { + processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, e.message) + } + } + } + } data class WhereInject(