import com.android.build.gradle.internal.api.ApkVariantOutputImpl import config.PublicKeys import config.setProductNames import org.gradle.api.tasks.testing.logging.TestExceptionFormat import utils.* plugins { alias(libs.plugins.sonarqube) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.rust.android) id("com.android.application") id("kotlin-android") alias(libs.plugins.ksp) alias(libs.plugins.compose.compiler) alias(libs.plugins.stem) } // only apply the plugin if we are dealing with an AppGallery build if (gradle.startParameter.taskRequests.toString().contains("Hms")) { logger.info("enabling hms plugin") apply { plugin("com.huawei.agconnect") } } /** * Only use the scheme ".." for the appVersion */ val appVersion = "6.0.0" /** * betaSuffix with leading dash (e.g. `-beta1`). * Should be one of (alpha|beta|rc) and an increasing number, or empty for a regular release. * Note: in nightly builds this will be overwritten with a nightly version "-n12345" */ val betaSuffix = "" val defaultVersionCode = 1070 /** * Map with keystore paths (if found). */ val keystores: Map = mapOf( "debug" to findKeystore(projectDir, "debug"), "release" to findKeystore(projectDir, "threema"), "hms_release" to findKeystore(projectDir, "threema_hms"), "onprem_release" to findKeystore(projectDir, "onprem"), "blue_release" to findKeystore(projectDir, "threema_blue"), ) android { // NOTE: When adjusting compileSdkVersion, buildToolsVersion or ndkVersion, // make sure to adjust them in `scripts/Dockerfile` as well! compileSdk = 35 buildToolsVersion = "35.0.0" ndkVersion = "25.2.9519653" defaultConfig { // https://developer.android.com/training/testing/espresso/setup#analytics with(testInstrumentationRunnerArguments) { put("notAnnotation", "ch.threema.app.TestFastlaneOnly,ch.threema.app.DangerousTest") put("disableAnalytics", "true") } minSdk = 21 //noinspection OldTargetApi targetSdk = 34 vectorDrawables.useSupportLibrary = true applicationId = "ch.threema.app" testApplicationId = "ch.threema.app.test" versionCode = defaultVersionCode versionName = "$appVersion$betaSuffix" setProductNames( appName = "Threema", ) // package name used for sync adapter - needs to match mime types below stringResValue("package_name", applicationId!!) stringResValue("contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.profile") stringResValue("call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.call") intBuildConfigField("MAX_GROUP_SIZE", 256) stringBuildConfigField("CHAT_SERVER_PREFIX", "g-") stringBuildConfigField("CHAT_SERVER_IPV6_PREFIX", "ds.g-") stringBuildConfigField("CHAT_SERVER_SUFFIX", ".0.threema.ch") intArrayBuildConfigField("CHAT_SERVER_PORTS", intArrayOf(5222, 443)) stringBuildConfigField("MEDIA_PATH", "Threema") booleanBuildConfigField("CHAT_SERVER_GROUPS", true) booleanBuildConfigField("DISABLE_CERT_PINNING", false) booleanBuildConfigField("VIDEO_CALLS_ENABLED", true) // This public key is pinned for the chat server protocol. byteArrayBuildConfigField("SERVER_PUBKEY", PublicKeys.prodServer) byteArrayBuildConfigField("SERVER_PUBKEY_ALT", PublicKeys.prodServerAlt) stringBuildConfigField("GIT_HASH", getGitHash()) stringBuildConfigField("DIRECTORY_SERVER_URL", "https://apip.threema.ch/") stringBuildConfigField("DIRECTORY_SERVER_IPV6_URL", "https://ds-apip.threema.ch/") stringBuildConfigField("WORK_SERVER_URL", null) stringBuildConfigField("WORK_SERVER_IPV6_URL", null) stringBuildConfigField("MEDIATOR_SERVER_URL", "wss://mediator-{deviceGroupIdPrefix4}.threema.ch/{deviceGroupIdPrefix8}") // Base blob url used for "download" and "done" calls stringBuildConfigField("BLOB_SERVER_URL", "https://blobp-{blobIdPrefix}.threema.ch") stringBuildConfigField("BLOB_SERVER_IPV6_URL", "https://ds-blobp-{blobIdPrefix}.threema.ch") // Specific blob url used for "upload" calls stringBuildConfigField("BLOB_SERVER_URL_UPLOAD", "https://blobp-upload.threema.ch/upload") stringBuildConfigField("BLOB_SERVER_IPV6_URL_UPLOAD", "https://ds-blobp-upload.threema.ch/upload") // Base blob mirror url used for "download", "upload", "done" stringBuildConfigField("BLOB_MIRROR_SERVER_URL", "https://blob-mirror-{deviceGroupIdPrefix4}.threema.ch/{deviceGroupIdPrefix8}") stringBuildConfigField("AVATAR_FETCH_URL", "https://avatar.threema.ch/") stringBuildConfigField("SAFE_SERVER_URL", "https://safe-{backupIdPrefix8}.threema.ch/") stringBuildConfigField("WEB_SERVER_URL", "https://web.threema.ch/") stringBuildConfigField("APP_RATING_URL", "https://threema.ch/app-rating/android/{rating}") stringBuildConfigField("MAP_STYLES_URL", "https://map.threema.ch/styles/streets/style.json") stringBuildConfigField("MAP_POI_URL", "https://poi.threema.ch/around/{latitude}/{longitude}/{radius}/") stringBuildConfigField("MAP_POI_NAMES_URL", "https://poi.threema.ch/names/{latitude}/{longitude}/{query}/") byteArrayBuildConfigField("THREEMA_PUSH_PUBLIC_KEY", PublicKeys.threemaPush) stringBuildConfigField("ONPREM_ID_PREFIX", "O") stringBuildConfigField("LOG_TAG", "3ma") stringBuildConfigField("DEFAULT_APP_THEME", "2") stringArrayBuildConfigField("ONPREM_CONFIG_TRUSTED_PUBLIC_KEYS", emptyArray()) booleanBuildConfigField("MD_SYNC_DISTRIBUTION_LISTS", false) booleanBuildConfigField("EDIT_MESSAGES_ENABLED", true) booleanBuildConfigField("DELETE_MESSAGES_ENABLED", true) booleanBuildConfigField("EMOJI_REACTIONS_ENABLED", true) booleanBuildConfigField("EMOJI_REACTIONS_WEB_ENABLED", true) // config fields for action URLs / deep links stringBuildConfigField("uriScheme", "threema") stringBuildConfigField("actionUrl", "go.threema.ch") stringBuildConfigField("contactActionUrl", "threema.id") stringBuildConfigField("groupLinkActionUrl", "threema.group") with(manifestPlaceholders) { put("uriScheme", "threema") put("contactActionUrl", "threema.id") put("groupLinkActionUrl", "threema.group") put("actionUrl", "go.threema.ch") put("callMimeType", "vnd.android.cursor.item/vnd.ch.threema.app.call") } testInstrumentationRunner = "ch.threema.app.ThreemaTestRunner" // Only include language resources for those languages androidResources.localeFilters.addAll( setOf( "en", "be-rBY", "ca", "cs", "de", "es", "fr", "gsw", "hu", "it", "ja", "nl-rNL", "no", "pl", "pt-rBR", "ru", "sk", "tr", "uk", "zh-rCN", "zh-rTW", ), ) } splits { abi { isEnable = true reset() if (project.hasProperty("noAbiSplits")) { isUniversalApk = true } else { include("armeabi-v7a", "x86", "arm64-v8a", "x86_64") isUniversalApk = project.hasProperty("buildUniversalApk") } } } // Assign different version code for each output android.applicationVariants.all { outputs.all { if (this is ApkVariantOutputImpl) { val abi = getFilter("ABI") val abiVersionCode = when (abi) { "armeabi-v7a" -> 2 "arm64-v8a" -> 3 "x86" -> 8 "x86_64" -> 9 else -> 0 } versionCodeOverride = abiVersionCode * 1_000_000 + defaultVersionCode } } } namespace = "ch.threema.app" flavorDimensions.add("default") productFlavors { create("none") create("store_google") create("store_threema") { stringResValue("shop_download_filename", "Threema-update.apk") } create("store_google_work") { versionName = "${appVersion}k$betaSuffix" applicationId = "ch.threema.app.work" testApplicationId = "ch.threema.app.work.test" setProductNames(appName = "Threema Work") stringResValue("package_name", applicationId!!) stringResValue("contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.profile") stringResValue("call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.call") stringBuildConfigField("CHAT_SERVER_PREFIX", "w-") stringBuildConfigField("CHAT_SERVER_IPV6_PREFIX", "ds.w-") stringBuildConfigField("MEDIA_PATH", "ThreemaWork") stringBuildConfigField("WORK_SERVER_URL", "https://apip-work.threema.ch/") stringBuildConfigField("WORK_SERVER_IPV6_URL", "https://ds-apip-work.threema.ch/") stringBuildConfigField("APP_RATING_URL", "https://threema.ch/app-rating/android-work/{rating}") stringBuildConfigField("LOG_TAG", "3mawrk") stringBuildConfigField("DEFAULT_APP_THEME", "2") // config fields for action URLs / deep links stringBuildConfigField("uriScheme", "threemawork") stringBuildConfigField("actionUrl", "work.threema.ch") with(manifestPlaceholders) { put("uriScheme", "threemawork") put("actionUrl", "work.threema.ch") put("callMimeType", "vnd.android.cursor.item/vnd.ch.threema.app.work.call") } } create("green") { applicationId = "ch.threema.app.green" testApplicationId = "ch.threema.app.green.test" setProductNames(appName = "Threema Green") stringResValue("package_name", applicationId!!) stringResValue("contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.green.profile") stringResValue("call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.green.call") stringBuildConfigField("MEDIA_PATH", "ThreemaGreen") stringBuildConfigField("CHAT_SERVER_SUFFIX", ".0.test.threema.ch") // This public key is pinned for the chat server protocol. byteArrayBuildConfigField("SERVER_PUBKEY", PublicKeys.sandboxServer) byteArrayBuildConfigField("SERVER_PUBKEY_ALT", PublicKeys.sandboxServer) stringBuildConfigField("DIRECTORY_SERVER_URL", "https://apip.test.threema.ch/") stringBuildConfigField("DIRECTORY_SERVER_IPV6_URL", "https://ds-apip.test.threema.ch/") stringBuildConfigField("MEDIATOR_SERVER_URL", "wss://mediator-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}") stringBuildConfigField("AVATAR_FETCH_URL", "https://avatar.test.threema.ch/") stringBuildConfigField("APP_RATING_URL", "https://test.threema.ch/app-rating/android/{rating}") stringBuildConfigField("BLOB_MIRROR_SERVER_URL", "https://blob-mirror-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}") } create("sandbox_work") { versionName = "${appVersion}k$betaSuffix" applicationId = "ch.threema.app.sandbox.work" testApplicationId = "ch.threema.app.sandbox.work.test" setProductNames( appName = "Threema Sandbox Work", appNameDesktop = "Threema Blue", ) stringResValue("package_name", applicationId!!) stringResValue("contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.sandbox.work.profile") stringResValue("call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.sandbox.work.call") stringBuildConfigField("CHAT_SERVER_PREFIX", "w-") stringBuildConfigField("CHAT_SERVER_IPV6_PREFIX", "ds.w-") stringBuildConfigField("CHAT_SERVER_SUFFIX", ".0.test.threema.ch") stringBuildConfigField("MEDIA_PATH", "ThreemaWorkSandbox") // This public key is pinned for the chat server protocol. byteArrayBuildConfigField("SERVER_PUBKEY", PublicKeys.sandboxServer) byteArrayBuildConfigField("SERVER_PUBKEY_ALT", PublicKeys.sandboxServer) stringBuildConfigField("DIRECTORY_SERVER_URL", "https://apip.test.threema.ch/") stringBuildConfigField("DIRECTORY_SERVER_IPV6_URL", "https://ds-apip.test.threema.ch/") stringBuildConfigField("WORK_SERVER_URL", "https://apip-work.test.threema.ch/") stringBuildConfigField("WORK_SERVER_IPV6_URL", "https://ds-apip-work.test.threema.ch/") stringBuildConfigField("MEDIATOR_SERVER_URL", "wss://mediator-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}") stringBuildConfigField("AVATAR_FETCH_URL", "https://avatar.test.threema.ch/") stringBuildConfigField("APP_RATING_URL", "https://test.threema.ch/app-rating/android-work/{rating}") stringBuildConfigField("LOG_TAG", "3mawrk") stringBuildConfigField("DEFAULT_APP_THEME", "2") stringBuildConfigField("BLOB_MIRROR_SERVER_URL", "https://blob-mirror-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}") // config fields for action URLs / deep links stringBuildConfigField("uriScheme", "threemawork") stringBuildConfigField("actionUrl", "work.test.threema.ch") stringBuildConfigField("MD_CLIENT_DOWNLOAD_URL", "https://three.ma/mdw") with(manifestPlaceholders) { put("uriScheme", "threemawork") put("actionUrl", "work.test.threema.ch") } } create("onprem") { versionName = "${appVersion}o$betaSuffix" applicationId = "ch.threema.app.onprem" testApplicationId = "ch.threema.app.onprem.test" setProductNames( appName = "Threema OnPrem", shortAppName = "Threema", companyName = "Threema", ) stringResValue("package_name", applicationId!!) stringResValue("contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.onprem.profile") stringResValue("call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.onprem.call") intBuildConfigField("MAX_GROUP_SIZE", 256) stringBuildConfigField("CHAT_SERVER_PREFIX", "") stringBuildConfigField("CHAT_SERVER_IPV6_PREFIX", "") stringBuildConfigField("CHAT_SERVER_SUFFIX", null) stringBuildConfigField("MEDIA_PATH", "ThreemaOnPrem") booleanBuildConfigField("CHAT_SERVER_GROUPS", false) byteArrayBuildConfigField("SERVER_PUBKEY", null) byteArrayBuildConfigField("SERVER_PUBKEY_ALT", null) stringBuildConfigField("DIRECTORY_SERVER_URL", null) stringBuildConfigField("DIRECTORY_SERVER_IPV6_URL", null) stringBuildConfigField("BLOB_SERVER_URL", null) stringBuildConfigField("BLOB_SERVER_IPV6_URL", null) stringBuildConfigField("BLOB_SERVER_URL_UPLOAD", null) stringBuildConfigField("BLOB_SERVER_IPV6_URL_UPLOAD", null) stringBuildConfigField("BLOB_MIRROR_SERVER_URL", null) stringArrayBuildConfigField("ONPREM_CONFIG_TRUSTED_PUBLIC_KEYS", PublicKeys.onPremTrusted) stringBuildConfigField("LOG_TAG", "3maop") // config fields for action URLs / deep links stringBuildConfigField("uriScheme", "threemaonprem") stringBuildConfigField("actionUrl", "onprem.threema.ch") stringBuildConfigField("MD_CLIENT_DOWNLOAD_URL", "https://three.ma/mdo") with(manifestPlaceholders) { put("uriScheme", "threemaonprem") put("actionUrl", "onprem.threema.ch") put("callMimeType", "vnd.android.cursor.item/vnd.ch.threema.app.onprem.call") } } create("blue") { // Essentially like sandbox work, but with a different icon and application id, used for internal testing versionName = "${appVersion}b$betaSuffix" // The app was previously named `red`. The app id remains unchanged to still be able to install updates. applicationId = "ch.threema.app.red" testApplicationId = "ch.threema.app.blue.test" setProductNames(appName = "Threema Blue") stringResValue("package_name", applicationId!!) stringResValue("contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.blue.profile") stringResValue("call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.blue.call") stringBuildConfigField("CHAT_SERVER_PREFIX", "w-") stringBuildConfigField("CHAT_SERVER_IPV6_PREFIX", "ds.w-") stringBuildConfigField("CHAT_SERVER_SUFFIX", ".0.test.threema.ch") stringBuildConfigField("MEDIA_PATH", "ThreemaBlue") // This public key is pinned for the chat server protocol. byteArrayBuildConfigField("SERVER_PUBKEY", PublicKeys.sandboxServer) byteArrayBuildConfigField("SERVER_PUBKEY_ALT", PublicKeys.sandboxServer) stringBuildConfigField("DIRECTORY_SERVER_URL", "https://apip.test.threema.ch/") stringBuildConfigField("DIRECTORY_SERVER_IPV6_URL", "https://ds-apip.test.threema.ch/") stringBuildConfigField("WORK_SERVER_URL", "https://apip-work.test.threema.ch/") stringBuildConfigField("WORK_SERVER_IPV6_URL", "https://ds-apip-work.test.threema.ch/") stringBuildConfigField("MEDIATOR_SERVER_URL", "wss://mediator-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}") stringBuildConfigField("AVATAR_FETCH_URL", "https://avatar.test.threema.ch/") stringBuildConfigField("APP_RATING_URL", "https://test.threema.ch/app-rating/android-work/{rating}") stringBuildConfigField("LOG_TAG", "3mablue") stringBuildConfigField("BLOB_MIRROR_SERVER_URL", "https://blob-mirror-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}") // config fields for action URLs / deep links stringBuildConfigField("uriScheme", "threemablue") stringBuildConfigField("actionUrl", "blue.threema.ch") with(manifestPlaceholders) { put("uriScheme", "threemablue") put("actionUrl", "blue.threema.ch") put("callMimeType", "vnd.android.cursor.item/vnd.ch.threema.app.blue.call") } } create("hms") { applicationId = "ch.threema.app.hms" } create("hms_work") { versionName = "${appVersion}k$betaSuffix" applicationId = "ch.threema.app.work.hms" testApplicationId = "ch.threema.app.work.test.hms" setProductNames(appName = "Threema Work") stringResValue("package_name", "ch.threema.app.work") stringResValue("contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.profile") stringResValue("call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.call") stringBuildConfigField("CHAT_SERVER_PREFIX", "w-") stringBuildConfigField("CHAT_SERVER_IPV6_PREFIX", "ds.w-") stringBuildConfigField("MEDIA_PATH", "ThreemaWork") stringBuildConfigField("WORK_SERVER_URL", "https://apip-work.threema.ch/") stringBuildConfigField("WORK_SERVER_IPV6_URL", "https://ds-apip-work.threema.ch/") stringBuildConfigField("APP_RATING_URL", "https://threema.ch/app-rating/android-work/{rating}") stringBuildConfigField("LOG_TAG", "3mawrk") stringBuildConfigField("DEFAULT_APP_THEME", "2") // config fields for action URLs / deep links stringBuildConfigField("uriScheme", "threemawork") stringBuildConfigField("actionUrl", "work.threema.ch") with(manifestPlaceholders) { put("uriScheme", "threemawork") put("actionUrl", "work.threema.ch") put("callMimeType", "vnd.android.cursor.item/vnd.ch.threema.app.work.call") } } create("libre") { versionName = "${appVersion}l$betaSuffix" applicationId = "ch.threema.app.libre" testApplicationId = "ch.threema.app.libre.test" stringResValue("package_name", applicationId!!) setProductNames( appName = "Threema Libre", appNameDesktop = "Threema", ) stringBuildConfigField("MEDIA_PATH", "ThreemaLibre") } } signingConfigs { // Debug config keystores["debug"] ?.let { keystore -> getByName("debug") { storeFile = keystore.storeFile } } ?: run { logger.warn("No debug keystore found. Falling back to locally generated keystore.") } // Release config keystores["release"] ?.let { keystore -> create("release") { apply(keystore) } } ?: run { logger.warn("No release keystore found. Falling back to locally generated keystore.") } // Release config keystores["hms_release"] ?.let { keystore -> create("hms_release") { apply(keystore) } } ?: run { logger.warn("No hms keystore found. Falling back to locally generated keystore.") } // Onprem release config keystores["onprem_release"] ?.let { keystore -> create("onprem_release") { apply(keystore) } } ?: run { logger.warn("No onprem keystore found. Falling back to locally generated keystore.") } // Blue release config keystores["blue_release"] ?.let { keystore -> create("blue_release") { apply(keystore) } } ?: run { logger.warn("No blue keystore found. Falling back to locally generated keystore.") } // Note: Libre release is signed with HSM, no config here } sourceSets { getByName("main") { assets.srcDirs("assets") jniLibs.srcDirs("libs") res.srcDir("src/main/res-rendezvous") } // Based on Google services getByName("none") { java.srcDir("src/google_services_based/java") } getByName("store_google") { java.srcDir("src/google_services_based/java") } getByName("store_google_work") { java.srcDir("src/google_services_based/java") } getByName("store_threema") { java.srcDir("src/google_services_based/java") } getByName("libre") { assets.srcDirs("src/foss_based/assets") java.srcDir("src/foss_based/java") } getByName("onprem") { java.srcDir("src/google_services_based/java") } getByName("green") { java.srcDir("src/google_services_based/java") manifest.srcFile("src/store_google/AndroidManifest.xml") } getByName("sandbox_work") { java.srcDir("src/google_services_based/java") res.srcDir("src/store_google_work/res") manifest.srcFile("src/store_google_work/AndroidManifest.xml") } getByName("blue") { java.srcDir("src/google_services_based/java") res.srcDir("src/blue/res") } // Based on Huawei services getByName("hms") { java.srcDir("src/hms_services_based/java") } getByName("hms_work") { java.srcDir("src/hms_services_based/java") res.srcDir("src/store_google_work/res") } // FOSS, no proprietary services getByName("libre") { assets.srcDirs("src/foss_based/assets") java.srcDir("src/foss_based/java") } } buildTypes { debug { isDebuggable = true isJniDebuggable = false ndk { debugSymbolLevel = "FULL" } enableUnitTestCoverage = false enableAndroidTestCoverage = false if (keystores["debug"] != null) { signingConfig = signingConfigs["debug"] } } release { isDebuggable = false isJniDebuggable = false isMinifyEnabled = true isShrinkResources = false // Caused inconsistencies between local and CI builds vcsInfo.include = false // For reproducible builds independent from git history proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-project.txt") ndk { debugSymbolLevel = "FULL" // 'SYMBOL_TABLE' } if (keystores["release"] != null) { val releaseSigningConfig = signingConfigs["release"] productFlavors["store_google"].signingConfig = releaseSigningConfig productFlavors["store_google_work"].signingConfig = releaseSigningConfig productFlavors["store_threema"].signingConfig = releaseSigningConfig productFlavors["green"].signingConfig = releaseSigningConfig productFlavors["sandbox_work"].signingConfig = releaseSigningConfig productFlavors["none"].signingConfig = releaseSigningConfig } if (keystores["hms_release"] != null) { val hmsReleaseSigningConfig = signingConfigs["hms_release"] productFlavors["hms"].signingConfig = hmsReleaseSigningConfig productFlavors["hms_work"].signingConfig = hmsReleaseSigningConfig } if (keystores["onprem_release"] != null) { productFlavors["onprem"].signingConfig = signingConfigs["onprem_release"] } if (keystores["blue_release"] != null) { productFlavors["blue"].signingConfig = signingConfigs["blue_release"] } // Note: Libre release is signed with HSM, no config here } } externalNativeBuild { ndkBuild { path("jni/Android.mk") } } packaging { jniLibs { // replacement for extractNativeLibs in AndroidManifest useLegacyPackaging = true } resources { excludes.addAll( setOf( "META-INF/DEPENDENCIES.txt", "META-INF/LICENSE.txt", "META-INF/NOTICE.txt", "META-INF/NOTICE", "META-INF/LICENSE", "META-INF/DEPENDENCIES", "META-INF/notice.txt", "META-INF/license.txt", "META-INF/dependencies.txt", "META-INF/LGPL2.1", "**/*.proto", "DebugProbesKt.bin", ), ) } } testOptions { // Disable animations in instrumentation tests animationsDisabled = true unitTests { all { test -> test.outputs.upToDateWhen { false } test.testLogging { events("passed", "skipped", "failed", "standardOut", "standardError") exceptionFormat = TestExceptionFormat.FULL } test.jvmArgs = test.jvmArgs!! + listOf( "--add-opens=java.base/java.util=ALL-UNNAMED", "--add-opens=java.base/java.util.stream=ALL-UNNAMED", "--add-opens=java.base/java.lang=ALL-UNNAMED", "-Xmx4096m", ) } // By default, local unit tests throw an exception any time the code you are testing tries to access // Android platform APIs (unless you mock Android dependencies yourself or with a testing // framework like Mockito). However, you can enable the following property so that the test // returns either null or zero when accessing platform APIs, rather than throwing an exception. isReturnDefaultValues = true } } compileOptions { isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) } } kotlin { jvmToolchain(17) } androidResources { noCompress.add("png") } lint { // if true, stop the gradle build if errors are found abortOnError = true // if true, check all issues, including those that are off by default checkAllWarnings = true // check dependencies checkDependencies = true // set to true to have all release builds run lint on issues with severity=fatal // and abort the build (controlled by abortOnError above) if fatal issues are found checkReleaseBuilds = true // turn off checking the given issue id's disable.addAll(setOf("TypographyFractions", "TypographyQuotes", "RtlHardcoded", "RtlCompat", "RtlEnabled")) // Set the severity of the given issues to error error.addAll(setOf("Wakelock", "TextViewEdits", "ResourceAsColor")) // Set the severity of the given issues to fatal (which means they will be // checked during release builds (even if the lint target is not included) fatal.addAll(setOf("NewApi", "InlinedApi")) ignoreWarnings = false // if true, don't include source code lines in the error output noLines = false // if true, show all locations for an error, do not truncate lists, etc. showAll = true // Set the severity of the given issues to warning warning.add("MissingTranslation") // if true, treat all warnings as errors warningsAsErrors = false // file to write report to (if not specified, defaults to lint-results.xml) xmlOutput = file("lint-report.xml") // if true, generate an XML report for use by for example Jenkins xmlReport = true } buildFeatures { compose = true buildConfig = true } } // Only build relevant buildType / flavor combinations androidComponents { beforeVariants { variant -> val name = variant.name if (variant.buildType == "release" && ("green" in name || "sandbox_work" in name)) { variant.enable = false } } } dependencies { configurations.all { // Prefer modules that are part of this build (multi-project or composite build) // over external modules resolutionStrategy.preferProjectModules() // Alternatively, we can fail eagerly on version conflict to see the conflicts // resolutionStrategy.failOnVersionConflict() } coreLibraryDesugaring(libs.desugarJdkLibs) implementation(project(":domain")) implementation(libs.sqlcipher.android) implementation(libs.subsamplingScaleImageView) implementation(libs.opencsv) implementation(libs.zip4j) implementation(libs.taptargetview) implementation(libs.commonsIo) implementation(libs.commonsText) implementation(libs.slf4j.api) implementation(libs.androidImageCropper) implementation(libs.trustkit) implementation(libs.fastscroll) implementation(libs.ezVcard) implementation(libs.gestureViews) // AndroidX / Jetpack support libraries implementation(libs.androidx.preference) implementation(libs.androidx.recyclerview) implementation(libs.androidx.palette) implementation(libs.androidx.swiperefreshlayout) implementation(libs.androidx.core) implementation(libs.androidx.appcompat) implementation(libs.androidx.constraintlayout) implementation(libs.androidx.biometric) implementation(libs.androidx.work.runtime) implementation(libs.androidx.fragment) implementation(libs.androidx.activity) implementation(libs.androidx.sqlite) implementation(libs.androidx.concurrent.futures) implementation(libs.androidx.camera2) implementation(libs.androidx.camera.lifecycle) implementation(libs.androidx.camera.view) implementation(libs.androidx.camera.video) implementation(libs.androidx.media) implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.session) implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.livedata) implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.lifecycle.viewmodel.savedstate) implementation(libs.androidx.lifecycle.service) implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.lifecycle.commonJava8) implementation(libs.androidx.lifecycle.extensions) implementation(libs.androidx.paging.runtime) implementation(libs.androidx.sharetarget) implementation(libs.androidx.room.runtime) implementation(libs.androidx.window) ksp(libs.androidx.room.compiler) // Jetpack Compose implementation(platform(libs.compose.bom)) implementation(libs.androidx.material3) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.activity.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.runtime.compose) debugImplementation(libs.androidx.ui.tooling) androidTestImplementation(platform(libs.compose.bom)) implementation(libs.bcprov.jdk15to18) implementation(libs.material) implementation(libs.zxing) implementation(libs.libphonenumber) // webclient dependencies implementation(libs.msgpack.core) implementation(libs.jackson.core) implementation(libs.nvWebsocket.client) implementation(libs.streamsupport.cfuture) implementation(libs.saltyrtc.client) { exclude(group = "org.json") } implementation(libs.chunkedDc) implementation(libs.webrtcAndroid) implementation(libs.saltyrtc.taskWebrtc) { exclude(module = "saltyrtc-client") } // Glide components implementation(libs.glide) ksp(libs.glide.compiler) annotationProcessor(libs.glide.compiler) // Kotlin implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.serialization.json) testImplementation(libs.kotlin.test) androidTestImplementation(libs.kotlin.test) // use leak canary in debug builds if requested if (project.hasProperty("leakCanary")) { debugImplementation(libs.leakcanary) } // test dependencies testImplementation(libs.junit) testImplementation(testFixtures(project(":domain"))) // custom test helpers, shared between unit test and android tests testImplementation(project(":test-helpers")) androidTestImplementation(project(":test-helpers")) testImplementation(libs.mockito.powermock.api) testImplementation(libs.mockito.powermock.junit4RuleAgent) testImplementation(libs.mockito.powermock.junit4Rule) testImplementation(libs.mockito.powermock.junit4) testImplementation(libs.mockk) // add JSON support to tests without mocking testImplementation(libs.json) testImplementation(libs.archunit.junit4) androidTestImplementation(testFixtures(project(":domain"))) androidTestImplementation(libs.androidx.test.rules) androidTestImplementation(libs.fastlane.screengrab) { exclude(group = "androidx.annotation", module = "annotation") } androidTestImplementation(libs.androidx.espresso.core) { exclude(group = "androidx.annotation", module = "annotation") } androidTestImplementation(libs.androidx.test.runner) { exclude(group = "androidx.annotation", module = "annotation") } androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.contrib) { exclude(group = "androidx.annotation", module = "annotation") exclude(group = "androidx.appcompat", module = "appcompat") exclude(group = "androidx.legacy", module = "legacy-support-v4") exclude(group = "com.google.android.material", module = "material") exclude(group = "androidx.recyclerview", module = "recyclerview") exclude(group = "org.checkerframework", module = "checker") exclude(module = "protobuf-lite") } androidTestImplementation(libs.androidx.espresso.intents) { exclude(group = "androidx.annotation", module = "annotation") } androidTestImplementation(libs.androidx.test.uiautomator) androidTestImplementation(libs.androidx.test.core) androidTestImplementation(libs.mockito.core) androidTestImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.kotlinx.coroutines.test) // Google Play Services and related libraries "noneImplementation"(libs.playServices.base) "store_googleImplementation"(libs.playServices.base) "store_google_workImplementation"(libs.playServices.base) "store_threemaImplementation"(libs.playServices.base) "onpremImplementation"(libs.playServices.base) "greenImplementation"(libs.playServices.base) "sandbox_workImplementation"(libs.playServices.base) "blueImplementation"(libs.playServices.base) fun ExternalModuleDependency.excludeFirebaseDependencies() { exclude(group = "com.google.firebase", module = "firebase-core") exclude(group = "com.google.firebase", module = "firebase-analytics") exclude(group = "com.google.firebase", module = "firebase-measurement-connector") } "noneImplementation"(libs.firebase.messaging) { excludeFirebaseDependencies() } "store_googleImplementation"(libs.firebase.messaging) { excludeFirebaseDependencies() } "store_google_workImplementation"(libs.firebase.messaging) { excludeFirebaseDependencies() } "store_threemaImplementation"(libs.firebase.messaging) { excludeFirebaseDependencies() } "onpremImplementation"(libs.firebase.messaging) { excludeFirebaseDependencies() } "greenImplementation"(libs.firebase.messaging) { excludeFirebaseDependencies() } "sandbox_workImplementation"(libs.firebase.messaging) { excludeFirebaseDependencies() } "blueImplementation"(libs.firebase.messaging) { excludeFirebaseDependencies() } // Google Assistant Voice Action verification library "noneImplementation"(group = "", name = "libgsaverification-client", ext = "aar") "store_googleImplementation"(group = "", name = "libgsaverification-client", ext = "aar") "store_google_workImplementation"(group = "", name = "libgsaverification-client", ext = "aar") "onpremImplementation"(group = "", name = "libgsaverification-client", ext = "aar") "store_threemaImplementation"(group = "", name = "libgsaverification-client", ext = "aar") "greenImplementation"(group = "", name = "libgsaverification-client", ext = "aar") "sandbox_workImplementation"(group = "", name = "libgsaverification-client", ext = "aar") "blueImplementation"(group = "", name = "libgsaverification-client", ext = "aar") // Maplibre (may have transitive dependencies on Google location services) "noneImplementation"(libs.maplibre) "store_googleImplementation"(libs.maplibre) "store_google_workImplementation"(libs.maplibre) "store_threemaImplementation"(libs.maplibre) "libreImplementation"(libs.maplibre) { exclude(group = "com.google.android.gms") } "onpremImplementation"(libs.maplibre) "greenImplementation"(libs.maplibre) "sandbox_workImplementation"(libs.maplibre) "blueImplementation"(libs.maplibre) "hmsImplementation"(libs.maplibre) "hms_workImplementation"(libs.maplibre) // Huawei related libraries (only for hms* build variants) // Exclude agconnect dependency, we'll replace it with the vendored version below "hmsImplementation"(libs.hmsPush) { exclude(group = "com.huawei.agconnect") } "hms_workImplementation"(libs.hmsPush) { exclude(group = "com.huawei.agconnect") } "hmsImplementation"(group = "", name = "agconnect-core-1.9.1.301", ext = "aar") "hms_workImplementation"(group = "", name = "agconnect-core-1.9.1.301", ext = "aar") } // Define the cargo attributes. These will be used by the rust-android plugin that will create the // 'cargoBuild' task that builds native libraries that will be added to the apk. Note that the // kotlin bindings are created in the domain module. Building native libraries with rust-android // cannot be done in any other module than 'app'. cargo { prebuiltToolchains = true targetDirectory = "$projectDir/build/generated/source/libthreema" module = "$projectDir/../domain/libthreema" // must contain Cargo.toml libname = "libthreema" // must match the Cargo.toml's package name profile = "release" pythonCommand = "python3" targets = listOf("x86_64", "arm64", "arm", "x86") features { defaultAnd(arrayOf("uniffi")) } extraCargoBuildArguments = listOf("--lib", "--target-dir", "$projectDir/build/generated/source/libthreema") verbose = false } afterEvaluate { // The `cargoBuild` task isn't available until after evaluation. android.applicationVariants.configureEach { val variantName = name.replaceFirstChar { it.uppercase() } // Set the dependency so that cargoBuild is executed before the native libs are merged tasks["merge${variantName}NativeLibs"].dependsOn(tasks["cargoBuild"]) } } sonarqube { properties { property("sonar.sources", "src/main/, ../scripts/, ../scripts-internal/") property( "sonar.exclusions", "src/main/java/ch/threema/localcrypto/**, src/test/java/ch/threema/localcrypto/**, src/*/res/, src/*/res-rendezvous/", ) property("sonar.tests", "src/test/") property("sonar.sourceEncoding", "UTF-8") property("sonar.verbose", "true") property("sonar.projectKey", "android-client") property("sonar.projectName", "Threema for Android") } } androidStem { includeLocalizedOnlyTemplates = true } // Set up Gradle tasks to fetch screenshots on UI test failures // See https://medium.com/stepstone-tech/how-to-capture-screenshots-for-failed-ui-tests-9927eea6e1e4 val reportsDirectory = "${layout.buildDirectory}/reports/androidTests/connected" val screenshotsDirectory = "/sdcard/testfailures/screenshots/" val clearScreenshotsTask = task("clearScreenshots") { executable = android.adbExecutable.toString() args("shell", "rm", "-r", screenshotsDirectory) } val createScreenshotsDirectoryTask = task("createScreenshotsDirectory") { group = "reporting" executable = android.adbExecutable.toString() args("shell", "mkdir", "-p", screenshotsDirectory) } val fetchScreenshotsTask = task("fetchScreenshots") { group = "reporting" executable = android.adbExecutable.toString() args("pull", "$screenshotsDirectory.", reportsDirectory) finalizedBy(clearScreenshotsTask) dependsOn(createScreenshotsDirectoryTask) doFirst { file(reportsDirectory).mkdirs() } } tasks.whenTaskAdded { if (name == "connectedDebugAndroidTest") { finalizedBy(fetchScreenshotsTask) } }