diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt
index cd3361e4c..05db7993e 100644
--- a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt
+++ b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt
@@ -50,6 +50,10 @@ import com.itsaky.androidide.tasks.executeWithProgress
import com.itsaky.androidide.tooling.api.messages.AndroidInitializationParams
import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams
import com.itsaky.androidide.tooling.api.messages.result.InitializeResult
+import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
+import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_DIRECTORY_INACCESSIBLE
+import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_NOT_DIRECTORY
+import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_NOT_FOUND
import com.itsaky.androidide.tooling.api.models.BuildVariantInfo
import com.itsaky.androidide.tooling.api.models.mapToSelectedVariants
import com.itsaky.androidide.utils.DURATION_INDEFINITE
@@ -394,7 +398,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
if (result == null || !result.isSuccessful || error != null) {
log.error("An error occurred initializing the project with Tooling API", error)
ThreadUtils.runOnUiThread {
- postProjectInit(false)
+ postProjectInit(false, result?.failure)
}
return@whenCompleteAsync
}
@@ -464,7 +468,9 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
manager.notifyProjectUpdate()
updateBuildVariants(manager.androidBuildVariants)
- com.itsaky.androidide.tasks.runOnUiThread { postProjectInit(true) }
+ com.itsaky.androidide.tasks.runOnUiThread {
+ postProjectInit(true, null)
+ }
}
}
@@ -473,11 +479,23 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
editorViewModel.isInitializing = true
}
- protected open fun postProjectInit(isSuccessful: Boolean) {
+ protected open fun postProjectInit(isSuccessful: Boolean, failure: TaskExecutionResult.Failure?) {
val manager = ProjectManagerImpl.getInstance()
if (!isSuccessful) {
- setStatus(getString(string.msg_project_initialization_failed))
- flashError(string.msg_project_initialization_failed)
+ val initFailed = getString(string.msg_project_initialization_failed)
+ setStatus(initFailed)
+
+ val msg = when(failure) {
+ PROJECT_DIRECTORY_INACCESSIBLE -> string.msg_project_dir_inaccessible
+ PROJECT_NOT_DIRECTORY -> string.msg_file_is_not_dir
+ PROJECT_NOT_FOUND -> string.msg_project_dir_doesnt_exist
+ else -> null
+ }?.let {
+ "$initFailed: ${getString(it)}"
+ }
+
+ flashError(msg ?: initFailed)
+
editorViewModel.isInitializing = false
manager.projectInitialized = false
return
diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml
index 96e75f770..c6d800f72 100644
--- a/resources/src/main/res/values/strings.xml
+++ b/resources/src/main/res/values/strings.xml
@@ -622,4 +622,7 @@
Language
Select a language for AndroidIDE.
System Default
+ Project directory is not accessible
+ Selected file is not a directory
+ Project directory does not exist
diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManagerImpl.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManagerImpl.kt
index 361fe6535..6d0b6fd72 100644
--- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManagerImpl.kt
+++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManagerImpl.kt
@@ -219,6 +219,10 @@ class ProjectManagerImpl : IProjectManager, EventReceiver {
return
}
+ if (builder.isBuildInProgress) {
+ return
+ }
+
val tasks = getAndroidModules().flatMap { module ->
val variant = module.getSelectedVariant()
if (variant == null) {
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/ToolingApiServerImpl.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/ToolingApiServerImpl.kt
index fe8a77373..fdf4d9668 100644
--- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/ToolingApiServerImpl.kt
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/ToolingApiServerImpl.kt
@@ -35,6 +35,10 @@ import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Fai
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.BUILD_FAILED
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.CONNECTION_CLOSED
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.CONNECTION_ERROR
+import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_DIRECTORY_INACCESSIBLE
+import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_NOT_DIRECTORY
+import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_NOT_FOUND
+import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_NOT_INITIALIZED
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.UNKNOWN
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.UNSUPPORTED_BUILD_ARGUMENT
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.UNSUPPORTED_CONFIGURATION
@@ -57,7 +61,6 @@ import org.gradle.tooling.internal.consumer.DefaultGradleConnector
import java.io.File
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException
-import java.util.concurrent.atomic.AtomicBoolean
import kotlin.system.exitProcess
/**
@@ -68,7 +71,6 @@ import kotlin.system.exitProcess
internal class ToolingApiServerImpl(private val project: ProjectImpl) :
IToolingApiServer {
- private val _isInitialized = AtomicBoolean(false)
private var client: IToolingApiClient? = null
private var connector: GradleConnector? = null
private var connection: ProjectConnection? = null
@@ -79,9 +81,13 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
/**
* Whether the project has been initialized or not.
*/
- var isInitialized: Boolean
- get() = _isInitialized.get()
- private set(value) = _isInitialized.set(value)
+ var isInitialized: Boolean = false
+ private set
+
+ /**
+ * Whether a build or project synchronization is in progress.
+ */
+ private var isBuildInProgress: Boolean = false
/**
* Whether the server has a live connection to Gradle.
@@ -102,7 +108,7 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
}
override fun initialize(params: InitializeProjectParams): CompletableFuture {
- return CompletableFuture.supplyAsync {
+ return runBuild {
try {
log.debug("Got initialize request", params)
@@ -112,6 +118,14 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
cancelCurrentBuild().get()
}
+ val projectDirectory = File(params.directory)
+ val failureReason = validateProjectDirectory(projectDirectory)
+
+ if (failureReason != null) {
+ log.error("Cannot initialize project: $failureReason")
+ return@runBuild InitializeResult(false, failureReason)
+ }
+
val stopWatch = StopWatch("Connection to project")
val isReinitializing = connector != null && connection != null && params == lastInitParams
@@ -123,7 +137,7 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
// or the project is being initialized with different parameters
connector?.disconnect()
- connector = GradleConnector.newConnector().forProjectDirectory(File(params.directory))
+ connector = GradleConnector.newConnector().forProjectDirectory(projectDirectory)
setupConnectorForGradleInstallation(this.connector!!, params.gradleDistribution)
stopWatch.lap("Connector created")
}
@@ -163,15 +177,24 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
this.isInitialized = true
notifyBuildSuccess(emptyList())
- return@supplyAsync InitializeResult(true)
+ return@runBuild InitializeResult(true)
} catch (err: Throwable) {
log.error("Failed to initialize project", err)
notifyBuildFailure(emptyList())
- return@supplyAsync InitializeResult(false)
+ return@runBuild InitializeResult(false, getTaskFailureType(err))
}
}
}
+ private fun validateProjectDirectory(
+ projectDirectory: File
+ ) = when {
+ !projectDirectory.exists() -> PROJECT_NOT_FOUND
+ !projectDirectory.isDirectory -> PROJECT_NOT_DIRECTORY
+ !projectDirectory.canRead() -> PROJECT_DIRECTORY_INACCESSIBLE
+ else -> null
+ }
+
override fun isServerInitialized(): CompletableFuture {
return CompletableFuture.supplyAsync { isInitialized }
}
@@ -184,8 +207,21 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
}
override fun executeTasks(message: TaskExecutionMessage): CompletableFuture {
- return CompletableFuture.supplyAsync {
- assertProjectInitialized()
+ return runBuild {
+ if (!isServerInitialized().get()) {
+ log.error("Cannot execute tasks: $PROJECT_NOT_INITIALIZED")
+ return@runBuild TaskExecutionResult(false, PROJECT_NOT_INITIALIZED)
+ }
+
+ val lastInitParams = this.lastInitParams
+ if (lastInitParams != null) {
+ val projectDirectory = File(lastInitParams.directory)
+ val failureReason = validateProjectDirectory(projectDirectory)
+ if (failureReason != null) {
+ log.error("Cannot execute tasks: $failureReason")
+ return@runBuild TaskExecutionResult(isSuccessful = false, failureReason)
+ }
+ }
log.debug("Received request to run tasks.", message)
@@ -215,10 +251,10 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
builder.run()
this.buildCancellationToken = null
notifyBuildSuccess(message.tasks)
- return@supplyAsync TaskExecutionResult.SUCCESS
+ return@runBuild TaskExecutionResult.SUCCESS
} catch (error: Throwable) {
notifyBuildFailure(message.tasks)
- return@supplyAsync TaskExecutionResult(false, getTaskFailureType(error), error.message)
+ return@runBuild TaskExecutionResult(false, getTaskFailureType(error))
}
}
}
@@ -330,6 +366,26 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
else -> UNKNOWN
}
+ private inline fun supplyAsync(crossinline action: () -> T): CompletableFuture =
+ CompletableFuture.supplyAsync {
+ action()
+ }
+
+ private inline fun runBuild(crossinline action: () -> T): CompletableFuture =
+ supplyAsync {
+ if (isBuildInProgress) {
+ log.error("Cannot run build, build is already in prorgess!")
+ throw IllegalStateException("Build is already in progress")
+ }
+
+ isBuildInProgress = true
+ try {
+ action()
+ } finally {
+ isBuildInProgress = false
+ }
+ }
+
private fun assertProjectInitialized() {
if (!isServerInitialized().get()) {
throw CompletionException(IllegalStateException("Project is not initialized!"))
diff --git a/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt b/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt
index 2e74c7caa..f38b9e056 100644
--- a/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt
+++ b/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt
@@ -28,6 +28,7 @@ import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.IToolingApiServer
import com.itsaky.androidide.tooling.api.ProjectType
import com.itsaky.androidide.tooling.api.messages.TaskExecutionMessage
+import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
import com.itsaky.androidide.tooling.api.models.AndroidProjectMetadata
import com.itsaky.androidide.tooling.api.models.JavaModuleExternalDependency
import com.itsaky.androidide.tooling.api.models.JavaModuleProjectDependency
@@ -37,11 +38,22 @@ import com.itsaky.androidide.tooling.events.ProgressEvent
import com.itsaky.androidide.tooling.events.task.TaskStartEvent
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher.MultiVersionTestClient
+import com.itsaky.androidide.utils.FileProvider
import com.itsaky.androidide.utils.ILogger
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import java.nio.file.Paths
+import java.nio.file.attribute.PosixFilePermission
import java.util.concurrent.CompletableFuture
+import kotlin.io.path.ExperimentalPathApi
+import kotlin.io.path.createDirectories
+import kotlin.io.path.createFile
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.deleteRecursively
+import kotlin.io.path.exists
+import kotlin.io.path.setPosixFilePermissions
+import kotlin.io.path.writeText
/** @author Akash Yadav */
@RunWith(JUnit4::class)
@@ -54,6 +66,26 @@ class MultiModuleAndroidProjectTest {
doAssertions(project, server)
}
+ @Test
+ fun `test non-existing project initialization`() {
+ val (_, _, result) = ToolingApiTestLauncher().launchServer(
+ projectDir = Paths.get("/directory/does/not/exist/"))
+ assertThat(result?.isSuccessful).isFalse()
+ assertThat(result?.failure).isEqualTo(TaskExecutionResult.Failure.PROJECT_NOT_FOUND)
+ }
+
+ @Test
+ fun `test project initialization with file path`() {
+ val path = FileProvider.projectRoot().resolve("build").resolve("should-be-directory.txt")
+ path.deleteIfExists()
+ path.createFile()
+
+ val (_, _, result) = ToolingApiTestLauncher().launchServer(
+ projectDir = path)
+ assertThat(result?.isSuccessful).isFalse()
+ assertThat(result?.failure).isEqualTo(TaskExecutionResult.Failure.PROJECT_NOT_DIRECTORY)
+ }
+
@Test
fun `test task name should be shown if gradle configuration cache is enabled`() {
diff --git a/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/result/InitializeResult.kt b/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/result/InitializeResult.kt
index 874c75980..2a3ca7820 100644
--- a/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/result/InitializeResult.kt
+++ b/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/result/InitializeResult.kt
@@ -23,4 +23,4 @@ package com.itsaky.androidide.tooling.api.messages.result
* @param isSuccessful Whether the project initialization was successful.
* @author Akash Yadav
*/
-class InitializeResult(val isSuccessful: Boolean)
+class InitializeResult(val isSuccessful: Boolean, val failure: TaskExecutionResult.Failure? = TaskExecutionResult.Failure.UNKNOWN)
diff --git a/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/result/TaskExecutionResult.kt b/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/result/TaskExecutionResult.kt
index 551860258..81d5452a4 100644
--- a/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/result/TaskExecutionResult.kt
+++ b/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/result/TaskExecutionResult.kt
@@ -22,11 +22,10 @@ package com.itsaky.androidide.tooling.api.messages.result
*
* @param isSuccessful The result of the task execution.
* @param failure The type of failure. Non-null only if [isSuccessful] is `false`.
- * @param failureMessage The message describing the failure.
*
* @author Akash Yadav
*/
-data class TaskExecutionResult(val isSuccessful: Boolean, val failure: Failure?, val failureMessage: String?) {
+data class TaskExecutionResult(val isSuccessful: Boolean, val failure: Failure?) {
companion object {
@@ -34,11 +33,14 @@ data class TaskExecutionResult(val isSuccessful: Boolean, val failure: Failure?,
* Result for a successful build.
*/
@JvmStatic
- val SUCCESS = TaskExecutionResult(true, null, null)
+ val SUCCESS = TaskExecutionResult(true, null)
}
enum class Failure {
PROJECT_NOT_FOUND,
+ PROJECT_NOT_INITIALIZED,
+ PROJECT_NOT_DIRECTORY,
+ PROJECT_DIRECTORY_INACCESSIBLE,
UNKNOWN,
UNSUPPORTED_GRADLE_VERSION,
UNSUPPORTED_CONFIGURATION,