Why we implemented SonarQube with Android (and how)

Cedric Creusot
Shadow Tech Blog
Published in
5 min readDec 9, 2022

After 5 years, Shadow—the cloud computing service that offers a Windows PC in the cloud—was bought out. And in true startup fashion, Shadow used this momentum to transition into a more professionalized environment.

When I arrived at Shadow, it was natural to presume that the Android codebase had been rewritten at least once by many developers of different backgrounds.

I could complain about the bad decisions and practices made with its programming language, Kotlin. But, as the project’s resident newbie, I didn't know all of the contexts that went into the decision-making process for creating this application. Even if I had been, I am also human and prone to my own faults; when I make choices, some of them will be good, and others not-so-good.

OK! So where should we start to make improvements?

For starters, we’ll need KPIs (Key Performance Indicators) that conventionally adhere to Kotlin and the best practices for Android.

To accomplish this, there were many code analysis tool alternatives I considered, including Ktlint, Detekt, or the default linter from Android.

One tool from another project tested inside our team stood out among these: SonarQube.

Why SonarQube?

SonarQube was the intuitive choice since it centralizes the many KPIs we want to track into a single board: code coverage, bugs, vulnerabilities, debts, and code smell. And because we already have an instance of SonarQube as well as a GitLab repository, it could be interfaced really easily (their GitLab documentation is great, you should check it out).

Alright! Now how should we use it with our Android project?

Configuring our project for SonarQube

First, we needed to add SonarQube to our Android project in our script file build.gradle.kts.

Note: Because we used gradle.kts for this project, the subsequent examples are written in the programming language Kotlin.

We added this line to our main build.gradle.kts:

plugins { 
id(“org.sonarqube”) version “3.3” // Or another version
}

Then we added the sonarqube {} scope immediately after allprojects:

sonarqube { 
properties {
property(“sonar.host.url”, “URL_SONARQUBE_INSTANCE”)
property(“sonar.projectKey”, “YOUR_PROJECT_KEY”)
property(“sonar.qualitygate.wait”, true)
}
}

Following that, we added this in our build.gradle.kts:

buildscript { 

}

plugins {
id(“org.sonarqube”) version “3.3”
}

allprojects {

}

sonarqube {
properties {
property(“sonar.host.url”, “URL_SONARQUBE_INSTANCE”)
property(“sonar.projectKey”, “YOUR_PROJECT_KEY”)
property(“sonar.qualitygate.wait”, true)
}
}

Now when we enter the command line:
./gradlew :sonarqube

It sends the result of the scan to our SonarQube instance.

Note: If you’re configuring SonarQube with on your own Android project, putting sonar.host.url and sonar.projectKey in your build.gralde.kts could be considered a bad practice (if your code is found in nature, you don't want people to use your SonarQube instances). You should use an environment variable instead. I am using it, in this article only as an example.

But things are not always this simple. If we fail to specify the source directories, the board won’t display anything, and we won’t see any reports.

My SonarQube report is empty :(

To see any reports, like the linting on our source code, we needed to define a few properties:

sonarqube {
properties {
property(“sonar.java.binaries”, “$buildDir”)
property(“sonar.sources”, LIST_OF_YOUR_SOURCE_DIRECTORIES)
}
}

We could have stopped here, but we have a multi-module project. The best way we found to enable linting on our desired modules was to exclude some from the scan.

Then we end up with this:

buildscript { 

}

plugins {
id(“org.sonarqube”) version “3.3”
}

allprojects {

}

sonarqube {
properties {
property(“sonar.host.url”, “URL_SONARQUBE_INSTANCE”)
property(“sonar.projectKey”, “YOUR_PROJECT_KEY”)
property(“sonar.qualitygate.wait”, true)
}
}
val excludeProject = listOf(“module_a”, “module_b”)
childProjects.filter {
!excludeProject.contains(it.key) && File(it.value.projectDir, “src/main/java”).exists()
}.forEach {
with(it.value) {
sonarqube {
properties {
property(“sonar.java.binaries”, “$buildDir”)
property(“sonar.sources”, LIST_OF_DIRECTORIES)
}
}
}
}
}

All is good now, but what can we use for tracking the code coverage?

A choice

Before we embarked on this journey with SonarQube, we used the open-source toolkit JaCoco. But there was one problem: JaCoco failed to compose reports because the configuration Gradle file was written in Groovy. SonarQube freed us from error compilations; a decision that left our teams unbridled and able to invoke more freedom into our already tight deadlines.

During this trial, we discovered that Jetbrains was working on a new code coverage engine named Kover. We ran a test despite the tool being in alpha, but we also had to accept the fact that it would only work if our project was not so complicated.

So instead of losing (more) hair, we decided to go back to JaCoco. But this time, we took a different approach.

Instead of using Gradle configuration files to configure JaCoco, we decided to spring for a Gradle Plugin. This plugin would generate the configuration files for each module when they are ready to be compiled, which is far more scalable.

Add JaCoco report to your project without Gradle files

In our project, we’ve added a Gradle Plugin module to help us to configure external tools (like JaCoco) at the compilation time, to be associated with our other modules.

So after scouring the web for an example, I found 2 samples that inspired us to create what we needed. One in Groovy, and the other one from GitHub Gist.

Thanks to Groovy and Gist, we could define a JaCoco plugin configuration that fitted our needs.

We could add the plugin in the specified modules (application, etc…) that will declare coverage tasks:

plugins {
id(“com.shadow.coverage”)
}

Note: You will not find this plugin in mavenCentral since it is only relevant to our project, with our specific needs.

Then we could run ./gradlew :testCoverageReport to generate the correct reports.

The last thing we needed to do was to send those reports to SonarQube.

We achieved this by completing the configuration with the corresponding SonarQube properties:

buildscript { 

}

plugins {
id(“org.sonarqube”) version “3.3”
}

allprojects {

}
// Global configuration
sonarqube {
properties {
property(“sonar.host.url”, “URL_SONARQUBE_INSTANCE”)
property(“sonar.projectKey”, “YOUR_PROJECT_KEY”)
property(“sonar.qualitygate.wait”, true)
}
}
val excludeProject = listOf(“module_a”, “module_b”)
childProjects.filter {
!excludeProject.contains(it.key) && File(it.value.projectDir, “src/main/java”).exists()
}.forEach {
with(it.value) {
// Modules configuration
sonarqube {
properties {
property(“sonar.java.binaries”, “$buildDir”)
property(“sonar.sources”, LIST_OF_DIRECTORIES)
property(“sonar.junit.reportPaths”, REPORT_PATH)
property(“sonar.coverage.jacoco.xmlReportPaths”, JACOCO_REPORT_PATH)
}
}
}
}
}

Conclusion

Admittedly, it took us a few days to add SonarQube and JaCoco to our project, but now we can verify the integrity of our Android app’s code seamlessly.

What do we do now? What we’ve always done, only now we have a tool in our back pocket for helping us find vulnerabilities, mitigate bad habits, or check if we have enough code coverage. It all depends on us.

--

--