/* * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2025 Jimly Asshiddiqy * SPDX-License-Identifier: AGPL-3.0-or-later */ @file:Suppress("UnstableApiUsage", "DEPRECATION") import com.android.build.gradle.internal.api.ApkVariantOutputImpl import com.github.spotbugs.snom.Confidence import com.github.spotbugs.snom.Effort import com.github.spotbugs.snom.SpotBugsTask import com.karumi.shot.ShotExtension import org.gradle.internal.jvm.Jvm import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.io.FileInputStream import java.util.Properties val shotTest = System.getenv("SHOT_TEST") == "true" val ciBuild = System.getenv("CI") == "true" val perfAnalysis = project.hasProperty("perfAnalysis") plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.compose) alias(libs.plugins.spotless) alias(libs.plugins.kapt) alias(libs.plugins.ksp) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.spotbugs) alias(libs.plugins.detekt) // needed to make renovate run without shot, as shot requires Android SDK // https://github.com/pedrovgs/Shot/issues/300 if (System.getenv("SHOT_TEST") == "true") alias(libs.plugins.shot) id("checkstyle") id("pmd") } apply(from = "${rootProject.projectDir}/jacoco.gradle.kts") println("Gradle uses Java ${Jvm.current()}") configurations.configureEach { // via prism4j, already using annotations explicitly exclude(group = "org.jetbrains", module = "annotations-java5") resolutionStrategy { force(libs.objenesis) eachDependency { if (requested.group == "org.checkerframework" && requested.name != "checker-compat-qual") { useVersion(libs.versions.checker.get()) because("https://github.com/google/ExoPlayer/issues/10007") } else if (requested.group == "org.jacoco") { useVersion(libs.versions.jacoco.get()) } else if (requested.group == "commons-logging" && requested.name == "commons-logging") { useTarget(libs.slfj) } } } } // semantic versioning for version code val versionMajor = 3 val versionMinor = 35 val versionPatch = 0 val versionBuild = 0 // 0-50=Alpha / 51-98=RC / 90-99=stable val ndkEnv = buildMap { file("${project.rootDir}/ndk.env").readLines().forEach { val (key, value) = it.split("=") put(key, value) } } val configProps = Properties().apply { val file = rootProject.file(".gradle/config.properties") if (file.exists()) load(FileInputStream(file)) } val ncTestServerUsername = configProps["NC_TEST_SERVER_USERNAME"] val ncTestServerPassword = configProps["NC_TEST_SERVER_PASSWORD"] val ncTestServerBaseUrl = configProps["NC_TEST_SERVER_BASEURL"] android { // install this NDK version and Cmake to produce smaller APKs. Build will still work if not installed ndkVersion = "${ndkEnv["NDK_VERSION"]}" namespace = "com.owncloud.android" testNamespace = "${namespace}.test" androidResources.generateLocaleConfig = true defaultConfig { applicationId = "com.nextcloud.client" minSdk = 27 targetSdk = 36 compileSdk = 36 buildConfigField("boolean", "CI", ciBuild.toString()) buildConfigField("boolean", "RUNTIME_PERF_ANALYSIS", perfAnalysis.toString()) javaCompileOptions.annotationProcessorOptions { arguments += mapOf("room.schemaLocation" to "$projectDir/schemas") } // arguments to be passed to functional tests testInstrumentationRunner = if (shotTest) "com.karumi.shot.ShotTestRunner" else "com.nextcloud.client.TestRunner" testInstrumentationRunnerArguments += mapOf( "TEST_SERVER_URL" to ncTestServerBaseUrl.toString(), "TEST_SERVER_USERNAME" to ncTestServerUsername.toString(), "TEST_SERVER_PASSWORD" to ncTestServerPassword.toString() ) testInstrumentationRunnerArguments["disableAnalytics"] = "true" versionCode = versionMajor * 10000000 + versionMinor * 10000 + versionPatch * 100 + versionBuild versionName = when { versionBuild > 89 -> "${versionMajor}.${versionMinor}.${versionPatch}" versionBuild > 50 -> "${versionMajor}.${versionMinor}.${versionPatch} RC" + (versionBuild - 50) else -> "${versionMajor}.${versionMinor}.${versionPatch} Alpha" + (versionBuild + 1) } // adapt structure from Eclipse to Gradle/Android Studio expectations; // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure flavorDimensions += "default" buildTypes { release { buildConfigField("String", "NC_TEST_SERVER_DATA_STRING", "\"\"") } debug { enableUnitTestCoverage = project.hasProperty("coverage") resConfigs("xxxhdpi") buildConfigField( "String", "NC_TEST_SERVER_DATA_STRING", "\"nc://login/user:${ncTestServerUsername}&password:${ncTestServerPassword}&server:${ncTestServerBaseUrl}\"" ) } } productFlavors { // used for f-droid register("generic") { applicationId = "com.nextcloud.client" dimension = "default" } register("gplay") { applicationId = "com.nextcloud.client" dimension = "default" } register("huawei") { applicationId = "com.nextcloud.client" dimension = "default" } register("versionDev") { applicationId = "com.nextcloud.android.beta" dimension = "default" versionCode = 20220322 versionName = "20220322" } register("qa") { applicationId = "com.nextcloud.android.qa" dimension = "default" versionCode = 1 versionName = "1" } } } applicationVariants.configureEach { outputs.configureEach { if (this is ApkVariantOutputImpl) this.outputFileName = "${this.baseName}-${this.versionCode}.apk" } } testOptions { unitTests.isReturnDefaultValues = true animationsDisabled = true } // adapt structure from Eclipse to Gradle/Android Studio expectations; // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure packaging.resources { excludes.addAll(listOf("META-INF/LICENSE*", "META-INF/versions/9/OSGI-INF/MANIFEST*")) pickFirsts.add("MANIFEST.MF") // workaround for duplicated manifest on some dependencies } buildFeatures { buildConfig = true dataBinding = true viewBinding = true aidl = true compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } lint { abortOnError = true warningsAsErrors = true checkGeneratedSources = true disable.addAll( listOf( "MissingTranslation", "GradleDependency", "VectorPath", "IconMissingDensityFolder", "IconDensities", "GoogleAppIndexingWarning", "MissingDefaultResource", "InvalidPeriodicWorkRequestInterval", "StringFormatInvalid", "MissingQuantity", "IconXmlAndPng", "SelectedPhotoAccess", "UnsafeIntentLaunch" ) ) htmlOutput = layout.buildDirectory.file("reports/lint/lint.html").get().asFile htmlReport = true } sourceSets { // Adds exported schema location as test app assets. getByName("androidTest") { assets.srcDirs(files("$projectDir/schemas")) } } } kapt.useBuildCache = true ksp.arg("room.schemaLocation", "$projectDir/schemas") kotlin.compilerOptions.jvmTarget.set(JvmTarget.JVM_17) spotless.kotlin { target("**/*.kt") ktlint() } detekt.config.setFrom("detekt.yml") if (shotTest) configure { showOnlyFailingTestsInReports = ciBuild // CI environment renders some shadows slightly different from local VMs // Add a 0.5% tolerance to account for that tolerance = if (ciBuild) 0.1 else 0.0 } spotbugs { ignoreFailures = true // should continue checking effort = Effort.MAX reportLevel = Confidence.valueOf("MEDIUM") } tasks.register("checkstyle") { configFile = file("${rootProject.projectDir}/checkstyle.xml") setConfigProperties( "checkstyleSuppressionsPath" to file("${rootProject.rootDir}/suppressions.xml").absolutePath ) source("src") include("**/*.java") exclude("**/gen/**") classpath = files() } tasks.register("pmd") { ruleSetFiles = files("${rootProject.rootDir}/ruleset.xml") ignoreFailures = true // should continue checking ruleSets = emptyList() source("src") include("**/*.java") exclude("**/gen/**") reports { xml.outputLocation.set(layout.buildDirectory.file("reports/pmd/pmd.xml").get().asFile) html.outputLocation.set(layout.buildDirectory.file("reports/pmd/pmd.html").get().asFile) } } tasks.withType().configureEach { val variantNameCap = name.replace("spotbugs", "") val variantName = variantNameCap.substring(0, 1).lowercase() + variantNameCap.substring(1) dependsOn("compile${variantNameCap}Sources") classes = fileTree( layout.buildDirectory.get().asFile.toString() + "/intermediates/javac/${variantName}/compile${variantNameCap}JavaWithJavac/classes/" ) excludeFilter.set(file("${project.rootDir}/scripts/analysis/spotbugs-filter.xml")) reports.create("xml") { required.set(true) } reports.create("html") { required.set(true) outputLocation.set(layout.buildDirectory.file("reports/spotbugs/spotbugs.html")) setStylesheet("fancy.xsl") } } // Run the compiler as a separate process tasks.withType().configureEach { options.isFork = true // Enable Incremental Compilation options.isIncremental = true } tasks.withType().configureEach { // Run tests in parallel maxParallelForks = Runtime.getRuntime().availableProcessors().div(2) // increased logging for tests testLogging.events("passed", "skipped", "failed") } tasks.named("check").configure { dependsOn("checkstyle", "spotbugsGplayDebug", "pmd", "lint", "spotlessKotlinCheck", "detekt") } dependencies { // region Nextcloud library implementation(libs.android.library) { exclude(group = "org.ogce", module = "xpp3") // unused in Android and brings wrong Junit version } // endregion // region Splash Screen implementation(libs.splashscreen) // endregion // region Jetpack Compose implementation(platform(libs.compose.bom)) implementation(libs.material.icons.core) implementation(libs.compose.ui) implementation(libs.compose.ui.graphics) implementation(libs.compose.material3) implementation(libs.compose.ui.tooling.preview) debugImplementation(libs.compose.ui.tooling) // endregion // region Media3 implementation(libs.bundles.media3) // endregion // region Room implementation(libs.room.runtime) ksp(libs.room.compiler) androidTestImplementation(libs.room.testing) // endregion // region Espresso androidTestImplementation(libs.bundles.espresso) // endregion // region Glide implementation(libs.glide) ksp(libs.ksp) // endregion // region UI implementation(libs.bundles.ui) // endregion // region Worker implementation(libs.work.runtime) implementation(libs.work.runtime.ktx) // endregion // region Lifecycle implementation(libs.lifecycle.viewmodel.ktx) implementation(libs.lifecycle.service) implementation(libs.lifecycle.runtime.ktx) // endregion // region JUnit androidTestImplementation(libs.junit) androidTestImplementation(libs.rules) androidTestImplementation(libs.runner) androidTestUtil(libs.orchestrator) androidTestImplementation(libs.core.ktx) androidTestImplementation(libs.core.testing) // endregion // region other libraries compileOnly(libs.org.jbundle.util.osgi.wrapped.org.apache.http.client) implementation(libs.commons.httpclient.commons.httpclient) // remove after entire switch to lib v2 implementation(libs.jackrabbit.webdav) // remove after entire switch to lib v2 implementation(libs.constraintlayout) implementation(libs.legacy.support.v4) implementation(libs.material) implementation(libs.disklrucache) implementation(libs.juniversalchardet) // need this version for Android <7 compileOnly(libs.annotations) implementation(libs.commons.io) implementation(libs.eventbus) implementation(libs.ez.vcard) implementation(libs.nnio) implementation(libs.bcpkix.jdk18on) implementation(libs.gson) implementation(libs.sectioned.recyclerview) implementation(libs.photoview) implementation(libs.android.gif.drawable) implementation(libs.qrcodescanner) // "com.github.blikoon:QRCodeScanner:0.1.2" implementation(libs.flexbox) implementation(libs.androidsvg) implementation(libs.annotation) implementation(libs.emoji.google) // endregion // region AppScan, document scanner not available on FDroid (generic) due to OpenCV binaries "gplayImplementation"(project(":appscan")) "huaweiImplementation"(project(":appscan")) "qaImplementation"(project(":appscan")) // endregion // region SpotBugs spotbugsPlugins(libs.findsecbugs.plugin) spotbugsPlugins(libs.fb.contrib) // endregion // region Dagger implementation(libs.dagger) implementation(libs.dagger.android) implementation(libs.dagger.android.support) ksp(libs.dagger.compiler) ksp(libs.dagger.processor) // endregion // region Crypto implementation(libs.conscrypt.android) // endregion // region Library implementation(libs.library) // endregion // region Shimmer implementation(libs.loaderviewlibrary) // endregion // region Markdown rendering implementation(libs.bundles.markdown.rendering) kapt(libs.prism4j.bundler) // endregion // region Image cropping / rotation implementation(libs.android.image.cropper) // endregion // region Maps implementation(libs.osmdroid.android) // endregion // region iCal4j implementation(libs.ical4j) { listOf("org.apache.commons", "commons-logging").forEach { groupName -> exclude(group = groupName) } } // endregion // region LeakCanary if (perfAnalysis) debugImplementation(libs.leakcanary) // endregion // region Local Unit Test testImplementation(libs.bundles.unit.test) // endregion // region Mocking support androidTestImplementation(libs.bundles.mocking) // endregion // region UIAutomator // UIAutomator - for cross-app UI tests, and to grant screen is turned on in Espresso tests // androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0" // fix conflict in dependencies; see http://g.co/androidstudio/app-test-app-conflict for details // androidTestImplementation("com.android.support:support-annotations:${supportLibraryVersion}" androidTestImplementation(libs.screengrab) // endregion // region Kotlin implementation(libs.kotlin.stdlib) // endregion // region Stateless implementation(libs.stateless4j) // endregion // region Google Play dependencies, upon each update first test: new registration, receive push "gplayImplementation"(libs.bundles.gplay) // endregion // region UI implementation(libs.ui) // endregion // region Image loading implementation(libs.coil) // endregion // kotlinx.serialization implementation(libs.kotlinx.serialization.json) }