import org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs plugins { id 'org.sonarqube' id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version" } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' // only apply the plugin if we are dealing with a AppGallery build if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")) { println "enabling hms plugin" apply plugin: 'com.huawei.agconnect' } // version codes // Only use the scheme ".." for the app_version def app_version = "5.5.1" // beta_suffix 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" def beta_suffix = "" def defaultVersionCode = 998 /** * Return the git hash, if git is installed. */ def getGitHash = { -> def stdout = new ByteArrayOutputStream() def stderr = new ByteArrayOutputStream() try { exec { commandLine 'git', 'rev-parse', '--short', 'HEAD' standardOutput = stdout errorOutput = stderr ignoreExitValue true } } catch (ignored) { /* If git binary is not found, carry on */ } def hash = stdout.toString().trim() return (hash.isEmpty()) ? "?" : hash } /** * Look up the keystore with the specified name in a `keystore` directory * adjacent to this project directory. If it exists, return a signing config. * Otherwise, return null. */ def findKeystore = { name -> def basePath = "${projectDir.getAbsolutePath()}/../../keystore" def storePath = "${basePath}/${name}.keystore" def storeFile = new File(storePath) if (storeFile.exists() && storeFile.isFile()) { def propertiesPath = "${basePath}/${name}.properties" def propertiesFile = new File(propertiesPath) if (propertiesFile.exists() && propertiesFile.isFile()) { Properties props = new Properties() propertiesFile.withInputStream { props.load(it) } return [ storeFile: storePath, storePassword: props.storePassword, keyAlias: props.keyAlias, keyPassword: props.keyPassword, ] } else { return [ storeFile: storePath, storePassword: null, keyAlias: null, keyPassword: null, ] } } } /** * Map with keystore paths (if found). */ def keystores = [ debug: findKeystore("debug"), release: findKeystore("threema"), hms_release: findKeystore("threema_hms"), onprem_release: findKeystore("onprem"), blue_release: findKeystore("threema_blue"), ] android { // NOTE: When adjusting compileSdkVersion, buildToolsVersion or ndkVersion, // make sure to adjust them in `scripts/Dockerfile` and // `.gitlab-ci.yml` as well! compileSdk 34 buildToolsVersion = '34.0.0' ndkVersion '25.2.9519653' defaultConfig { // https://developer.android.com/training/testing/espresso/setup#analytics testInstrumentationRunnerArguments notAnnotation: 'ch.threema.app.TestFastlaneOnly,ch.threema.app.DangerousTest', disableAnalytics: 'true' minSdkVersion 21 //noinspection OldTargetApi targetSdkVersion 33 vectorDrawables.useSupportLibrary = true applicationId "ch.threema.app" testApplicationId 'ch.threema.app.test' versionCode defaultVersionCode versionName "${app_version}${beta_suffix}" resValue "string", "app_name", "Threema" // package name used for sync adapter - needs to match mime types below resValue "string", "package_name", applicationId resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.profile" resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.call" buildConfigField "int", "MAX_GROUP_SIZE", "256" buildConfigField "String", "CHAT_SERVER_PREFIX", "\"g-\"" buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.g-\"" buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.threema.ch\"" buildConfigField "int[]", "CHAT_SERVER_PORTS", "{5222, 443}" buildConfigField "String", "MEDIA_PATH", "\"Threema\"" buildConfigField "boolean", "CHAT_SERVER_GROUPS", "true" buildConfigField "boolean", "DISABLE_CERT_PINNING", "false" buildConfigField "boolean", "VIDEO_CALLS_ENABLED", "true" buildConfigField "byte[]", "SERVER_PUBKEY", "new byte[] {(byte) 0x45, (byte) 0x0b, (byte) 0x97, (byte) 0x57, (byte) 0x35, (byte) 0x27, (byte) 0x9f, (byte) 0xde, (byte) 0xcb, (byte) 0x33, (byte) 0x13, (byte) 0x64, (byte) 0x8f, (byte) 0x5f, (byte) 0xc6, (byte) 0xee, (byte) 0x9f, (byte) 0xf4, (byte) 0x36, (byte) 0x0e, (byte) 0xa9, (byte) 0x2a, (byte) 0x8c, (byte) 0x17, (byte) 0x51, (byte) 0xc6, (byte) 0x61, (byte) 0xe4, (byte) 0xc0, (byte) 0xd8, (byte) 0xc9, (byte) 0x09 }" buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0xda, (byte) 0x7c, (byte) 0x73, (byte) 0x79, (byte) 0x8f, (byte) 0x97, (byte) 0xd5, (byte) 0x87, (byte) 0xc3, (byte) 0xa2, (byte) 0x5e, (byte) 0xbe, (byte) 0x0a, (byte) 0x91, (byte) 0x41, (byte) 0x7f, (byte) 0x76, (byte) 0xdb, (byte) 0xcc, (byte) 0xcd, (byte) 0xda, (byte) 0x29, (byte) 0x30, (byte) 0xe6, (byte) 0xa9, (byte) 0x09, (byte) 0x0a, (byte) 0xf6, (byte) 0x2e, (byte) 0xba, (byte) 0x6f, (byte) 0x15 }" buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\"" buildConfigField "String", "DIRECTORY_SERVER_URL", "\"https://apip.threema.ch/\"" buildConfigField "String", "DIRECTORY_SERVER_IPV6_URL", "\"https://ds-apip.threema.ch/\"" buildConfigField "String", "WORK_SERVER_URL", "null" buildConfigField "String", "WORK_SERVER_IPV6_URL", "null" buildConfigField "String", "MEDIATOR_SERVER_URL", "\"wss://mediator-{deviceGroupIdPrefix4}.threema.ch/{deviceGroupIdPrefix8}\"" buildConfigField "String", "BLOB_SERVER_DOWNLOAD_URL", "\"https://blobp-{blobIdPrefix}.threema.ch/{blobId}\"" buildConfigField "String", "BLOB_SERVER_DOWNLOAD_IPV6_URL", "\"https://ds-blobp-{blobIdPrefix}.threema.ch/{blobId}\"" buildConfigField "String", "BLOB_SERVER_DONE_URL", "\"https://blobp-{blobIdPrefix}.threema.ch/{blobId}/done\"" buildConfigField "String", "BLOB_SERVER_DONE_IPV6_URL", "\"https://ds-blobp-{blobIdPrefix}.threema.ch/{blobId}/done\"" buildConfigField "String", "BLOB_SERVER_UPLOAD_URL", "\"https://blobp-upload.threema.ch/upload\"" buildConfigField "String", "BLOB_SERVER_UPLOAD_IPV6_URL", "\"https://ds-blobp-upload.threema.ch/upload\"" buildConfigField "String", "AVATAR_FETCH_URL", "\"https://avatar.threema.ch/\"" buildConfigField "String", "SAFE_SERVER_URL", "\"https://safe-{backupIdPrefix8}.threema.ch/\"" buildConfigField "String", "WEB_SERVER_URL", "\"https://web.threema.ch/\"" buildConfigField "String", "APP_RATING_URL", "\"https://threema.ch/app-rating/android/{rating}\"" buildConfigField "byte[]", "THREEMA_PUSH_PUBLIC_KEY", "new byte[] {(byte) 0xfd, (byte) 0x71, (byte) 0x1e, (byte) 0x1a, (byte) 0x0d, (byte) 0xb0, (byte) 0xe2, (byte) 0xf0, (byte) 0x3f, (byte) 0xca, (byte) 0xab, (byte) 0x6c, (byte) 0x43, (byte) 0xda, (byte) 0x25, (byte) 0x75, (byte) 0xb9, (byte) 0x51, (byte) 0x36, (byte) 0x64, (byte) 0xa6, (byte) 0x2a, (byte) 0x12, (byte) 0xbd, (byte) 0x07, (byte) 0x28, (byte) 0xd8, (byte) 0x7f, (byte) 0x71, (byte) 0x25, (byte) 0xcc, (byte) 0x24}" buildConfigField "String", "ONPREM_ID_PREFIX", "\"O\"" buildConfigField "String", "LOG_TAG", "\"3ma\"" buildConfigField "String", "DEFAULT_APP_THEME", "\"2\"" buildConfigField "String[]", "ONPREM_CONFIG_TRUSTED_PUBLIC_KEYS", "null" buildConfigField "boolean", "MD_ENABLED", "false" buildConfigField "boolean", "EDIT_MESSAGES_ENABLED", "true" buildConfigField "boolean", "DELETE_MESSAGES_ENABLED", "true" // config fields for action URLs / deep links buildConfigField "String", "uriScheme", "\"threema\"" buildConfigField "String", "actionUrl", "\"go.threema.ch\"" buildConfigField "String", "contactActionUrl", "\"threema.id\"" buildConfigField "String", "groupLinkActionUrl", "\"threema.group\"" // duplicated for manifest manifestPlaceholders = [ uriScheme: "threema", contactActionUrl: "threema.id", groupLinkActionUrl: "threema.group", actionUrl: "go.threema.ch", callMimeType: "vnd.android.cursor.item/vnd.ch.threema.app.call", ] testInstrumentationRunner 'ch.threema.app.ThreemaTestRunner' // Only include language resources for those languages resourceConfigurations += [ "en", "be-rBY", "ca", "cs", "de", "es", "fr", "gsw", "hu", "it", "ja", "nl-rNL", "no", "pl", "pt-rBR", "rm", "ru", "sk", "tr", "uk", "zh-rCN", "zh-rTW" ] } splits { abi { enable true reset() include 'armeabi-v7a', 'x86', 'arm64-v8a', 'x86_64' exclude 'armeabi', 'mips', 'mips64' universalApk project.hasProperty("buildUniversalApk") } } // Assign different version code for each output def abiVersionCodes = ['armeabi-v7a': 2, 'arm64-v8a': 3, 'x86': 8, 'x86_64': 9] android.applicationVariants.all { variant -> variant.outputs.each { output -> def abi = output.getFilter("ABI") output.versionCodeOverride = abiVersionCodes.get(abi, 0) * 1000000 + defaultVersionCode } } namespace 'ch.threema.app' flavorDimensions "default" productFlavors { none { } store_google { } store_threema { resValue "string", "shop_download_filename", "Threema-update.apk" } store_google_work { versionName "${app_version}k${beta_suffix}" applicationId "ch.threema.app.work" testApplicationId 'ch.threema.app.work.test' resValue "string", "app_name", "Threema Work" resValue "string", "package_name", applicationId resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.profile" resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.call" buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\"" buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.w-\"" buildConfigField "String", "MEDIA_PATH", "\"ThreemaWork\"" buildConfigField "String", "WORK_SERVER_URL", "\"https://apip-work.threema.ch/\"" buildConfigField "String", "WORK_SERVER_IPV6_URL", "\"https://ds-apip-work.threema.ch/\"" buildConfigField "String", "APP_RATING_URL", "\"https://threema.ch/app-rating/android-work/{rating}\"" buildConfigField "String", "LOG_TAG", "\"3mawrk\"" buildConfigField "String", "DEFAULT_APP_THEME", "\"2\"" // config fields for action URLs / deep links buildConfigField "String", "uriScheme", "\"threemawork\"" buildConfigField "String", "actionUrl", "\"work.threema.ch\"" manifestPlaceholders = [ uriScheme: "threemawork", actionUrl: "work.threema.ch", callMimeType: "vnd.android.cursor.item/vnd.ch.threema.app.work.call", ] } green { // The app was previously named `sandbox`. The app id remains unchanged to still be able to install updates. applicationId "ch.threema.app.sandbox" testApplicationId 'ch.threema.app.sandbox.test' resValue "string", "app_name", "Threema Green" resValue "string", "package_name", applicationId resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.sandbox.profile" resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.sandbox.call" buildConfigField "String", "MEDIA_PATH", "\"ThreemaGreen\"" buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.test.threema.ch\"" buildConfigField "byte[]", "SERVER_PUBKEY", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }" buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }" buildConfigField "String", "DIRECTORY_SERVER_URL", "\"https://apip.test.threema.ch/\"" buildConfigField "String", "DIRECTORY_SERVER_IPV6_URL", "\"https://ds-apip.test.threema.ch/\"" buildConfigField "String", "MEDIATOR_SERVER_URL", "\"wss://mediator-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}\"" buildConfigField "String", "AVATAR_FETCH_URL", "\"https://avatar.test.threema.ch/\"" buildConfigField "String", "APP_RATING_URL", "\"https://test.threema.ch/app-rating/android/{rating}\"" buildConfigField "boolean", "MD_ENABLED", "true" } sandbox_work { versionName "${app_version}k${beta_suffix}" applicationId "ch.threema.app.sandbox.work" testApplicationId 'ch.threema.app.sandbox.work.test' resValue "string", "app_name", "Threema Sandbox Work" resValue "string", "package_name", applicationId resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.sandbox.work.profile" resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.sandbox.work.call" buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\"" buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.w-\"" buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.test.threema.ch\"" buildConfigField "String", "MEDIA_PATH", "\"ThreemaWorkSandbox\"" buildConfigField "byte[]", "SERVER_PUBKEY", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }" buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }" buildConfigField "String", "DIRECTORY_SERVER_URL", "\"https://apip.test.threema.ch/\"" buildConfigField "String", "DIRECTORY_SERVER_IPV6_URL", "\"https://ds-apip.test.threema.ch/\"" buildConfigField "String", "WORK_SERVER_URL", "\"https://apip-work.test.threema.ch/\"" buildConfigField "String", "WORK_SERVER_IPV6_URL", "\"https://ds-apip-work.test.threema.ch/\"" buildConfigField "String", "MEDIATOR_SERVER_URL", "\"wss://mediator-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}\"" buildConfigField "String", "AVATAR_FETCH_URL", "\"https://avatar.test.threema.ch/\"" buildConfigField "String", "APP_RATING_URL", "\"https://test.threema.ch/app-rating/android-work/{rating}\"" buildConfigField "String", "LOG_TAG", "\"3mawrk\"" buildConfigField "String", "DEFAULT_APP_THEME", "\"2\"" // config fields for action URLs / deep links buildConfigField "String", "uriScheme", "\"threemawork\"" buildConfigField "String", "actionUrl", "\"work.test.threema.ch\"" buildConfigField "boolean", "MD_ENABLED", "true" manifestPlaceholders = [ uriScheme : "threemawork", actionUrl : "work.test.threema.ch", ] } onprem { versionName "${app_version}o${beta_suffix}" applicationId "ch.threema.app.onprem" testApplicationId 'ch.threema.app.onprem.test' resValue "string", "app_name", "Threema OnPrem" resValue "string", "package_name", applicationId resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.onprem.profile" resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.onprem.call" buildConfigField "int", "MAX_GROUP_SIZE", "256" buildConfigField "String", "CHAT_SERVER_PREFIX", "\"\"" buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"\"" buildConfigField "String", "CHAT_SERVER_SUFFIX", "null" buildConfigField "String", "MEDIA_PATH", "\"ThreemaOnPrem\"" buildConfigField "boolean", "CHAT_SERVER_GROUPS", "false" buildConfigField "byte[]", "SERVER_PUBKEY", "null" buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "null" buildConfigField "String", "DIRECTORY_SERVER_URL", "null" buildConfigField "String", "DIRECTORY_SERVER_IPV6_URL", "null" buildConfigField "String", "BLOB_SERVER_DOWNLOAD_URL", "null" buildConfigField "String", "BLOB_SERVER_DOWNLOAD_IPV6_URL", "null" buildConfigField "String", "BLOB_SERVER_DONE_URL", "null" buildConfigField "String", "BLOB_SERVER_DONE_IPV6_URL", "null" buildConfigField "String", "BLOB_SERVER_UPLOAD_URL", "null" buildConfigField "String", "BLOB_SERVER_UPLOAD_IPV6_URL", "null" buildConfigField "String[]", "ONPREM_CONFIG_TRUSTED_PUBLIC_KEYS", "new String[] {\"ek1qBp4DyRmLL9J5sCmsKSfwbsiGNB4veDAODjkwe/k=\", \"Hrk8aCjwKkXySubI7CZ3y9Sx+oToEHjNkGw98WSRneU=\", \"5pEn1T/5bhecNWrp9NgUQweRfgVtu/I8gRb3VxGP7k4=\"}" buildConfigField "String", "LOG_TAG", "\"3maop\"" // config fields for action URLs / deep links buildConfigField "String", "uriScheme", "\"threemaonprem\"" buildConfigField "String", "actionUrl", "\"onprem.threema.ch\"" manifestPlaceholders = [ uriScheme: "threemaonprem", actionUrl: "onprem.threema.ch", callMimeType: "vnd.android.cursor.item/vnd.ch.threema.app.onprem.call", ] } onprem_internal { versionName "${app_version}o${beta_suffix}" applicationId "ch.threema.app.onprem.internal" testApplicationId 'ch.threema.app.onprem.internal.test' resValue "string", "app_name", "Threema OnPrem Internal" resValue "string", "package_name", applicationId resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.onprem.internal.profile" resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.onprem.internal.call" buildConfigField "int", "MAX_GROUP_SIZE", "256" buildConfigField "String", "CHAT_SERVER_PREFIX", "\"\"" buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"\"" buildConfigField "String", "CHAT_SERVER_SUFFIX", "null" buildConfigField "String", "MEDIA_PATH", "\"ThreemaOnPremInt\"" buildConfigField "boolean", "CHAT_SERVER_GROUPS", "false" buildConfigField "byte[]", "SERVER_PUBKEY", "null" buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "null" buildConfigField "String", "DIRECTORY_SERVER_URL", "null" buildConfigField "String", "DIRECTORY_SERVER_IPV6_URL", "null" buildConfigField "String", "BLOB_SERVER_DOWNLOAD_URL", "null" buildConfigField "String", "BLOB_SERVER_DOWNLOAD_IPV6_URL", "null" buildConfigField "String", "BLOB_SERVER_DONE_URL", "null" buildConfigField "String", "BLOB_SERVER_DONE_IPV6_URL", "null" buildConfigField "String", "BLOB_SERVER_UPLOAD_URL", "null" buildConfigField "String", "BLOB_SERVER_UPLOAD_IPV6_URL", "null" buildConfigField "String[]", "ONPREM_CONFIG_TRUSTED_PUBLIC_KEYS", "new String[] {\"ek1qBp4DyRmLL9J5sCmsKSfwbsiGNB4veDAODjkwe/k=\", \"Hrk8aCjwKkXySubI7CZ3y9Sx+oToEHjNkGw98WSRneU=\", \"5pEn1T/5bhecNWrp9NgUQweRfgVtu/I8gRb3VxGP7k4=\"}" buildConfigField "String", "LOG_TAG", "\"3maoi\"" // config fields for action URLs / deep links buildConfigField "String", "uriScheme", "\"threemaonprem\"" buildConfigField "String", "actionUrl", "\"onprem.threema.ch\"" manifestPlaceholders = [ uriScheme: "threemaonprem", actionUrl: "onprem.threema.ch", callMimeType: "vnd.android.cursor.item/vnd.ch.threema.app.onprem.internal.call", ] } blue { // Essentially like sandbox work, but with a different icon and accent color, used for internal testing versionName "${app_version}r${beta_suffix}" // 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' resValue "string", "app_name", "Threema Blue" resValue "string", "package_name", applicationId resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.blue.profile" resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.blue.call" buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\"" buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.w-\"" buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.test.threema.ch\"" buildConfigField "String", "MEDIA_PATH", "\"ThreemaBlue\"" buildConfigField "byte[]", "SERVER_PUBKEY", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }" buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }" buildConfigField "String", "DIRECTORY_SERVER_URL", "\"https://apip.test.threema.ch/\"" buildConfigField "String", "DIRECTORY_SERVER_IPV6_URL", "\"https://ds-apip.test.threema.ch/\"" buildConfigField "String", "WORK_SERVER_URL", "\"https://apip-work.test.threema.ch/\"" buildConfigField "String", "WORK_SERVER_IPV6_URL", "\"https://ds-apip-work.test.threema.ch/\"" buildConfigField "String", "MEDIATOR_SERVER_URL", "\"wss://mediator-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}\"" buildConfigField "String", "AVATAR_FETCH_URL", "\"https://avatar.test.threema.ch/\"" buildConfigField "String", "APP_RATING_URL", "\"https://test.threema.ch/app-rating/android-work/{rating}\"" buildConfigField "String", "LOG_TAG", "\"3mablue\"" // config fields for action URLs / deep links buildConfigField "String", "uriScheme", "\"threemablue\"" buildConfigField "String", "actionUrl", "\"blue.threema.ch\"" manifestPlaceholders = [ uriScheme: "threemablue", actionUrl: "blue.threema.ch", callMimeType: "vnd.android.cursor.item/vnd.ch.threema.app.blue.call", ] } hms { applicationId "ch.threema.app.hms" } hms_work { versionName "${app_version}k${beta_suffix}" applicationId "ch.threema.app.work.hms" testApplicationId 'ch.threema.app.work.test.hms' resValue "string", "app_name", "Threema Work" resValue "string", "package_name", "ch.threema.app.work" resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.profile" resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.call" buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\"" buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.w-\"" buildConfigField "String", "MEDIA_PATH", "\"ThreemaWork\"" buildConfigField "String", "WORK_SERVER_URL", "\"https://apip-work.threema.ch/\"" buildConfigField "String", "WORK_SERVER_IPV6_URL", "\"https://ds-apip-work.threema.ch/\"" buildConfigField "String", "APP_RATING_URL", "\"https://threema.ch/app-rating/android-work/{rating}\"" buildConfigField "String", "LOG_TAG", "\"3mawrk\"" buildConfigField "String", "DEFAULT_APP_THEME", "\"2\"" // config fields for action URLs / deep links buildConfigField "String", "uriScheme", "\"threemawork\"" buildConfigField "String", "actionUrl", "\"work.threema.ch\"" manifestPlaceholders = [ uriScheme: "threemawork", actionUrl: "work.threema.ch", callMimeType: "vnd.android.cursor.item/vnd.ch.threema.app.work.call", ] } libre { versionName "${app_version}l${beta_suffix}" applicationId "ch.threema.app.libre" testApplicationId 'ch.threema.app.libre.test' resValue "string", "package_name", applicationId resValue "string", "app_name", "Threema Libre" buildConfigField "String", "MEDIA_PATH", "\"ThreemaLibre\"" } } signingConfigs { // Debug config if (keystores.debug != null) { debug { storeFile file(keystores.debug.storeFile) } } else { logger.warn("No debug keystore found. Falling back to locally generated keystore.") } // Release config if (keystores.release != null) { release { storeFile file(keystores.release.storeFile) storePassword keystores.release.storePassword keyAlias keystores.release.keyAlias keyPassword keystores.release.keyPassword } } else { logger.warn("No release keystore found. Falling back to locally generated keystore.") } // Release config if (keystores.hms_release != null) { hms_release { storeFile file(keystores.hms_release.storeFile) storePassword keystores.hms_release.storePassword keyAlias keystores.hms_release.keyAlias keyPassword keystores.hms_release.keyPassword } } else { logger.warn("No hms keystore found. Falling back to locally generated keystore.") } // Onprem release config if (keystores.onprem_release != null) { onprem_release { storeFile file(keystores.onprem_release.storeFile) storePassword keystores.onprem_release.storePassword keyAlias keystores.onprem_release.keyAlias keyPassword keystores.onprem_release.keyPassword } } else { logger.warn("No onprem keystore found. Falling back to locally generated keystore.") } // Blue release config if (keystores.blue_release != null) { blue_release { storeFile file(keystores.blue_release.storeFile) storePassword keystores.blue_release.storePassword keyAlias keystores.blue_release.keyAlias keyPassword keystores.blue_release.keyPassword } } else { logger.warn("No blue keystore found. Falling back to locally generated keystore.") } // Note: Libre release is signed with HSM, no config here } sourceSets { main { assets.srcDirs = ['assets'] jniLibs.srcDirs = ['libs'] } // Based on Google services none { java.srcDir 'src/google_services_based/java' } store_google { java.srcDir 'src/google_services_based/java' } store_google_work { java.srcDir 'src/google_services_based/java' } store_threema { java.srcDir 'src/google_services_based/java' } libre { assets.srcDirs = ['src/foss_based/assets'] java.srcDir 'src/foss_based/java' } onprem { java.srcDir 'src/google_services_based/java' } onprem_internal { java.srcDir 'src/google_services_based/java' } green { java.srcDir 'src/google_services_based/java' manifest.srcFile 'src/store_google/AndroidManifest.xml' } sandbox_work { java.srcDir 'src/google_services_based/java' res.srcDir 'src/store_google_work/res' manifest.srcFile 'src/store_google_work/AndroidManifest.xml' } blue { java.srcDir 'src/google_services_based/java' res.srcDir 'src/blue/res' } // Based on Huawei services hms { java.srcDir 'src/hms_services_based/java' } hms_work { java.srcDir 'src/hms_services_based/java' res.srcDir 'src/store_google_work/res' } // FOSS, no proprietary services libre { assets.srcDirs = ['src/foss_based/assets'] java.srcDir 'src/foss_based/java' } } buildTypes { debug { debuggable true jniDebuggable false ndk { debugSymbolLevel 'FULL' } enableUnitTestCoverage false enableAndroidTestCoverage false if (keystores['debug'] != null) { signingConfig signingConfigs.debug } } release { debuggable false jniDebuggable false minifyEnabled true shrinkResources 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) { productFlavors.store_google.signingConfig signingConfigs.release productFlavors.store_google_work.signingConfig signingConfigs.release productFlavors.store_threema.signingConfig signingConfigs.release productFlavors.green.signingConfig signingConfigs.release productFlavors.sandbox_work.signingConfig signingConfigs.release productFlavors.none.signingConfig signingConfigs.release } if (keystores['hms_release'] != null) { productFlavors.hms.signingConfig signingConfigs.hms_release productFlavors.hms_work.signingConfig signingConfigs.hms_release } 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 } } // Only build relevant buildType / flavor combinations variantFilter { variant -> def names = variant.flavors*.name if ( variant.buildType.name == "release" && ( names.contains("green") || names.contains("sandbox_work") ) ) { setIgnore(true) } } externalNativeBuild { ndkBuild { path 'jni/Android.mk' } } packagingOptions { jniLibs { // replacement for extractNativeLibs in AndroidManifest useLegacyPackaging = true } resources { excludes += [ '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 { // All the usual Gradle options. testLogging { events "passed", "skipped", "failed", "standardOut", "standardError" outputs.upToDateWhen { false } exceptionFormat = 'full' } jvmArgs = jvmArgs + ['--add-opens=java.base/java.util=ALL-UNNAMED'] jvmArgs = jvmArgs + ['--add-opens=java.base/java.util.stream=ALL-UNNAMED'] jvmArgs = jvmArgs + ['--add-opens=java.base/java.lang=ALL-UNNAMED'] } // 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. returnDefaultValues true } } compileOptions { coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) } } kotlin { jvmToolchain(17) } androidResources { noCompress '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 'TypographyFractions', 'TypographyQuotes', 'RtlHardcoded', 'RtlCompat', 'RtlEnabled' // Set the severity of the given issues to error error '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 'NewApi', 'InlinedApi' // Set the severity of the given issues to ignore (same as disabling the check) ignore 'TypographyQuotes' 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 '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 } } 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 'com.android.tools:desugar_jdk_libs:2.0.4' implementation project(':domain') implementation 'net.zetetic:sqlcipher-android:4.5.7@aar' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'net.sf.opencsv:opencsv:2.3' implementation 'net.lingala.zip4j:zip4j:2.11.5' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3' // commons-io >2.6 requires android 8 implementation 'commons-io:commons-io:2.6' implementation 'org.apache.commons:commons-text:1.10.0' implementation "org.slf4j:slf4j-api:$slf4j_version" implementation 'com.vanniktech:android-image-cropper:4.5.0' implementation 'com.datatheorem.android.trustkit:trustkit:1.1.5' implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'com.googlecode.ez-vcard:ez-vcard:0.11.3' implementation 'com.alexvasilkov:gesture-views:2.8.3' // AndroidX / Jetpack support libraries implementation "androidx.preference:preference-ktx:1.2.1" implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.palette:palette-ktx:1.0.0' implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.work:work-runtime-ktx:2.9.0' implementation 'androidx.fragment:fragment-ktx:1.8.0' implementation 'androidx.activity:activity-ktx:1.9.0' implementation 'androidx.sqlite:sqlite:2.2.2' implementation "androidx.concurrent:concurrent-futures:1.2.0" implementation "androidx.camera:camera-camera2:1.3.4" implementation "androidx.camera:camera-lifecycle:1.3.4" implementation "androidx.camera:camera-view:1.3.4" implementation 'androidx.camera:camera-video:1.3.4' implementation "androidx.media:media:1.7.0" implementation 'androidx.media3:media3-exoplayer:1.3.1' implementation 'androidx.media3:media3-ui:1.3.1' implementation "androidx.media3:media3-session:1.3.1" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.2" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.2" implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2" implementation "androidx.lifecycle:lifecycle-service:2.8.2" implementation "androidx.lifecycle:lifecycle-process:2.8.2" implementation "androidx.lifecycle:lifecycle-common-java8:2.8.2" implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation "androidx.paging:paging-runtime-ktx:3.3.0" implementation "androidx.sharetarget:sharetarget:1.2.0" implementation 'androidx.room:room-runtime:2.6.1' implementation 'androidx.window:window:1.3.0' kapt 'androidx.room:room-compiler:2.6.1' implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1' implementation 'com.google.android.material:material:1.10.0' // last version before switch to tonal system: https://github.com/material-components/material-components-android/releases/tag/1.11.0 implementation 'com.google.zxing:core:3.3.3' // zxing 3.4 crashes on API < 24 implementation 'com.googlecode.libphonenumber:libphonenumber:8.13.39' // make sure to update this in domain's build.gradle as well // webclient dependencies implementation 'org.msgpack:msgpack-core:0.8.24!!' implementation 'com.fasterxml.jackson.core:jackson-core:2.12.5!!' implementation 'com.neovisionaries:nv-websocket-client:2.9' // Backport of Streams and CompletableFuture. Remove once API level 24 is supported. implementation 'net.sourceforge.streamsupport:streamsupport-cfuture:1.7.4' implementation('org.saltyrtc:saltyrtc-client:0.14.2') { exclude group: 'org.json' } implementation 'org.saltyrtc:chunked-dc:1.0.1' implementation 'ch.threema:webrtc-android:123.0.0' implementation('org.saltyrtc:saltyrtc-task-webrtc:0.18.1') { exclude module: 'saltyrtc-client' } // Glide components // Glide 4.15+ does not work on API 21 implementation 'com.github.bumptech.glide:glide:4.16.0' kapt 'com.github.bumptech.glide:compiler:4.16.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' // Kotlin implementation 'androidx.core:core-ktx:1.13.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" // use leak canary in debug builds if (!project.hasProperty("noLeakCanary")) { debugImplementation('com.squareup.leakcanary:leakcanary-android:2.13') } // test dependencies testImplementation "junit:junit:$junit_version" testImplementation(testFixtures(project(":domain"))) // custom test helpers, shared between unit test and android tests testImplementation(project(":test-helpers")) androidTestImplementation(project(":test-helpers")) // use powermock instead of mockito. it support mocking static classes. def mockitoVersion = '2.0.9' testImplementation "org.powermock:powermock-api-mockito2:${mockitoVersion}" testImplementation "org.powermock:powermock-module-junit4-rule-agent:${mockitoVersion}" testImplementation "org.powermock:powermock-module-junit4-rule:${mockitoVersion}" testImplementation "org.powermock:powermock-module-junit4:${mockitoVersion}" // add JSON support to tests without mocking testImplementation 'org.json:json:20220924' testImplementation 'com.tngtech.archunit:archunit-junit4:0.18.0' androidTestImplementation(testFixtures(project(":domain"))) androidTestImplementation 'androidx.test:rules:1.6.0' androidTestImplementation 'tools.fastlane:screengrab:2.1.1', { exclude group: 'androidx.annotation', module: 'annotation' } androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0', { exclude group: 'androidx.annotation', module: 'annotation' } androidTestImplementation 'androidx.test:runner:1.4.0', { exclude group: 'androidx.annotation', module: 'annotation' } androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0', { 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 'androidx.test.espresso:espresso-intents:3.4.0', { exclude group: 'androidx.annotation', module: 'annotation' } androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0' androidTestImplementation 'androidx.test:core-ktx:1.6.0' androidTestImplementation "org.mockito:mockito-core:4.8.1" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_coroutines_version" // Google Play Services and related libraries def googleDependencies = [ // Play services 'com.google.android.gms:play-services-base:18.0.1': [], // Firebase push 'com.google.firebase:firebase-messaging:23.1.2': [ [group: 'com.google.firebase', module: 'firebase-core'], [group: 'com.google.firebase', module: 'firebase-analytics'], [group: 'com.google.firebase', module: 'firebase-measurement-connector'], ], ] googleDependencies.each { def dependency = it.key def excludes = it.value noneImplementation(dependency) { excludes.each { exclude it } } store_googleImplementation(dependency) { excludes.each { exclude it } } store_google_workImplementation(dependency) { excludes.each { exclude it } } store_threemaImplementation(dependency) { excludes.each { exclude it } } onpremImplementation(dependency) { excludes.each { exclude it } } onprem_internalImplementation(dependency) { excludes.each { exclude it } } greenImplementation(dependency) { excludes.each { exclude it } } sandbox_workImplementation(dependency) { excludes.each { exclude it } } blueImplementation(dependency) { excludes.each { exclude it } } } // Google Assistant Voice Action verification library noneImplementation(name: 'libgsaverification-client', ext: 'aar') store_googleImplementation(name: 'libgsaverification-client', ext: 'aar') store_google_workImplementation(name: 'libgsaverification-client', ext: 'aar') onpremImplementation(name: 'libgsaverification-client', ext: 'aar') onprem_internalImplementation(name: 'libgsaverification-client', ext: 'aar') store_threemaImplementation(name: 'libgsaverification-client', ext: 'aar') greenImplementation(name: 'libgsaverification-client', ext: 'aar') sandbox_workImplementation(name: 'libgsaverification-client', ext: 'aar') blueImplementation(name: 'libgsaverification-client', ext: 'aar') // Maplibre (may have transitive dependencies on Google location services) def maplibreDependency = 'org.maplibre.gl:android-sdk:11.0.1' noneImplementation maplibreDependency store_googleImplementation maplibreDependency store_google_workImplementation maplibreDependency store_threemaImplementation maplibreDependency libreImplementation maplibreDependency, { exclude group: 'com.google.android.gms' } onpremImplementation maplibreDependency onprem_internalImplementation maplibreDependency greenImplementation maplibreDependency sandbox_workImplementation maplibreDependency blueImplementation maplibreDependency hmsImplementation maplibreDependency hms_workImplementation maplibreDependency // Huawei related libraries (only for hms* build variants) def huaweiDependencies = [ // HMS push 'com.huawei.hms:push:6.3.0.304': [ // Exclude agconnect dependency, we'll replace it with the vendored version below [group: 'com.huawei.agconnect'], ], ] huaweiDependencies.each { def dependency = it.key def excludes = it.value hmsImplementation(dependency) { excludes.each { exclude it } } hms_workImplementation(dependency) { excludes.each { exclude it } } } hmsImplementation(name: 'agconnect-core-1.9.1.301', ext: 'aar') hms_workImplementation(name: 'agconnect-core-1.9.1.301', ext: 'aar') } tasks.withType(KaptGenerateStubs).configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } 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/**" 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' } } // 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 def reportsDirectory = "$buildDir/reports/androidTests/connected" def screenshotsDirectory = "/sdcard/testfailures/screenshots/" def clearScreenshotsTask = task('clearScreenshots', type: Exec) { executable "${android.getAdbExe().toString()}" args 'shell', 'rm', '-r', screenshotsDirectory } def createScreenshotsDirectoryTask = task('createScreenshotsDirectory', type: Exec, group: 'reporting') { executable "${android.getAdbExe().toString()}" args 'shell', 'mkdir', '-p', screenshotsDirectory } def fetchScreenshotsTask = task('fetchScreenshots', type: Exec, group: 'reporting') { executable "${android.getAdbExe().toString()}" args 'pull', screenshotsDirectory + '.', reportsDirectory finalizedBy { clearScreenshotsTask } dependsOn { createScreenshotsDirectoryTask } doFirst { new File(reportsDirectory).mkdirs() } } tasks.whenTaskAdded { task -> if (task.name == 'connectedDebugAndroidTest') { task.finalizedBy { fetchScreenshotsTask } } }