diff --git a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt index afc4256c52..2a2732e68b 100644 --- a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt +++ b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt @@ -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 @@ -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 @@ -352,10 +354,13 @@ 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) var buildParams = if (FeatureFlags.isExperimentsEnabled) { @@ -363,7 +368,7 @@ class GradleBuildService : newTuningConfig = GradleBuildTuner.autoTune( device = DeviceInfo.buildDeviceProfile(applicationContext), - build = BuildProfile(isDebugBuild = buildType == "debug"), + build = BuildProfile(isDebugBuild), previousConfig = currentTuningConfig, analyticsManager = analyticsManager, buildId = buildInfo.buildId, @@ -440,7 +445,10 @@ class GradleBuildService : eventListener?.onProgressEvent(event) } - private fun getGradleExtraArgs(): List { + private fun getGradleExtraArgs( + enableJdwp: Boolean = JdwpOptions.JDWP_ENABLED, + enableLogSender: Boolean = DevOpsPreferences.logsenderEnabled, + ): List { val extraArgs = ArrayList() extraArgs.add("--init-script") extraArgs.add(Environment.INIT_SCRIPT.absolutePath) @@ -448,7 +456,8 @@ class GradleBuildService : // 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) { diff --git a/gradle-plugin-config/src/main/java/com/itsaky/androidide/tooling/api/GradlePluginConfig.java b/gradle-plugin-config/src/main/java/com/itsaky/androidide/tooling/api/GradlePluginConfig.java new file mode 100644 index 0000000000..226e658c43 --- /dev/null +++ b/gradle-plugin-config/src/main/java/com/itsaky/androidide/tooling/api/GradlePluginConfig.java @@ -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 . + */ + +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 LogSender in the project. Value can be true or false. + */ + 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. + *

+ * This is an internal property and should not be manually set by users. + */ + 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. + * + * This is an internal property and should not be manually set by users. + */ + public static final String _PROPERTY_MAVEN_LOCAL_REPOSITORY = "androidide.plugins.internal.mavenLocalRepositories"; + + private GradlePluginConfig() { + throw new UnsupportedOperationException("This class cannot be instantiated."); + } +} diff --git a/gradle-plugin-config/src/main/java/com/itsaky/androidide/tooling/api/LogSenderConfig.java b/gradle-plugin-config/src/main/java/com/itsaky/androidide/tooling/api/LogSenderConfig.java deleted file mode 100644 index fb770b7d8d..0000000000 --- a/gradle-plugin-config/src/main/java/com/itsaky/androidide/tooling/api/LogSenderConfig.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.api; - -/** - * Configuration options for LogSender. Properties defined in this class whose name start with an - * _ are internal. - * - * @author Akash Yadav - */ -public final class LogSenderConfig { - - /** - * Property to enable or disable LogSender in the project. Value can be - * true or false. - */ - 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. - *

- * This is an internal property and should not be manually set by users. - */ - 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. - * - * This is an internal property and should not be manually set by users. - */ - public static final String _PROPERTY_MAVEN_LOCAL_REPOSITORY = "androidide.plugins.internal.mavenLocalRepositories"; - - private LogSenderConfig() { - throw new UnsupportedOperationException("This class cannot be instantiated."); - } -} diff --git a/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/AndroidIDEGradlePlugin.kt b/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/AndroidIDEGradlePlugin.kt index bb63ef6a9b..dea43eacdb 100644 --- a/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/AndroidIDEGradlePlugin.kt +++ b/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/AndroidIDEGradlePlugin.kt @@ -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 @@ -38,11 +39,13 @@ class AndroidIDEGradlePlugin : Plugin { 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) } } } diff --git a/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/AndroidIDEInitScriptPlugin.kt b/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/AndroidIDEInitScriptPlugin.kt index 50f8e018ce..29ee857794 100644 --- a/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/AndroidIDEInitScriptPlugin.kt +++ b/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/AndroidIDEInitScriptPlugin.kt @@ -58,7 +58,7 @@ class AndroidIDEInitScriptPlugin : Plugin { 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 } diff --git a/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/COTGSettingsPlugin.kt b/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/COTGSettingsPlugin.kt index 12924f9e1d..165413b9df 100644 --- a/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/COTGSettingsPlugin.kt +++ b/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/COTGSettingsPlugin.kt @@ -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 @@ -56,7 +56,11 @@ class COTGSettingsPlugin : Plugin { 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() } } } diff --git a/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/JdwpPlugin.kt b/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/JdwpPlugin.kt new file mode 100644 index 0000000000..9558410985 --- /dev/null +++ b/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/JdwpPlugin.kt @@ -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 { + 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), + ) + } +} diff --git a/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/LogSenderPlugin.kt b/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/LogSenderPlugin.kt index b32a2266aa..3484df150d 100644 --- a/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/LogSenderPlugin.kt +++ b/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/LogSenderPlugin.kt @@ -21,17 +21,14 @@ import com.android.build.api.component.analytics.AnalyticsEnabledApplicationVari import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.ApplicationVariant import com.android.build.api.variant.impl.ApplicationVariantImpl -import com.itsaky.androidide.tooling.api.LogSenderConfig.PROPERTY_LOGSENDER_AAR +import com.itsaky.androidide.tooling.api.GradlePluginConfig.PROPERTY_LOGSENDER_AAR import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.Configuration -import org.gradle.api.artifacts.ExternalDependency -import org.gradle.api.artifacts.ExternalModuleDependency import org.gradle.api.logging.Logging import java.io.File import java.io.FileNotFoundException -import java.util.concurrent.TimeUnit /** * Plugin to manage LogSender in Android applications. @@ -40,87 +37,60 @@ import java.util.concurrent.TimeUnit */ class LogSenderPlugin : Plugin { - - companion object { - private val logger = Logging.getLogger(LogSenderPlugin::class.java) - } - - override fun apply(target: Project) { - if (target.isTestEnv) { - logger.lifecycle("Applying {} to project '{}'", javaClass.simpleName, target.path) - } - - val logsenderAar = - target.findProperty(PROPERTY_LOGSENDER_AAR)?.let { aarPath -> File(aarPath.toString()) } - ?: throw GradleException("LogSenderPlugin has been applied but no property '$PROPERTY_LOGSENDER_AAR' is set") - - if (!logsenderAar.exists()) { - throw FileNotFoundException("LogSender AAR file not found at '${logsenderAar.absolutePath}'") - } - - if (!logsenderAar.isFile) { - throw GradleException("LogSender AAR file at '${logsenderAar.absolutePath}' is not a file") - } - - target.run { - check(plugins.hasPlugin(APP_PLUGIN)) { - "${javaClass.simpleName} can only be applied to Android application projects." - } - - extensions.getByType(ApplicationAndroidComponentsExtension::class.java).apply { - - val debuggableBuilds = hashSetOf() - - beforeVariants { variantBuilder -> - logger.info( - "Variant :'{}' isDebuggable: {}", - variantBuilder.name, - variantBuilder.debuggable - ) - - if (variantBuilder.debuggable) { - debuggableBuilds.add(variantBuilder.name) - } - } - - onVariants { variant -> - logger.info( - "Found {} debuggable builds in project '{}': {}", - debuggableBuilds.size, - project.path, - debuggableBuilds - ) - - if (debuggableBuilds.isEmpty()) { - logger.warn("No debuggable builds found in project '${project.path}'") - } - - if (variant.name !in debuggableBuilds) { - return@onVariants - } - - variant.withRuntimeConfiguration { - logger.lifecycle( - "Adding LogSender dependency to variant '{}' of project '{}'", - variant.name, - project.path - ) - - logger.debug("Adding logsender dependency: {}", logsenderAar.absolutePath) - dependencies.add(project.dependencies.create(project.fileTree(logsenderAar))) - } - } - } - } - } - - private fun ApplicationVariant.withRuntimeConfiguration( - action: Configuration.() -> Unit - ) { - if (this is ApplicationVariantImpl) { - variantDependencies.runtimeClasspath.action() - } else if (this is AnalyticsEnabledApplicationVariant) { - delegate.withRuntimeConfiguration(action) - } - } + companion object { + private val logger = Logging.getLogger(LogSenderPlugin::class.java) + } + + override fun apply(target: Project) { + if (!target.plugins.hasPlugin(APP_PLUGIN)) { + return + } + + logger.info("Applying {} to project '${target.path}'", LogSenderPlugin::class.simpleName) + + if (target.isTestEnv) { + logger.lifecycle("Applying {} to project '{}'", javaClass.simpleName, target.path) + } + + val logsenderAar = + target.findProperty(PROPERTY_LOGSENDER_AAR)?.let { aarPath -> File(aarPath.toString()) } + ?: throw GradleException("LogSenderPlugin has been applied but no property '$PROPERTY_LOGSENDER_AAR' is set") + + if (!logsenderAar.exists()) { + throw FileNotFoundException("LogSender AAR file not found at '${logsenderAar.absolutePath}'") + } + + if (!logsenderAar.isFile) { + throw GradleException("LogSender AAR file at '${logsenderAar.absolutePath}' is not a file") + } + + target.run { + check(plugins.hasPlugin(APP_PLUGIN)) { + "${javaClass.simpleName} can only be applied to Android application projects." + } + + extensions.getByType(ApplicationAndroidComponentsExtension::class.java).apply { + onDebuggableVariants { variant -> + variant.withRuntimeConfiguration { + logger.lifecycle( + "Adding LogSender dependency to variant '{}' of project '{}'", + variant.name, + project.path, + ) + + logger.debug("Adding logsender dependency: {}", logsenderAar.absolutePath) + dependencies.add(project.dependencies.create(project.fileTree(logsenderAar))) + } + } + } + } + } + + private fun ApplicationVariant.withRuntimeConfiguration(action: Configuration.() -> Unit) { + if (this is ApplicationVariantImpl) { + variantDependencies.runtimeClasspath.action() + } else if (this is AnalyticsEnabledApplicationVariant) { + delegate.withRuntimeConfiguration(action) + } + } } diff --git a/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/common.kt b/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/common.kt index 09f352bef4..737e1da717 100644 --- a/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/common.kt +++ b/gradle-plugin/src/main/java/com/itsaky/androidide/gradle/common.kt @@ -17,8 +17,11 @@ package com.itsaky.androidide.gradle +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.api.variant.ApplicationVariant +import com.android.build.api.variant.Variant import com.itsaky.androidide.buildinfo.BuildInfo -import com.itsaky.androidide.tooling.api.LogSenderConfig._PROPERTY_IS_TEST_ENV +import com.itsaky.androidide.tooling.api.GradlePluginConfig._PROPERTY_IS_TEST_ENV import org.gradle.api.Project import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.dsl.DependencyHandler @@ -35,21 +38,46 @@ const val APP_PLUGIN = "com.android.application" const val LIBRARY_PLUGIN = "com.android.library" internal val Project.isTestEnv: Boolean - get() = hasProperty(_PROPERTY_IS_TEST_ENV) && property( - _PROPERTY_IS_TEST_ENV).toString().toBoolean() + get() = hasProperty(_PROPERTY_IS_TEST_ENV) && property( + _PROPERTY_IS_TEST_ENV + ).toString().toBoolean() internal fun depVersion(testEnv: Boolean): String { - return if (testEnv && !System.getenv("CI").toBoolean()) { - BuildInfo.VERSION_NAME_SIMPLE - } else { - BuildInfo.VERSION_NAME_DOWNLOAD - } + return if (testEnv && !System.getenv("CI").toBoolean()) { + BuildInfo.VERSION_NAME_SIMPLE + } else { + BuildInfo.VERSION_NAME_DOWNLOAD + } } fun Project.ideDependency(artifact: String): Dependency { - return dependencies.ideDependency(artifact, isTestEnv) + return dependencies.ideDependency(artifact, isTestEnv) } fun DependencyHandler.ideDependency(artifact: String, testEnv: Boolean): Dependency { - return create("${BuildInfo.MVN_GROUP_ID}:${artifact}:${depVersion(testEnv)}") + return create("${BuildInfo.MVN_GROUP_ID}:${artifact}:${depVersion(testEnv)}") } + +/** + * Perform the given [action] on debuggable variants in this [ApplicationAndroidComponentsExtension]. + */ +fun ApplicationAndroidComponentsExtension.onDebuggableVariants(action: (ApplicationVariant) -> Unit) { + val debuggableBuilds = hashSetOf() + + beforeVariants { variantBuilder -> + if (variantBuilder.debuggable) { + debuggableBuilds.add(variantBuilder.name) + } + } + + onVariants { variant -> + if (variant.name !in debuggableBuilds) { + return@onVariants + } + + action(variant) + } +} + +fun Variant.generateTaskName(prefix: String, suffix: String = "") = + prefix + this.name.replaceFirstChar { it.uppercase() } + suffix diff --git a/gradle-plugin/src/test/java/com/itsaky/androidide/gradle/AndroidIDEPluginTest.kt b/gradle-plugin/src/test/java/com/itsaky/androidide/gradle/AndroidIDEPluginTest.kt index 76f6af0a69..3f84182d59 100644 --- a/gradle-plugin/src/test/java/com/itsaky/androidide/gradle/AndroidIDEPluginTest.kt +++ b/gradle-plugin/src/test/java/com/itsaky/androidide/gradle/AndroidIDEPluginTest.kt @@ -18,41 +18,43 @@ package com.itsaky.androidide.gradle import com.google.common.truth.Truth.assertThat -import com.itsaky.androidide.tooling.api.LogSenderConfig.PROPERTY_LOGSENDER_ENABLED +import com.itsaky.androidide.tooling.api.GradlePluginConfig.PROPERTY_LOGSENDER_ENABLED import org.junit.jupiter.api.Test /** * @author Akash Yadav */ class AndroidIDEPluginTest { + @Test + fun `test logsender must be enabled by default`() { + val result = buildProject() + assertThat(result.output).doesNotContain("LogSender is disabled") + } - @Test - fun `test logsender must be enabled by default`() { - val result = buildProject() - assertThat(result.output).doesNotContain("LogSender is disabled") - } + @Test + fun `test logsender must be enabled if specified explicitly`() { + val result = + buildProject(configureArgs = { + it.add("-P$PROPERTY_LOGSENDER_ENABLED=true") + }) + assertThat(result.output).doesNotContain("LogSender is disabled") + } - @Test - fun `test logsender must be enabled if specified explicitly`() { - val result = buildProject(configureArgs = { - it.add("-P$PROPERTY_LOGSENDER_ENABLED=true") - }) - assertThat(result.output).doesNotContain("LogSender is disabled") - } + @Test + fun `test logsender must be disabled if specified explicitly`() { + val result = + buildProject(configureArgs = { + it.add("-P$PROPERTY_LOGSENDER_ENABLED=false") + }) + assertThat(result.output).contains("LogSender is disabled") + } - @Test - fun `test logsender must be disabled if specified explicitly`() { - val result = buildProject(configureArgs = { - it.add("-P$PROPERTY_LOGSENDER_ENABLED=false") - }) - assertThat(result.output).contains("LogSender is disabled") - } - - @Test - fun `test logsender must be added as non-changing dependency`() { - val result = buildProject(configureArgs = { - it.add("--debug") - }) - assertThat(result.output).contains("Marking logsender dependency as not-changing") - } -} \ No newline at end of file + @Test + fun `test logsender must be added as non-changing dependency`() { + val result = + buildProject(configureArgs = { + it.add("--debug") + }) + assertThat(result.output).contains("Marking logsender dependency as not-changing") + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt index d2f3fa2f83..c5f096f702 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt @@ -136,7 +136,10 @@ class JavaLanguageServer : ILanguageServer { override fun connectDebugClient(client: IDebugClient) { if (JdwpOptions.JDWP_ENABLED) { + log.info("Connecting to debug client: {}", client) this.debugAdapter.connectDebugClient(client) + } else { + log.info("Not connecting to debug client. JDWP disabled.") } } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt index 2835cd0c4f..896af9b34f 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt @@ -59,6 +59,7 @@ internal class JavaDebugAdapter : EventConsumer, AutoCloseable { private val vmm = Bootstrap.virtualMachineManager() + private val vms = CopyOnWriteArraySet() private val adapterScope = CoroutineScope(Dispatchers.Default) @@ -117,18 +118,25 @@ internal class JavaDebugAdapter : fun evalContext() = connVm().evalContext override fun connectDebugClient(client: IDebugClient) { - val connector = vmm.listeningConnectors().firstOrNull() as? SocketListeningConnector? + val listeningConnectors = vmm.listeningConnectors() + listeningConnectors.forEach { conn -> + logger.info("Listening connector: {}", conn.javaClass.canonicalName) + } + + val connector = + vmm.listeningConnectors().filterIsInstance().firstOrNull() if (connector == null) { logger.error("No listening connectors found, or the connector is not a SocketListeningConnector") return } val args = connector.defaultArguments() + args[JdwpOptions.CONNECTOR_LOCAL_ADDR]!!.setValue(JdwpOptions.DEFAULT_JDWP_HOST) args[JdwpOptions.CONNECTOR_PORT]!!.setValue(JdwpOptions.DEFAULT_JDWP_PORT.toString()) args[JdwpOptions.CONNECTOR_TIMEOUT]!!.setValue(JdwpOptions.DEFAULT_JDWP_TIMEOUT.inWholeMilliseconds.toString()) logger.debug( - "Starting JDWP listener. Arguments={}", + "Starting JDWP listener. Arguments: {}", args.map { (_, value) -> "$value" }.joinToString(), ) @@ -351,8 +359,18 @@ internal class JavaDebugAdapter : val resolveSuccess = result.getOrDefault(false) when { - resolveSuccess && spec.isResolved -> BreakpointResult.Success(breakpoint, false) - resolveSuccess && !spec.isResolved -> BreakpointResult.Success(breakpoint, true) + resolveSuccess && spec.isResolved -> + BreakpointResult.Success( + breakpoint, + false, + ) + + resolveSuccess && !spec.isResolved -> + BreakpointResult.Success( + breakpoint, + true, + ) + else -> BreakpointResult.Failure(breakpoint, failure) } }, @@ -539,6 +557,7 @@ internal class JavaDebugAdapter : } override fun close() { + logger.debug("close") try { _listenerState?.stopListening() listenerThread?.interrupt() @@ -584,14 +603,19 @@ internal class JDWPListenerThread( } override fun run() { + logger.debug("run::start") if (!listenerState.isListening) { + logger.debug("startListening") listenerState.startListening() } while (isAlive && !isInterrupted) { try { logger.debug("Waiting for VM connection") - onConnect(listenerState.accept()) + val client = listenerState.accept() + logger.debug("client: {}", client) + + onConnect(client) } catch (_: TransportTimeoutException) { logger.warn("Timeout waiting for VM connection") } catch (e: SocketException) { @@ -606,5 +630,7 @@ internal class JDWPListenerThread( logger.error("An error occurred while listening for VM connections", err) } } + + logger.debug("run::end") } } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JdwpOptions.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JdwpOptions.kt index 77c44eae3a..23ff6b5951 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JdwpOptions.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JdwpOptions.kt @@ -1,5 +1,6 @@ package com.itsaky.androidide.lsp.java.debug +import com.itsaky.androidide.lsp.java.debug.JdwpOptions.JDWP_OPTIONS_MAP import kotlin.time.Duration.Companion.seconds /** @@ -8,48 +9,60 @@ import kotlin.time.Duration.Companion.seconds * @author Akash Yadav */ object JdwpOptions { + /** + * Whether the debugger is enabled or not. + */ + const val JDWP_ENABLED = true - /** - * Whether the debugger is enabled or not. - */ - const val JDWP_ENABLED = true - - /** - * The port on which the debugger will listen for connections. - */ - const val DEFAULT_JDWP_PORT = 42233 - - /** - * The timeout duration for waiting for a VM to connect to the debugger. The default value - * is to wait indefinitely. - */ - val DEFAULT_JDWP_TIMEOUT = 0.seconds - - /** - * Options for configuring the JDWP agent in a VM. - */ - val JDWP_OPTIONS_MAP = mapOf( - "suspend" to "n", - "server" to "n", - "transport" to "dt_socket", - "address" to DEFAULT_JDWP_PORT.toString(), - ) - - /** - * [JDWP_OPTIONS_MAP] converted to a comma-separated string, as expected by the debugger - * agent. - */ - val JDWP_OPTIONS = JDWP_OPTIONS_MAP.map { (key, value) -> "$key=$value" }.joinToString(",") - - /** - * The argument provided to JDI [Connector][com.sun.jdi.connect.Connector] to provide the port to listen at. - */ - const val CONNECTOR_PORT = "port" - - /** - * The argument provided to JDI [Connector][com.sun.jdi.connect.Connector] to provide the timeout - * to wait for a VM to connect. - */ - const val CONNECTOR_TIMEOUT = "timeout" - -} \ No newline at end of file + /** + * The hostname of the JDWP listener socket. + */ + const val DEFAULT_JDWP_HOST = "127.0.0.1" + + /** + * The port on which the debugger will listen for connections. + */ + const val DEFAULT_JDWP_PORT = 42233 + + /** + * The timeout duration for waiting for a VM to connect to the debugger. The default value + * is to wait indefinitely. + */ + val DEFAULT_JDWP_TIMEOUT = 0.seconds + + /** + * Options for configuring the JDWP agent in a VM. + */ + val JDWP_OPTIONS_MAP = + mapOf( + "suspend" to "n", + "server" to "n", + "transport" to "dt_socket", + "address" to "$DEFAULT_JDWP_HOST:$DEFAULT_JDWP_PORT", + "quiet" to "n", + "logflags" to "0xfff", + ) + + /** + * [JDWP_OPTIONS_MAP] converted to a comma-separated string, as expected by the debugger + * agent. + */ + val JDWP_OPTIONS = JDWP_OPTIONS_MAP.map { (key, value) -> "$key=$value" }.joinToString(",") + + /** + * The argument provided to JDI [Connector][com.sun.jdi.connect.Connector] to provide the + * hostname of the listener. + */ + const val CONNECTOR_LOCAL_ADDR = "localAddress" + + /** + * The argument provided to JDI [Connector][com.sun.jdi.connect.Connector] to provide the port to listen at. + */ + const val CONNECTOR_PORT = "port" + + /** + * The argument provided to JDI [Connector][com.sun.jdi.connect.Connector] to provide the timeout + * to wait for a VM to connect. + */ + const val CONNECTOR_TIMEOUT = "timeout" +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/ListenerState.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/ListenerState.kt index 3297b01d4d..5f3afccdc2 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/ListenerState.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/ListenerState.kt @@ -7,29 +7,29 @@ import com.sun.tools.jdi.SocketListeningConnector import com.sun.tools.jdi.isListening internal data class ListenerState( - val client: IDebugClient, - val connector: SocketListeningConnector, - val args: Map + val client: IDebugClient, + val connector: SocketListeningConnector, + val args: Map, ) { val isListening: Boolean get() = connector.isListening(args) - /** - * Start listening for connections from VMs. - * - * @return The address of the listening socket. - */ - fun startListening(): String = connector.startListening(args) + /** + * Start listening for connections from VMs. + * + * @return The address of the listening socket. + */ + fun startListening(): String = connector.startListening(args) - /** - * Stop listening for connections from VMs. - */ - fun stopListening() = connector.stopListening(args) + /** + * Stop listening for connections from VMs. + */ + fun stopListening() = connector.stopListening(args) - /** - * Accept a connection from a VM. - * - * @return The connected VM. - */ - fun accept(): VirtualMachine = connector.accept(args) -} \ No newline at end of file + /** + * Accept a connection from a VM. + * + * @return The connected VM. + */ + fun accept(): VirtualMachine = connector.accept(args) +} diff --git a/subprojects/shizuku-manager/src/main/java/moe/shizuku/manager/ShizukuManagerProvider.kt b/subprojects/shizuku-manager/src/main/java/moe/shizuku/manager/ShizukuManagerProvider.kt index 73ee553343..49bc7e0935 100644 --- a/subprojects/shizuku-manager/src/main/java/moe/shizuku/manager/ShizukuManagerProvider.kt +++ b/subprojects/shizuku-manager/src/main/java/moe/shizuku/manager/ShizukuManagerProvider.kt @@ -3,7 +3,6 @@ package moe.shizuku.manager import android.os.Bundle import androidx.core.os.bundleOf import com.itsaky.androidide.buildinfo.BuildInfo -import moe.shizuku.api.BinderContainer import org.slf4j.LoggerFactory import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuApiConstants.USER_SERVICE_ARG_TOKEN @@ -30,10 +29,8 @@ class ShizukuManagerProvider : ShizukuProvider() { return if (method == METHOD_SEND_USER_SERVICE) { try { - extras.classLoader = BinderContainer::class.java.classLoader - val token = extras.getString(USER_SERVICE_ARG_TOKEN) ?: return null - val binder = extras.getParcelable(EXTRA_BINDER)?.binder ?: return null + val binder = extras.getBinder(EXTRA_BINDER) ?: return null val countDownLatch = CountDownLatch(1) var reply: Bundle? = Bundle() @@ -48,7 +45,7 @@ class ShizukuManagerProvider : ShizukuProvider() { USER_SERVICE_ARG_TOKEN to token, ), ) - reply!!.putParcelable(EXTRA_BINDER, BinderContainer(Shizuku.getBinder())) + reply!!.putBinder(EXTRA_BINDER, Shizuku.getBinder()) } catch (e: Throwable) { logger.error("attachUserService $token", e) reply = null diff --git a/subprojects/shizuku-manager/src/main/java/moe/shizuku/manager/adb/AdbPairingService.kt b/subprojects/shizuku-manager/src/main/java/moe/shizuku/manager/adb/AdbPairingService.kt index 9d43e99ef7..12cd715ec1 100644 --- a/subprojects/shizuku-manager/src/main/java/moe/shizuku/manager/adb/AdbPairingService.kt +++ b/subprojects/shizuku-manager/src/main/java/moe/shizuku/manager/adb/AdbPairingService.kt @@ -189,14 +189,16 @@ class AdbPairingService : Service() { return@launch } - AdbPairingClient(host, port, code, key) - .runCatching { - start() - }.onFailure { - handleResult(false, it) - }.onSuccess { - handleResult(it, null) - } + AdbPairingClient(host, port, code, key).use { pairingClient -> + pairingClient + .runCatching { + start() + }.onFailure { + handleResult(false, it) + }.onSuccess { + handleResult(it, null) + } + } } return workingNotification diff --git a/subprojects/shizuku-provider/src/main/java/rikka/shizuku/ShizukuProvider.java b/subprojects/shizuku-provider/src/main/java/rikka/shizuku/ShizukuProvider.java index 2b397ebd63..9d18d23c3c 100644 --- a/subprojects/shizuku-provider/src/main/java/rikka/shizuku/ShizukuProvider.java +++ b/subprojects/shizuku-provider/src/main/java/rikka/shizuku/ShizukuProvider.java @@ -12,7 +12,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.itsaky.androidide.buildinfo.BuildInfo; -import moe.shizuku.api.BinderContainer; /** *

@@ -83,8 +82,6 @@ public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundl return null; } - extras.setClassLoader(BinderContainer.class.getClassLoader()); - Bundle reply = new Bundle(); switch (method) { case METHOD_SEND_BINDER: { @@ -141,7 +138,7 @@ private boolean handleGetBinder(@NonNull Bundle reply) { if (binder == null || !binder.pingBinder()) return false; - reply.putParcelable(EXTRA_BINDER, new BinderContainer(binder)); + reply.putBinder(EXTRA_BINDER, binder); return true; } @@ -151,10 +148,10 @@ private void handleSendBinder(@NonNull Bundle extras) { return; } - BinderContainer container = extras.getParcelable(EXTRA_BINDER); - if (container != null && container.binder != null) { + IBinder container = extras.getBinder(EXTRA_BINDER); + if (container != null) { Log.d(TAG, "binder received"); - Shizuku.onBinderReceived(container.binder, getContext().getPackageName()); + Shizuku.onBinderReceived(container, getContext().getPackageName()); } } } diff --git a/subprojects/shizuku-server/src/main/java/rikka/shizuku/server/ShizukuService.java b/subprojects/shizuku-server/src/main/java/rikka/shizuku/server/ShizukuService.java index d82f443054..fc509947a0 100644 --- a/subprojects/shizuku-server/src/main/java/rikka/shizuku/server/ShizukuService.java +++ b/subprojects/shizuku-server/src/main/java/rikka/shizuku/server/ShizukuService.java @@ -22,12 +22,8 @@ import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; - import com.itsaky.androidide.buildinfo.BuildInfo; - import java.util.List; - -import moe.shizuku.api.BinderContainer; import moe.shizuku.common.util.OsUtils; import moe.shizuku.common.util.UserUtils; import moe.shizuku.server.IShizukuApplication; @@ -116,7 +112,7 @@ static void sendBinderToUserApp(ShizukuService binder, String packageName, int u } Bundle extra = new Bundle(); - extra.putParcelable(BuildInfo.PACKAGE_NAME + ".shizuku.intent.extra.BINDER", new BinderContainer(binder)); + extra.putBinder(BuildInfo.PACKAGE_NAME + ".shizuku.intent.extra.BINDER", binder); Bundle reply = IContentProviderUtils.callCompat(provider, null, name, "sendBinder", null, extra); if (reply != null) { @@ -283,10 +279,6 @@ public ShizukuUserServiceManager onCreateUserServiceManager() { return new ShizukuUserServiceManager(); } - void sendBinderToManager() { - sendBinderToManager(this); - } - public void onUidGone(int uid) { if (!isManager(uid)) { return; @@ -300,4 +292,8 @@ public void onUidGone(int uid) { System.exit(ServerConstants.MANAGER_APP_NOT_FOUND); } } + + void sendBinderToManager() { + sendBinderToManager(this); + } } diff --git a/subprojects/shizuku-starter/src/main/java/moe/shizuku/starter/ServiceStarter.java b/subprojects/shizuku-starter/src/main/java/moe/shizuku/starter/ServiceStarter.java index 6cf11e51b6..a7a786700a 100644 --- a/subprojects/shizuku-starter/src/main/java/moe/shizuku/starter/ServiceStarter.java +++ b/subprojects/shizuku-starter/src/main/java/moe/shizuku/starter/ServiceStarter.java @@ -9,7 +9,6 @@ import android.util.Pair; import com.itsaky.androidide.buildinfo.BuildInfo; import java.util.Locale; -import moe.shizuku.api.BinderContainer; import moe.shizuku.starter.util.IContentProviderCompat; import rikka.hidden.compat.ActivityManagerApis; import rikka.shizuku.ShizukuApiConstants; @@ -123,19 +122,18 @@ private static boolean sendBinder(IBinder binder, String token, boolean retry) { } Bundle extra = new Bundle(); - extra.putParcelable(EXTRA_BINDER, new BinderContainer(binder)); + extra.putBinder(EXTRA_BINDER, binder); extra.putString(ShizukuApiConstants.USER_SERVICE_ARG_TOKEN, token); Bundle reply = IContentProviderCompat.call(provider, null, null, name, "sendUserService", null, extra); if (reply != null) { - reply.setClassLoader(BinderContainer.class.getClassLoader()); Log.i(TAG, String.format("send binder to %s in user %d", packageName, userId)); - BinderContainer container = reply.getParcelable(EXTRA_BINDER); + IBinder container = reply.getBinder(EXTRA_BINDER); - if (container != null && container.binder != null && container.binder.pingBinder()) { - shizukuBinder = container.binder; + if (container != null && container.pingBinder()) { + shizukuBinder = container; shizukuBinder.linkToDeath(() -> { Log.i(TAG, "exiting..."); System.exit(0);