Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ abstract class BaseBuildAction : EditorActivityAction() {

/**
* Refuses a tap while an internal build (Quick Build's proxy app build) holds the one Gradle
* slot, and flashes why. [prepare] leaves these actions enabled in that window, and starting a
* second build would throw BuildInProgressException deep in the service and surface as a raw
* slot, and flashes why. [prepare] leaves these actions enabled in that window, and the service
* would refuse a second build, which surfaces only as a generic "A build is already running!"
Comment thread
jatezzz marked this conversation as resolved.
* error. Reads the raw [BuildService.isBuildInProgress], because this guards the slot rather
* than presenting state.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.BaseBuildAction
import com.itsaky.androidide.actions.getContext
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
import com.itsaky.androidide.plugins.extensions.CommandOutput
import com.itsaky.androidide.plugins.extensions.CommandResult
import com.itsaky.androidide.plugins.extensions.CommandSpec
import com.itsaky.androidide.plugins.manager.build.PluginBuildActionManager
import com.itsaky.androidide.plugins.manager.build.RegisteredBuildAction
import com.itsaky.androidide.plugins.manager.core.PluginManager
Expand All @@ -19,135 +24,154 @@ import com.itsaky.androidide.plugins.services.IdeCommandService
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.resolveAttr
import com.itsaky.androidide.viewmodel.BottomSheetViewModel
import com.google.android.material.bottomsheet.BottomSheetBehavior
import androidx.lifecycle.lifecycleScope
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class PluginBuildActionItem(
Comment thread
jatezzz marked this conversation as resolved.
context: Context,
private val registered: RegisteredBuildAction,
override val order: Int
context: Context,
private val registered: RegisteredBuildAction,
override val order: Int,
) : BaseBuildAction() {

override val id: String = "plugin.build.${registered.pluginId}.${registered.action.id}"

init {
label = registered.action.name
icon = resolvePluginIcon(context)
location = ActionItem.Location.EDITOR_TOOLBAR
requiresUIThread = true
}

override fun prepare(data: ActionData) {
val context = data.getActivity()
if (context == null) {
visible = false
return
}
visible = true

val manager = PluginBuildActionManager.getInstance()
val isRunning = manager.isActionRunning(registered.pluginId, registered.action.id)

if (isRunning) {
label = "Cancel ${registered.action.name}"
icon = ContextCompat.getDrawable(context, R.drawable.ic_stop)
enabled = true
} else {
label = registered.action.name
icon = resolvePluginIcon(context)
enabled = true
}
}

override fun createColorFilter(data: ActionData): ColorFilter? {
val context = data.getContext() ?: return null
val isRunning = PluginBuildActionManager.getInstance()
.isActionRunning(registered.pluginId, registered.action.id)
val attr = if (isRunning) R.attr.colorError else R.attr.colorOnSurface
return PorterDuffColorFilter(
context.resolveAttr(attr),
PorterDuff.Mode.SRC_ATOP
)
}

private fun resolvePluginIcon(fallbackContext: Context): Drawable? {
val iconResId = registered.action.icon ?: return ContextCompat.getDrawable(fallbackContext, R.drawable.ic_run_outline)
return PluginDrawableResolver.resolve(iconResId, registered.pluginId, fallbackContext)
?: ContextCompat.getDrawable(fallbackContext, R.drawable.ic_run_outline)
}

override suspend fun execAction(data: ActionData): Any {
val manager = PluginBuildActionManager.getInstance()
val pluginId = registered.pluginId
val actionId = registered.action.id

if (manager.isActionRunning(pluginId, actionId)) {
manager.cancelAction(pluginId, actionId)
data.getActivity()?.let { resetProgressIfIdle(it) }
return true
}

val activity = data.getActivity() ?: return false

val pluginManager = PluginManager.getInstance() ?: return false
val loadedPlugin = pluginManager.getLoadedPlugin(pluginId) ?: return false
val commandService = loadedPlugin.context.services.get(IdeCommandService::class.java)
?: return false

val execution = manager.executeAction(pluginId, actionId, commandService) ?: return false

activity.editorViewModel.isBuildInProgress = true
val currentSheetState = activity.bottomSheetViewModel.sheetBehaviorState
val targetState = if (currentSheetState == BottomSheetBehavior.STATE_HIDDEN)
BottomSheetBehavior.STATE_COLLAPSED else currentSheetState
activity.bottomSheetViewModel.setSheetState(
sheetState = targetState,
currentTab = BottomSheetViewModel.TAB_BUILD_OUTPUT
)
activity.appendBuildOutput("━━━ ${registered.action.name} ━━━")
activity.invalidateOptionsMenu()

activity.lifecycleScope.launch(Dispatchers.Default) {
runCatching {
execution.output.collect { output ->
val line = when (output) {
is CommandOutput.StdOut -> output.line
is CommandOutput.StdErr -> output.line
is CommandOutput.ExitCode ->
if (output.code != 0) "Process failed with code ${output.code}" else null
}
if (line != null) {
withContext(Dispatchers.Main) {
activity.appendBuildOutput(line)
}
}
}

val result = execution.await()
manager.notifyActionCompleted(pluginId, actionId, result)
withContext(Dispatchers.Main) { resetProgressIfIdle(activity) }
}.onFailure { e ->
if (e is CancellationException) {
manager.cancelAction(pluginId, actionId)
throw e
}
withContext(Dispatchers.Main) { resetProgressIfIdle(activity) }
}
}

return true
}

private fun resetProgressIfIdle(activity: EditorHandlerActivity) {
val manager = PluginBuildActionManager.getInstance()
if (buildService?.isBuildInProgress != true && !manager.hasActiveExecutions()) {
activity.editorViewModel.isBuildInProgress = false
}
activity.invalidateOptionsMenu()
}
override val id: String = "plugin.build.${registered.pluginId}.${registered.action.id}"

init {
label = registered.action.name
icon = resolvePluginIcon(context)
location = ActionItem.Location.EDITOR_TOOLBAR
requiresUIThread = true
}

override fun prepare(data: ActionData) {
val context = data.getActivity()
if (context == null) {
visible = false
return
}
visible = true

val manager = PluginBuildActionManager.getInstance()
val isRunning = manager.isActionRunning(registered.pluginId, registered.action.id)

if (isRunning) {
label = "Cancel ${registered.action.name}"
icon = ContextCompat.getDrawable(context, R.drawable.ic_stop)
enabled = true
} else {
label = registered.action.name
icon = resolvePluginIcon(context)
enabled = true
}
}

override fun createColorFilter(data: ActionData): ColorFilter? {
val context = data.getContext() ?: return null
val isRunning =
PluginBuildActionManager
.getInstance()
.isActionRunning(registered.pluginId, registered.action.id)
val attr = if (isRunning) R.attr.colorError else R.attr.colorOnSurface
return PorterDuffColorFilter(
context.resolveAttr(attr),
PorterDuff.Mode.SRC_ATOP,
)
}

private fun resolvePluginIcon(fallbackContext: Context): Drawable? {
val iconResId = registered.action.icon ?: return ContextCompat.getDrawable(fallbackContext, R.drawable.ic_run_outline)
return PluginDrawableResolver.resolve(iconResId, registered.pluginId, fallbackContext)
?: ContextCompat.getDrawable(fallbackContext, R.drawable.ic_run_outline)
}

override suspend fun execAction(data: ActionData): Any {
val manager = PluginBuildActionManager.getInstance()
val pluginId = registered.pluginId
val actionId = registered.action.id

if (manager.isActionRunning(pluginId, actionId)) {
manager.cancelAction(pluginId, actionId)
data.getActivity()?.let { resetProgressIfIdle(it) }
return true
}

val activity = data.getActivity() ?: return false

val pluginManager = PluginManager.getInstance() ?: return false
val loadedPlugin = pluginManager.getLoadedPlugin(pluginId) ?: return false
val commandService =
loadedPlugin.context.services.get(IdeCommandService::class.java)
?: return false

val execution = manager.executeAction(pluginId, actionId, commandService) ?: return false

activity.editorViewModel.isBuildInProgress = true
val currentSheetState = activity.bottomSheetViewModel.sheetBehaviorState
val targetState =
if (currentSheetState == BottomSheetBehavior.STATE_HIDDEN) {
BottomSheetBehavior.STATE_COLLAPSED
} else {
currentSheetState
}
activity.bottomSheetViewModel.setSheetState(
sheetState = targetState,
currentTab = BottomSheetViewModel.TAB_BUILD_OUTPUT,
)
// The tooling server streams a Gradle task's output into the pane itself, and clears the
// pane when the build starts, so a header would be wiped.
val echoOutput = registered.action.command !is CommandSpec.GradleTask
if (echoOutput) activity.appendBuildOutput("━━━ ${registered.action.name} ━━━")
activity.invalidateOptionsMenu()

activity.lifecycleScope.launch(Dispatchers.Default) {
runCatching {
execution.output.collect { output ->
if (!echoOutput) return@collect
val line =
when (output) {
is CommandOutput.StdOut -> {
output.line
}

is CommandOutput.StdErr -> {
output.line
}

is CommandOutput.ExitCode -> {
if (output.code != 0) "Process failed with code ${output.code}" else null
}
}
if (line != null) {
withContext(Dispatchers.Main) {
activity.appendBuildOutput(line)
}
}
}

val result = execution.await()
// A refused or timed-out task never reached the pane, so say why here.
if (!echoOutput && result is CommandResult.Failure && result.exitCode == -1) {
result.error?.let { withContext(Dispatchers.Main) { activity.appendBuildOutput(it) } }
}
manager.notifyActionCompleted(pluginId, actionId, result)
withContext(Dispatchers.Main) { resetProgressIfIdle(activity) }
}.onFailure { e ->
if (e is CancellationException) {
manager.cancelAction(pluginId, actionId)
throw e
}
withContext(Dispatchers.Main) { resetProgressIfIdle(activity) }
}
}

return true
}

private fun resetProgressIfIdle(activity: EditorHandlerActivity) {
val manager = PluginBuildActionManager.getInstance()
if (buildService?.isBuildInProgress != true && !manager.hasActiveExecutions()) {
activity.editorViewModel.isBuildInProgress = false
}
activity.invalidateOptionsMenu()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ 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.BUILD_CANCELLED
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.BUILD_IN_PROGRESS
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.CACHE_READ_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
Expand Down Expand Up @@ -1703,6 +1704,7 @@ abstract class ProjectHandlerActivity :
PROJECT_NOT_DIRECTORY -> string.msg_file_is_not_dir
PROJECT_NOT_FOUND -> string.msg_project_dir_doesnt_exist
CACHE_READ_ERROR -> string.msg_project_cache_read_failure
BUILD_IN_PROGRESS -> string.build_in_progress_warning
else -> null
}?.let {
"$initFailed: ${getString(it)}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ import com.itsaky.androidide.managers.ToolsManager
import com.itsaky.androidide.plugins.PluginLogger
import com.itsaky.androidide.plugins.base.PluginFragmentHelper
import com.itsaky.androidide.plugins.manager.core.PluginManager
import com.itsaky.androidide.plugins.manager.services.IdeBuildServiceImpl
import com.itsaky.androidide.plugins.manager.services.IdeLogServiceImpl
import com.itsaky.androidide.plugins.manager.services.IdeTerminalServiceImpl
import com.itsaky.androidide.preferences.internal.DevOpsPreferences
import com.itsaky.androidide.preferences.internal.GeneralPreferences
import com.itsaky.androidide.resources.localization.LocaleProvider
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
import com.itsaky.androidide.ui.themes.IDETheme
import com.itsaky.androidide.ui.themes.IThemeManager
import com.itsaky.androidide.utils.EditorDecorationBridge
Expand Down Expand Up @@ -393,15 +396,14 @@ internal object CredentialProtectedApplicationLoader : ApplicationLoader {
setupBuildServiceProviders()
setupProjectManipulationProviders()
IdeLogServiceImpl.getInstance().setLogReader(LogsProvider::read)
IdeTerminalServiceImpl.setSessionLauncher(PluginTerminalLauncher { application.foregroundActivity })
logger.info("Plugin services configured successfully")
}
}

@OptIn(DelicateCoroutinesApi::class)
private fun setupBuildServiceProviders() {
val buildServiceImpl =
com.itsaky.androidide.plugins.manager.services.IdeBuildServiceImpl
.getInstance()
val buildServiceImpl = IdeBuildServiceImpl.getInstance()

// Provide runApp functionality
buildServiceImpl.setRunAppProvider { callback ->
Expand All @@ -423,7 +425,12 @@ internal object CredentialProtectedApplicationLoader : ApplicationLoader {
val result = buildService.executeTasks(listOf("generateDebugSources")).get()

if (result == null || !result.isSuccessful) {
val errorMsg = result?.failure?.toString() ?: "Unknown error"
val errorMsg =
when (val failure = result?.failure) {
null -> "Unknown error"
TaskExecutionResult.Failure.BUILD_IN_PROGRESS -> IdeBuildServiceImpl.BUILD_IN_PROGRESS_REASON
else -> failure.toString()
}
logger.error("Gradle sync failed: {}", errorMsg)
callback.onComplete(false, "Gradle sync failed: $errorMsg")
} else {
Expand Down
Loading
Loading