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 @@ -34,6 +34,7 @@ import com.itsaky.androidide.analytics.gradle.BuildStartedMetric
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.app.IDEApplication
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.java.debug.JdwpOptions
import com.itsaky.androidide.managers.ToolsManager
import com.itsaky.androidide.preferences.internal.BuildPreferences
import com.itsaky.androidide.preferences.internal.DevOpsPreferences
Expand All @@ -45,10 +46,11 @@ import com.itsaky.androidide.services.builder.ToolingServerRunner.OnServerStartL
import com.itsaky.androidide.tasks.ifCancelledOrInterrupted
import com.itsaky.androidide.tasks.runOnUiThread
import com.itsaky.androidide.tooling.api.ForwardingToolingApiClient
import com.itsaky.androidide.tooling.api.GradlePluginConfig.PROPERTY_JDWP_ENABLED
import com.itsaky.androidide.tooling.api.GradlePluginConfig.PROPERTY_LOGSENDER_AAR
import com.itsaky.androidide.tooling.api.GradlePluginConfig.PROPERTY_LOGSENDER_ENABLED
import com.itsaky.androidide.tooling.api.IToolingApiClient
import com.itsaky.androidide.tooling.api.IToolingApiServer
import com.itsaky.androidide.tooling.api.LogSenderConfig.PROPERTY_LOGSENDER_AAR
import com.itsaky.androidide.tooling.api.LogSenderConfig.PROPERTY_LOGSENDER_ENABLED
import com.itsaky.androidide.tooling.api.messages.BuildId
import com.itsaky.androidide.tooling.api.messages.ClientGradleBuildConfig
import com.itsaky.androidide.tooling.api.messages.GradleBuildParams
Expand Down Expand Up @@ -352,18 +354,21 @@ class GradleBuildService :

val projectPath = ProjectManagerImpl.getInstance().projectDirPath ?: "unknown"
val buildType = getBuildType(buildInfo.tasks)
val isDebugBuild = buildType == "debug"

val currentTuningConfig = tuningConfig
var newTuningConfig: GradleTuningConfig? = null
val extraArgs = getGradleExtraArgs()

@Suppress("SimplifyBooleanWithConstants")
val extraArgs = getGradleExtraArgs(enableJdwp = JdwpOptions.JDWP_ENABLED && isDebugBuild)
Comment on lines 356 to +363
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

JDWP debug gating can miss valid debug task flows.

Line 357 derives debug intent from getBuildType(...), which only inspects the first task and only maps assembleDebug to debug. If a debug session runs installDebug/bundleDebug (or debug task is not first), Line 363 will disable JDWP and skip the intended manifest permission path.

🔧 Proposed fix
-			val buildType = getBuildType(buildInfo.tasks)
-			val isDebugBuild = buildType == "debug"
+			val buildType = getBuildType(buildInfo.tasks)
+			val isDebugBuild =
+				buildInfo.tasks.any { task ->
+					task.contains("assembleDebug", ignoreCase = true) ||
+						task.contains("installDebug", ignoreCase = true) ||
+						task.contains("bundleDebug", ignoreCase = true)
+				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt`
around lines 356 - 363, The current JDWP gating derives isDebugBuild using
getBuildType(buildInfo.tasks) which only checks the first task; update the logic
so isDebugBuild is true when any task in buildInfo.tasks represents a debug
variant (e.g., tasks that end with "Debug" or explicit names like
"assembleDebug", "installDebug", "bundleDebug"), then pass that result into
getGradleExtraArgs(enableJdwp = JdwpOptions.JDWP_ENABLED && isDebugBuild).
Modify either getBuildType to examine all tasks or replace the single-task check
with a new predicate that scans buildInfo.tasks for debug-related task names so
JDWP is enabled for all valid debug task flows.


var buildParams =
if (FeatureFlags.isExperimentsEnabled) {
runCatching {
newTuningConfig =
GradleBuildTuner.autoTune(
device = DeviceInfo.buildDeviceProfile(applicationContext),
build = BuildProfile(isDebugBuild = buildType == "debug"),
build = BuildProfile(isDebugBuild),
previousConfig = currentTuningConfig,
analyticsManager = analyticsManager,
buildId = buildInfo.buildId,
Expand Down Expand Up @@ -440,15 +445,19 @@ class GradleBuildService :
eventListener?.onProgressEvent(event)
}

private fun getGradleExtraArgs(): List<String> {
private fun getGradleExtraArgs(
enableJdwp: Boolean = JdwpOptions.JDWP_ENABLED,
enableLogSender: Boolean = DevOpsPreferences.logsenderEnabled,
): List<String> {
val extraArgs = ArrayList<String>()
extraArgs.add("--init-script")
extraArgs.add(Environment.INIT_SCRIPT.absolutePath)

// Override AAPT2 binary
// The one downloaded from Maven is not built for Android
extraArgs.add("-Pandroid.aapt2FromMavenOverride=${Environment.AAPT2.absolutePath}")
extraArgs.add("-P${PROPERTY_LOGSENDER_ENABLED}=${DevOpsPreferences.logsenderEnabled}")
extraArgs.add("-P${PROPERTY_JDWP_ENABLED}=$enableJdwp")
extraArgs.add("-P${PROPERTY_LOGSENDER_ENABLED}=$enableLogSender")
extraArgs.add("-P${PROPERTY_LOGSENDER_AAR}=${Environment.LOGSENDER_AAR.absolutePath}")

if (BuildPreferences.isStacktraceEnabled) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/

package com.itsaky.androidide.tooling.api;

/**
* Configuration options for the Gradle plugin.
*
* @author Akash Yadav
*/
public final class GradlePluginConfig {

/**
* Property used by the Gradle plugin to determine whether the Gradle build includes JDWP (debugging) support. This is usually set when for builds which are intended to be launched in debug mode.
*/
public static final String PROPERTY_JDWP_ENABLED = "cotg.jdwp.enabled";

/**
* Property to enable or disable <code>LogSender</code> in the project. Value can be <code>true</code> or <code>false</code>.
*/
public static final String PROPERTY_LOGSENDER_ENABLED = "androidide.logsender.isEnabled";

/**
* The path to the LogSender AAR file.
*/
public static final String PROPERTY_LOGSENDER_AAR = "androidide.logsender.aar";

/**
* Property that is set in tests to indicate that the plugin is being applied in a test environment.
* <p>
* <b>This is an internal property and should not be manually set by users.</b>
*/
public static final String _PROPERTY_IS_TEST_ENV = "androidide.plugins.internal.isTestEnv";

/**
* Property that is set in tests to provide path to the local maven repository. If this property is empty, `null` or not set at all, the default maven local repository is used.
*
* <b>This is an internal property and should not be manually set by users.</b>
*/
public static final String _PROPERTY_MAVEN_LOCAL_REPOSITORY = "androidide.plugins.internal.mavenLocalRepositories";

private GradlePluginConfig() {
throw new UnsupportedOperationException("This class cannot be instantiated.");
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
*/
package com.itsaky.androidide.gradle

import com.itsaky.androidide.tooling.api.LogSenderConfig.PROPERTY_LOGSENDER_ENABLED
import com.itsaky.androidide.tooling.api.GradlePluginConfig.PROPERTY_JDWP_ENABLED
import com.itsaky.androidide.tooling.api.GradlePluginConfig.PROPERTY_LOGSENDER_ENABLED
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.logging.Logging
Expand All @@ -38,11 +39,13 @@ class AndroidIDEGradlePlugin : Plugin<Project> {

target.run {
val isLogSenderEnabled = findProperty(PROPERTY_LOGSENDER_ENABLED) == "true"
if (plugins.hasPlugin(APP_PLUGIN)) {
if (isLogSenderEnabled) {
logger.info("Applying LogSender plugin to project '${project.path}'")
pluginManager.apply(LogSenderPlugin::class.java)
}
if (isLogSenderEnabled) {
pluginManager.apply(LogSenderPlugin::class.java)
}

val isJdwpEnabled = findProperty(PROPERTY_JDWP_ENABLED) == "true"
if (isJdwpEnabled) {
pluginManager.apply(JdwpPlugin::class.java)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class AndroidIDEInitScriptPlugin : Plugin<Gradle> {
gradle.rootProject.subprojects { sub ->
if (!sub.buildFile.exists()) {
// For subproject ':nested:module',
// ':nested' represented as a 'Project', but it may or may not have a buildscript file
// ':nested' is represented as a 'Project', but it may or may not have a buildscript file
// if the project doesn't have a buildscript, then the plugins should not be applied
return@subprojects
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.itsaky.androidide.gradle

import com.itsaky.androidide.tooling.api.LogSenderConfig._PROPERTY_IS_TEST_ENV
import com.itsaky.androidide.tooling.api.LogSenderConfig._PROPERTY_MAVEN_LOCAL_REPOSITORY
import com.itsaky.androidide.tooling.api.GradlePluginConfig._PROPERTY_IS_TEST_ENV
import com.itsaky.androidide.tooling.api.GradlePluginConfig._PROPERTY_MAVEN_LOCAL_REPOSITORY
import org.adfa.constants.MAVEN_LOCAL_REPOSITORY
import org.gradle.StartParameter
import org.gradle.api.Plugin
Expand Down Expand Up @@ -56,7 +56,11 @@ class COTGSettingsPlugin : Plugin<Settings> {
val mavenLocalRepos =
projectProperties.getOrDefault(_PROPERTY_MAVEN_LOCAL_REPOSITORY, "")

isTestEnv to mavenLocalRepos.split(File.pathSeparatorChar).toList().filter { it.isNotBlank() }
isTestEnv to
mavenLocalRepos
.split(File.pathSeparatorChar)
.toList()
.filter { it.isNotBlank() }
}
}

Expand Down
107 changes: 107 additions & 0 deletions gradle-plugin/src/main/java/com/itsaky/androidide/gradle/JdwpPlugin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.itsaky.androidide.gradle

import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult

class JdwpPlugin : Plugin<Project> {
override fun apply(target: Project) =
target
.run {
if (!target.plugins.hasPlugin(APP_PLUGIN)) {
return
}

logger.info("Applying {} to project '${target.path}'", JdwpPlugin::class.simpleName)

extensions.getByType(ApplicationAndroidComponentsExtension::class.java).apply {
onDebuggableVariants { variant ->
val jdwpManifestTransformer =
tasks.register(
variant.generateTaskName("transform", "JdwpManifest"),
JdwpManifestTransformerTask::class.java,
)

variant.artifacts
.use(jdwpManifestTransformer)
.wiredWithFiles(
taskInput = JdwpManifestTransformerTask::mergedManifest,
taskOutput = JdwpManifestTransformerTask::updatedManifest,
).toTransform(SingleArtifact.MERGED_MANIFEST)
}
}
}.let { }
}

abstract class JdwpManifestTransformerTask : DefaultTask() {
@get:InputFile
abstract val mergedManifest: RegularFileProperty

@get:OutputFile
abstract val updatedManifest: RegularFileProperty

@TaskAction
fun transform() {
val inputFile = mergedManifest.get().asFile
val outputFile = updatedManifest.get().asFile

val factory =
DocumentBuilderFactory.newInstance().apply {
isNamespaceAware = true
setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
setFeature("http://xml.org/sax/features/external-general-entities", false)
setFeature("http://xml.org/sax/features/external-parameter-entities", false)
setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
}

val documentBuilder = factory.newDocumentBuilder()
val document = documentBuilder.parse(inputFile)

val manifest = document.documentElement
val androidNs = "http://schemas.android.com/apk/res/android"

// Check if INTERNET permission already exists
val existingPermissions = document.getElementsByTagName("uses-permission")
var hasInternetPermission = false

for (i in 0 until existingPermissions.length) {
val node = existingPermissions.item(i)
val nameAttr = node.attributes?.getNamedItemNS(androidNs, "name")
if (nameAttr?.nodeValue == "android.permission.INTERNET") {
hasInternetPermission = true
break
}
}

if (!hasInternetPermission) {
logger.info("Adding INTERNET permission")
val permissionNode = document.createElement("uses-permission")
permissionNode.setAttributeNS(
androidNs,
"android:name",
"android.permission.INTERNET",
)
manifest.appendChild(permissionNode)
}

val transformer = TransformerFactory.newInstance().newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
transformer.transform(
DOMSource(document),
StreamResult(outputFile),
)
}
}
Loading