=
- if (this.project != null) this.project!!.getVariantData(request)
- else CompletableFuture.completedFuture(null)
-}
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AbstractModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AbstractModelBuilder.kt
new file mode 100644
index 000000000..7e7af8edc
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AbstractModelBuilder.kt
@@ -0,0 +1,106 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+package com.itsaky.androidide.tooling.impl.sync
+
+import com.android.builder.model.v2.models.Versions
+import com.itsaky.androidide.tooling.impl.Main
+import com.itsaky.androidide.utils.ILogger
+import com.itsaky.androidide.utils.LogUtils
+import org.gradle.tooling.BuildController
+import org.gradle.tooling.model.Model
+
+/**
+ * Abstract class for [IModelBuilder] implementations.
+ *
+ * @property androidVariant The name of the variant for which the Android models will be built.
+ * @author Akash Yadav
+ */
+abstract class AbstractModelBuilder(protected val androidVariant: String = VARIANT_DEBUG) :
+ IModelBuilder
{
+
+ companion object {
+
+ /**
+ * Default variant name used for [AbstractModelBuilder.androidVariant].
+ */
+ const val VARIANT_DEBUG = "debug"
+
+ /**
+ * Checks the Android Gradle Plugin version from the given [Versions] model and compares
+ * it with [Main.MIN_SUPPORTED_AGP_VERSION]. If the version is less than the [Main.MIN_SUPPORTED_AGP_VERSION],
+ * throws an [UnsupportedOperationException].
+ *
+ * @param versions The [Versions] model.
+ */
+ @JvmStatic
+ protected fun assertMinimumAgp(versions: Versions) {
+ if (versions.agp < Main.MIN_SUPPORTED_AGP_VERSION) {
+ throw ModelBuilderException(
+ "Android Gradle Plugin version "
+ + versions.agp
+ + " is not supported by AndroidIDE. "
+ + "Please update your project to use at least v"
+ + Main.MIN_SUPPORTED_AGP_VERSION
+ + " of Android Gradle Plugin to build this project.")
+ }
+ }
+
+ /**
+ * Get the [Versions] information about Android projects. This returns `null` if
+ * the project is not an Android project.
+ *
+ * @param model The model element, usually a project.
+ * @param controller The build controller that is used for finding the model.
+ * @return The [Versions] model if available, `null` otherwise.
+ */
+ @JvmStatic
+ protected fun getAndroidVersions(model: Model, controller: BuildController): Versions? {
+ return controller.findModel(model, Versions::class.java)
+ }
+
+ @JvmStatic
+ protected fun log(vararg objects: Any?) {
+ buildLog(*objects)
+ }
+
+ /**
+ * Logs the given objects to the error stream.
+ *
+ * @param objects The objects to log.
+ */
+ protected fun buildLog(vararg objects: Any?) {
+ System.err.println(generateMessage(*objects))
+ }
+
+ /**
+ * Generates the log message for the given objects. This works similar to
+ * [ generateMessage(Object...)][com.itsaky.androidide.utils.ILogger.generateMessage] in [ILogger][com.itsaky.androidide.utils.ILogger].
+ *
+ * @param objects The objects to print in the message.
+ * @return The generated message.
+ */
+ protected fun generateMessage(vararg objects: Any?): String {
+ val sb = StringBuilder()
+ for (msg in objects) {
+ sb.append(if (msg is Throwable) "\n" else ILogger.MSG_SEPARATOR)
+ sb.append(if (msg is Throwable) LogUtils.getFullStackTrace(
+ msg as Throwable?) else msg)
+ }
+ return sb.toString()
+ }
+ }
+}
\ No newline at end of file
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AndroidProjectModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AndroidProjectModelBuilder.kt
new file mode 100644
index 000000000..9a5a8c037
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AndroidProjectModelBuilder.kt
@@ -0,0 +1,49 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+package com.itsaky.androidide.tooling.impl.sync
+
+import com.android.builder.model.v2.models.AndroidProject
+import com.android.builder.model.v2.models.BasicAndroidProject
+import com.android.builder.model.v2.models.ModelBuilderParameter
+import com.android.builder.model.v2.models.VariantDependencies
+import com.itsaky.androidide.tooling.api.IAndroidProject
+import com.itsaky.androidide.tooling.impl.internal.AndroidProjectImpl
+
+/**
+ * Builds model for Android application and library projects.
+ *
+ * @author Akash Yadav
+ */
+class AndroidProjectModelBuilder(androidVariant: String) :
+ AbstractModelBuilder(androidVariant) {
+
+ override fun build(param: BuildControllderAndIdeaModule): IAndroidProject {
+ val (controller, module) = param
+ val basicModel = controller.findModel(module, BasicAndroidProject::class.java)
+ val androidModel = controller.findModel(module, AndroidProject::class.java)
+ val variantDependencies = controller.findModel(module, VariantDependencies::class.java,
+ ModelBuilderParameter::class.java) { it.variantName = androidVariant }
+
+ return AndroidProjectImpl(
+ module.gradleProject,
+ androidVariant,
+ basicModel,
+ androidModel,
+ variantDependencies
+ )
+ }
+}
\ No newline at end of file
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/GradleProjectModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/GradleProjectModelBuilder.kt
new file mode 100644
index 000000000..58dcfacd2
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/GradleProjectModelBuilder.kt
@@ -0,0 +1,34 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+package com.itsaky.androidide.tooling.impl.sync
+
+import com.itsaky.androidide.tooling.api.IGradleProject
+import com.itsaky.androidide.tooling.impl.internal.GradleProjectImpl
+import org.gradle.tooling.model.GradleProject
+
+/**
+ * Builds model for root Gradle project (represented with [IGradleProject].
+ *
+ * @author Akash Yadav
+ */
+class GradleProjectModelBuilder : AbstractModelBuilder() {
+
+ @Throws(ModelBuilderException::class)
+ override fun build(param: GradleProject): IGradleProject {
+ return GradleProjectImpl(param)
+ }
+}
\ No newline at end of file
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/IModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/IModelBuilder.kt
new file mode 100644
index 000000000..59a400f38
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/IModelBuilder.kt
@@ -0,0 +1,38 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+package com.itsaky.androidide.tooling.impl.sync
+
+/**
+ * A model builder builds project models when the project is initialized/synced.
+ *
+ * @param The parameter for building the model.
+ * @param The type of model that is built.
+ * @author Akash Yadav
+ */
+interface IModelBuilder {
+
+ /**
+ * Builds the model.
+ *
+ * @param param Parameter for building the model.
+ * @return The built model. Implementations should throw [ModelBuilderException] instead of
+ * returning `null`.
+ * @throws ModelBuilderException If the model could not be built.
+ */
+ @Throws(ModelBuilderException::class)
+ fun build(param: P): R
+}
\ No newline at end of file
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/JavaProjectModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/JavaProjectModelBuilder.kt
new file mode 100644
index 000000000..278d4ba2b
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/JavaProjectModelBuilder.kt
@@ -0,0 +1,34 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.tooling.impl.sync
+
+import com.itsaky.androidide.tooling.api.IJavaProject
+import com.itsaky.androidide.tooling.impl.internal.JavaProjectImpl
+import org.gradle.tooling.model.idea.IdeaModule
+
+/**
+ * Builds model for Java library projects.
+ *
+ * @author Akash Yadav
+ */
+class JavaProjectModelBuilder : AbstractModelBuilder() {
+
+ override fun build(param: IdeaModule): IJavaProject {
+ return JavaProjectImpl(param.gradleProject)
+ }
+}
\ No newline at end of file
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/ModelBuilderException.java b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/ModelBuilderException.java
new file mode 100644
index 000000000..a1c2dc7eb
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/ModelBuilderException.java
@@ -0,0 +1,41 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.tooling.impl.sync;
+
+/**
+ * Exception thrown while building project models.
+ *
+ * @author Akash Yadav
+ */
+public class ModelBuilderException extends RuntimeException {
+
+ public ModelBuilderException() {
+ }
+
+ public ModelBuilderException(String message) {
+ super(message);
+ }
+
+ public ModelBuilderException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ModelBuilderException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/ModuleProjectModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/ModuleProjectModelBuilder.kt
new file mode 100644
index 000000000..2cb88a23e
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/ModuleProjectModelBuilder.kt
@@ -0,0 +1,36 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.tooling.impl.sync
+
+import com.itsaky.androidide.tooling.api.IModuleProject
+
+/**
+ * Builds models for module projects (either Android app/library or Java library projects).
+ *
+ * @author Akash Yadav
+ */
+class ModuleProjectModelBuilder(androidVariant: String = VARIANT_DEBUG) :
+ AbstractModelBuilder(androidVariant) {
+
+ override fun build(param: BuildControllderAndIdeaModule): IModuleProject {
+ val (controller, module) = param
+ val isAndroidProject = getAndroidVersions(module, controller) != null
+ return if (isAndroidProject) AndroidProjectModelBuilder(androidVariant).build(
+ controller to module) else JavaProjectModelBuilder().build(module)
+ }
+}
\ No newline at end of file
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/NotAnAndroidProjectException.java b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/NotAnAndroidProjectException.java
new file mode 100644
index 000000000..31c143dea
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/NotAnAndroidProjectException.java
@@ -0,0 +1,35 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.tooling.impl.sync;
+
+/**
+ * Exception thrown to indicate that a project is not an Android project.
+ *
+ * @author Akash Yadav
+ */
+public class NotAnAndroidProjectException extends ModelBuilderException {
+
+ /**
+ * Creates a new {@link NotAnAndroidProjectException} for the given project name.
+ *
+ * @param projectName The name of the project which is not an Android project.
+ */
+ public NotAnAndroidProjectException(String projectName) {
+ super("Project " + projectName + " is not an Android project");
+ }
+}
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/RootModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/RootModelBuilder.kt
new file mode 100644
index 000000000..dd16f4a9c
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/RootModelBuilder.kt
@@ -0,0 +1,96 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.tooling.impl.sync
+
+import com.itsaky.androidide.tooling.api.IAndroidProject
+import com.itsaky.androidide.tooling.api.IProject
+import com.itsaky.androidide.tooling.impl.Main
+import com.itsaky.androidide.tooling.impl.Main.finalizeLauncher
+import com.itsaky.androidide.tooling.impl.internal.ProjectImpl
+import com.itsaky.androidide.utils.ILogger
+import org.gradle.tooling.BuildController
+import org.gradle.tooling.ConfigurableLauncher
+import org.gradle.tooling.model.idea.IdeaProject
+
+/**
+ * Utility class to build the project models.
+ *
+ * @author Akash Yadav
+ */
+object RootModelBuilder : AbstractModelBuilder("") {
+
+ override fun build(param: ProjectConnectionAndAndroidVariant): IProject {
+ val (connection, androidVariant) = param
+ val executor = connection.action { controller ->
+ val ideaProject = findIdeaProject(controller) ?: throw ModelBuilderException(
+ "IdeaProject model could not be created")
+
+ val ideaModules = ideaProject.modules
+ val rootModule = ideaModules.find { it.gradleProject.path == ":" }
+ ?: throw ModelBuilderException(
+ "No GradleProject model is associated with project path: ':'")
+
+ val rootProjectVersions = getAndroidVersions(rootModule, controller)
+
+ val rootProject = if (rootProjectVersions != null) {
+ // Root project is an Android project
+ assertMinimumAgp(rootProjectVersions)
+ AndroidProjectModelBuilder(androidVariant).build(controller to rootModule)
+ } else {
+ GradleProjectModelBuilder().build(rootModule.gradleProject)
+ }
+
+ val projects = ideaModules.map { ideaModule ->
+ ModuleProjectModelBuilder(androidVariant).build(controller to ideaModule)
+ }
+
+ ProjectImpl(rootProject, projects)
+ }
+
+ finalizeLauncher(executor)
+ applyAndroidModelBuilderProps(executor)
+
+ val logger = ILogger.newInstance("ProjectReader")
+ logger.warn("Starting build. See build output for more details...")
+
+ if (Main.client != null) {
+ Main.client.logOutput("Starting build...")
+ }
+
+ return executor.run().also {
+ logger.debug("Build action executed. Result:", it)
+ }
+ }
+
+ private fun findIdeaProject(controller: BuildController): IdeaProject? {
+ log("Creating Idea project model...")
+ return controller.findModel(IdeaProject::class.java)?.also {
+ log("IdeaProject model created...")
+ }
+ }
+
+ private fun applyAndroidModelBuilderProps(
+ launcher: ConfigurableLauncher<*>) {
+ launcher.addProperty(IAndroidProject.PROPERTY_BUILD_MODEL_ONLY, true)
+ launcher.addProperty(IAndroidProject.PROPERTY_INVOKED_FROM_IDE, true)
+ }
+
+ private fun ConfigurableLauncher<*>.addProperty(property: String, value: Any) {
+ addArguments(String.format("-P%s=%s", property, value))
+ }
+}
\ No newline at end of file
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/types.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/types.kt
new file mode 100644
index 000000000..8459b87a1
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/types.kt
@@ -0,0 +1,26 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.tooling.impl.sync
+
+import org.gradle.tooling.BuildController
+import org.gradle.tooling.ProjectConnection
+import org.gradle.tooling.model.idea.IdeaModule
+
+
+typealias BuildControllderAndIdeaModule = Pair
+typealias ProjectConnectionAndAndroidVariant = Pair
\ No newline at end of file
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/util/AnnotationUtil.java b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/util/AnnotationUtil.java
new file mode 100644
index 000000000..e3221ba35
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/util/AnnotationUtil.java
@@ -0,0 +1,160 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.tooling.impl.util;
+
+/**
+ * @author Akash Yadav
+ */
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.function.Consumer;
+import org.eclipse.lsp4j.jsonrpc.services.JsonDelegate;
+import org.eclipse.lsp4j.jsonrpc.services.JsonNotification;
+import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
+import org.eclipse.lsp4j.jsonrpc.services.JsonSegment;
+
+public final class AnnotationUtil {
+
+ private AnnotationUtil() {
+ }
+
+ public static void findDelegateSegments(Class> clazz, Set> visited,
+ Consumer acceptor) {
+ if (clazz == null || !visited.add(clazz)) {
+ return;
+ }
+ findDelegateSegments(clazz.getSuperclass(), visited, acceptor);
+ for (Class> interf : clazz.getInterfaces()) {
+ findDelegateSegments(interf, visited, acceptor);
+ }
+ for (Method method : clazz.getDeclaredMethods()) {
+ if (isDelegateMethod(method)) {
+ acceptor.accept(method);
+ }
+ }
+ }
+
+ public static boolean isDelegateMethod(Method method) {
+ if (!method.isSynthetic()) {
+ JsonDelegate jsonDelegate = method.getAnnotation(JsonDelegate.class);
+ if (jsonDelegate != null) {
+ if (!(method.getParameterCount() == 0 && method.getReturnType().isInterface())) {
+ throw new IllegalStateException(
+ "The method " + method.toString() + " is not a proper @JsonDelegate method.");
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Depth first search for annotated methods in hierarchy.
+ */
+ public static void findRpcMethods(Class> clazz, Set> visited,
+ Consumer acceptor) {
+ if (clazz == null || !visited.add(clazz)) {
+ return;
+ }
+ findRpcMethods(clazz.getSuperclass(), visited, acceptor);
+ for (Class> interf : clazz.getInterfaces()) {
+ findRpcMethods(interf, visited, acceptor);
+ }
+ String segment = getSegment(clazz);
+ for (Method method : clazz.getDeclaredMethods()) {
+ MethodInfo methodInfo = createMethodInfo(method, segment);
+ if (methodInfo != null) {
+ acceptor.accept(methodInfo);
+ }
+ }
+ }
+
+ protected static String getSegment(Class> clazz) {
+ JsonSegment jsonSegment = clazz.getAnnotation(JsonSegment.class);
+ return jsonSegment == null ? "" : jsonSegment.value() + "/";
+ }
+
+ protected static MethodInfo createMethodInfo(Method method, String segment) {
+ if (!method.isSynthetic()) {
+ JsonRequest jsonRequest = method.getAnnotation(JsonRequest.class);
+ if (jsonRequest != null) {
+ return createRequestInfo(method, segment, jsonRequest);
+ }
+ JsonNotification jsonNotification = method.getAnnotation(JsonNotification.class);
+ if (jsonNotification != null) {
+ return createNotificationInfo(method, segment, jsonNotification);
+ }
+ }
+ return null;
+ }
+
+ protected static MethodInfo createNotificationInfo(Method method, String segment,
+ JsonNotification jsonNotification) {
+ MethodInfo methodInfo = createMethodInfo(method, jsonNotification.useSegment(), segment,
+ jsonNotification.value());
+ methodInfo.isNotification = true;
+ return methodInfo;
+ }
+
+ protected static MethodInfo createRequestInfo(Method method, String segment,
+ JsonRequest jsonRequest) {
+ return createMethodInfo(method, jsonRequest.useSegment(), segment, jsonRequest.value());
+ }
+
+ protected static MethodInfo createMethodInfo(Method method, boolean useSegment, String segment,
+ String value) {
+ method.setAccessible(true);
+
+ MethodInfo methodInfo = new MethodInfo();
+ methodInfo.method = method;
+ methodInfo.parameterTypes = getParameterTypes(method);
+ methodInfo.name = getMethodName(method, useSegment, segment, value);
+ return methodInfo;
+ }
+
+ protected static String getMethodName(Method method, boolean useSegment, String segment,
+ String value) {
+ String name = value != null && value.length() > 0 ? value : method.getName();
+ return useSegment ? segment + name : name;
+ }
+
+ protected static Type[] getParameterTypes(Method method) {
+ return Arrays.stream(method.getParameters()).map(t -> t.getParameterizedType())
+ .toArray(Type[]::new);
+ }
+
+ public static class MethodInfo {
+
+ private static Type[] EMPTY_TYPE_ARRAY = {};
+ public String name;
+ public Method method;
+ public Type[] parameterTypes = EMPTY_TYPE_ARRAY;
+ public boolean isNotification = false;
+ }
+
+ public static class DelegateInfo {
+
+ public Method method;
+ public Object delegate;
+ }
+}
+
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/util/ProjectReader.java b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/util/ProjectReader.java
index 1278f4da8..e8d05dcc4 100644
--- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/util/ProjectReader.java
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/util/ProjectReader.java
@@ -17,42 +17,25 @@
package com.itsaky.androidide.tooling.impl.util;
-import static com.itsaky.androidide.tooling.impl.Main.finalizeLauncher;
import static com.itsaky.androidide.utils.ILogger.MSG_SEPARATOR;
import static java.util.Collections.emptyList;
import com.android.builder.model.ModelBuilderParameter;
-import com.android.builder.model.v2.ide.GraphItem;
-import com.android.builder.model.v2.ide.Library;
import com.android.builder.model.v2.ide.ProjectType;
import com.android.builder.model.v2.models.AndroidProject;
import com.android.builder.model.v2.models.BasicAndroidProject;
import com.android.builder.model.v2.models.ProjectSyncIssues;
import com.android.builder.model.v2.models.VariantDependencies;
import com.android.builder.model.v2.models.Versions;
-import com.itsaky.androidide.builder.model.DefaultLibrary;
import com.itsaky.androidide.builder.model.DefaultProjectSyncIssues;
import com.itsaky.androidide.builder.model.IJavaCompilerSettings;
-import com.itsaky.androidide.tooling.api.model.AndroidModule;
-import com.itsaky.androidide.tooling.api.model.GradleArtifact;
-import com.itsaky.androidide.tooling.api.model.GradleTask;
-import com.itsaky.androidide.tooling.api.model.IdeGradleProject;
-import com.itsaky.androidide.tooling.api.model.JavaContentRoot;
-import com.itsaky.androidide.tooling.api.model.JavaModule;
-import com.itsaky.androidide.tooling.api.model.JavaModuleCompilerSettings;
-import com.itsaky.androidide.tooling.api.model.JavaModuleDependency;
-import com.itsaky.androidide.tooling.api.model.JavaModuleExternalDependency;
-import com.itsaky.androidide.tooling.api.model.JavaModuleProjectDependency;
-import com.itsaky.androidide.tooling.api.model.JavaSourceDirectory;
-import com.itsaky.androidide.tooling.api.model.util.AndroidModulePropertyCopier;
-import com.itsaky.androidide.tooling.api.model.util.ProjectBuilder;
import com.itsaky.androidide.tooling.impl.Main;
-import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.LogUtils;
-
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
import org.gradle.api.JavaVersion;
import org.gradle.tooling.BuildController;
-import org.gradle.tooling.ConfigurableLauncher;
import org.gradle.tooling.ProjectConnection;
import org.gradle.tooling.UnknownModelException;
import org.gradle.tooling.model.GradleProject;
@@ -63,492 +46,416 @@ import org.gradle.tooling.model.idea.IdeaProject;
import org.gradle.tooling.model.idea.IdeaSingleEntryLibraryDependency;
import org.jetbrains.annotations.Nullable;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutionException;
-
-import kotlin.Pair;
-
/**
* @author Akash Yadav
*/
public class ProjectReader {
- /** All modules mapped by their names (not paths). */
- private static final Map allModules = new ConcurrentHashMap<>();
-
- public static IdeGradleProject read(
- ProjectConnection connection, Map outIssues)
- throws ExecutionException, InterruptedException {
- final var buildActionExecutor =
- connection.action(
- controller -> {
- log("Creating Idea project model...");
- final var ideaProject = controller.findModel(IdeaProject.class);
- log("IdeaProject model created...");
- return buildGradleProjectModel(ideaProject, controller, outIssues);
- });
- finalizeLauncher(buildActionExecutor);
- applyAndroidModelBuilderProps(buildActionExecutor);
-
- final var logger = ILogger.newInstance("ProjectReader");
- logger.warn("Starting build. See build output for more details...");
-
- if (Main.client != null) {
- Main.client.logOutput("Starting build...");
- }
-
- final var project = buildActionExecutor.run();
-
- // Fetch java module dependencies
- final var ideaProject = connection.model(IdeaProject.class).get();
- final var modules = ideaProject.getModules();
-
- for (var module : modules) {
- allModules.put(module.getName(), module);
- }
-
- for (final var module : modules) {
- final var moduleProject = project.findByPath(module.getGradleProject().getPath()).get();
- if (!(moduleProject instanceof JavaModule)) {
- continue;
- }
-
- final var dependencies = ((JavaModule) moduleProject).getJavaDependencies();
- dependencies.clear();
- dependencies.addAll(collectJavaDependencies(module));
- }
-
- allModules.clear();
-
- return project;
- }
-
- public static AndroidModule buildAndroidModuleProject(
- GradleProject gradle, AndroidProject android, ProjectType type) {
- log("Building AndroidModule for project: " + gradle.getName());
- final var builder = new ProjectBuilder();
- final var copier = AndroidModulePropertyCopier.INSTANCE;
- builder.setName(gradle.getName());
- builder.setDescription(gradle.getDescription());
- builder.setPath(gradle.getPath());
- builder.setProjectDir(gradle.getProjectDirectory());
- builder.setBuildDir(gradle.getBuildDirectory());
- builder.setBuildScript(gradle.getBuildScript().getSourceFile());
- builder.setSimpleVariants(copier.asSimpleVariants(android.getVariants()));
- builder.setJavaCompileOptions(copier.copy(android.getJavaCompileOptions()));
- builder.setResourcePrefix(android.getResourcePrefix());
- builder.setDynamicFeatures(android.getDynamicFeatures());
- builder.setViewBindingOptions(copier.copy(android.getViewBindingOptions()));
- builder.setFlags(copier.copy(android.getFlags()));
- builder.setModelSyncFiles(emptyList());
- builder.setLintChecksJars(android.getLintChecksJars());
- builder.setProjectType(type);
-
- final var module = builder.buildAndroidModule();
- tryFillNamespaces(android, module);
- addTasks(gradle, module);
-
- return module;
- }
-
- private static void tryFillNamespaces(final AndroidProject android, final AndroidModule module) {
- try {
- module.setNamespace(android.getNamespace());
- module.setAndroidTestNamespace(android.getAndroidTestNamespace());
- module.setTestFixturesNamespace(android.getTestFixturesNamespace());
- } catch (Throwable err) {
- log("Unable to get namespaces of module: '" + module.projectPath + "'.", err.getMessage());
- }
- }
-
- private static void addTasks(GradleProject gradle, IdeGradleProject project) {
- for (final var task : gradle.getTasks()) {
- project.tasks.add(buildGradleTaskModel(project, task));
- }
- }
-
- private static GradleTask buildGradleTaskModel(
- IdeGradleProject project, org.gradle.tooling.model.GradleTask task) {
- return new GradleTask(
- task.getName(),
- task.getDescription(),
- task.getGroup(),
- task.getPath(),
- task.getDisplayName(),
- task.isPublic(),
- project.projectPath);
- }
-
- private static IdeGradleProject buildModuleProject(
- BuildController controller,
- final IdeaProject ideaProject,
- IdeaModule ideaModule,
- Map outIssues) {
- return buildModuleProject(controller, ideaProject, ideaModule, outIssues, false);
- }
-
- private static IdeGradleProject buildModuleProject(
- BuildController controller,
- final IdeaProject ideaProject,
- IdeaModule ideaModule,
- Map outIssues,
- boolean androidOnly) {
-
- final var gradle = ideaModule.getGradleProject();
-
- try {
- final var info = createAndroidModelInfo(gradle, controller);
- if (info == null) {
- throw new NotAnAndroidProjectException(gradle.getName());
- }
-
- outIssues.put(gradle.getPath(), info.getSyncIssues());
- return info.getProject();
- } catch (Throwable error) {
-
- if (androidOnly) {
- if (error instanceof NotAnAndroidProjectException && gradle.getParent() == null) {
- // A root project is not an Android project in most cases
- // do not emit misleading logs
- return null;
- }
-
- log(error.getMessage());
- return null;
- }
-
- try {
- log("Building IdeGradleProject model for project: " + gradle.getPath());
- if (!(error instanceof UnknownModelException)) {
- // If the error is something else than UnknownModelException, we should log it
- error.printStackTrace();
- }
-
- return buildJavaModuleProject(gradle, ideaProject, ideaModule);
- } catch (Throwable e) {
- log("Unable to create model for project");
- e.printStackTrace();
- return null;
- }
- }
- }
-
- private static ModelInfoContainer createAndroidModelInfo(
- GradleProject gradle, BuildController controller) {
- final var start = System.currentTimeMillis();
- final var versions = controller.findModel(gradle, Versions.class);
- if (versions == null) {
- return null;
- }
-
- if (versions.getAgp().compareTo(Main.MIN_SUPPORTED_AGP_VERSION) < 0) {
- throw new UnsupportedOperationException(
- "Android Gradle Plugin version "
- + versions.getAgp()
- + " is not supported by AndroidIDE. "
- + "Please update your project to use at least v"
- + Main.MIN_SUPPORTED_AGP_VERSION
- + " of Android Gradle Plugin to build this project.");
- }
-
- final var basicAndroid = controller.findModel(gradle, BasicAndroidProject.class);
-
- log("Fetching Android project model...");
- final var android = controller.findModel(gradle, AndroidProject.class);
- final var module = buildAndroidModuleProject(gradle, android, basicAndroid.getProjectType());
- module.setBootClassPaths(basicAndroid.getBootClasspath());
- module.setMainSourceSet(
- basicAndroid.getMainSourceSet() == null
- ? null
- : AndroidModulePropertyCopier.INSTANCE.copy(basicAndroid.getMainSourceSet()));
-
- log("Fetching 'debug' dependencies");
- final var variantDependencies =
- controller.findModel(
- gradle,
- VariantDependencies.class,
- ModelBuilderParameter.class,
- it -> it.setVariantName("debug"));
-
- final var dependencies = getAllCompileDependencies(variantDependencies);
- module.setLibraryMap(dependencies.getFirst());
- module.setLibraries(dependencies.getSecond());
-
- log("Fetching project sync issues...");
- final var issues = controller.findModel(gradle, ProjectSyncIssues.class);
- final var syncIssues =
- issues == null
- ? new DefaultProjectSyncIssues(emptyList())
- : AndroidModulePropertyCopier.INSTANCE.copy(issues);
-
- log(
- "Android project model for project '" + gradle.getName() + "' created in",
- System.currentTimeMillis() - start,
- "ms");
- return new ModelInfoContainer(module, syncIssues);
- }
-
- private static Pair