plugins { id "org.sonarqube" version "3.0" } apply plugin: 'com.android.application' /** * 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"), ] android { // NOTE: When adjusting compileSdkVersion or buildToolsVersion, // make sure to adjust them in `scripts/Dockerfile` as well! compileSdkVersion 29 buildToolsVersion '29.0.3' defaultConfig { minSdkVersion 19 //noinspection OldTargetApi targetSdkVersion 29 vectorDrawables.useSupportLibrary = true applicationId "ch.threema.app" testApplicationId 'ch.threema.app.test' versionCode 655 versionName "4.5-beta1" resValue "string", "version_name_suffix", "" resValue "string", "app_name", "Threema" resValue "string", "uri_scheme", "threema" resValue "string", "action_url", "go.threema.ch" resValue "string", "contact_action_url", "threema.id" // package name used for sync adapter 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" resValue "integer", "max_group_size", "100" resValue "string", "shop_download_filename", "Threema-update.apk" buildConfigField "String", "CHAT_SERVER_PREFIX", "\"g-\"" buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.\"" buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.threema.ch\"" 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()}\"" manifestPlaceholders = [ actionUrl: "go.threema.ch", uriScheme: "threema", contactActionUrl: "threema.id" ] ndk { abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64" } testInstrumentationRunner 'ch.threema.app.ThreemaTestRunner' testInstrumentationRunnerArgument 'notAnnotation', 'ch.threema.app.TestFastlaneOnly,ch.threema.app.DangerousTest' testInstrumentationRunnerArgument 'disableAnalytics', 'true' // https://developer.android.com/training/testing/espresso/setup#analytics } splits { abi { enable true reset() include 'armeabi-v7a', 'x86', "arm64-v8a", "x86_64" exclude 'armeabi', 'mips', 'mips64' universalApk true } } // Assign different version code for each output project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9] android.applicationVariants.all { variant -> variant.outputs.each { output -> output.versionCodeOverride = project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + android.defaultConfig.versionCode } } flavorDimensions "default" productFlavors { none { } store_google { resValue "string", "shop_download_filename", "" } store_threema { } store_google_work { versionName "4.5k-beta1" applicationId "ch.threema.app.work" testApplicationId 'ch.threema.app.work.test' resValue "string", "package_name", applicationId resValue "string", "app_name", "Threema Work" resValue "string", "uri_scheme", "threemawork" resValue "string", "action_url", "work.threema.ch" resValue "string", "contact_action_url", "threema.id" 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" resValue "integer", "max_group_size", "100" resValue "string", "shop_download_filename", "" buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\"" buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.\"" buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.threema.ch\"" buildConfigField "String", "MEDIA_PATH", "\"ThreemaWork\"" buildConfigField "boolean", "CHAT_SERVER_GROUPS", "true" manifestPlaceholders = [ actionUrl: "work.threema.ch", uriScheme: "threemawork", contactActionUrl: "threema.id" ] } sandbox { applicationId "ch.threema.app.sandbox" testApplicationId 'ch.threema.app.sandbox.test' resValue "string", "package_name", applicationId resValue "string", "app_name", "Threema Sandbox" buildConfigField "String", "MEDIA_PATH", "\"ThreemaSandbox\"" 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 }" } sandbox_work { versionName "4.5k-beta1" applicationId "ch.threema.app.sandbox.work" testApplicationId 'ch.threema.app.sandbox.work.test' resValue "string", "package_name", applicationId resValue "string", "app_name", "Threema Sandbox Work" resValue "string", "uri_scheme", "threemawork" resValue "string", "action_url", "work.threema.ch" resValue "string", "contact_action_url", "threema.id" 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" resValue "integer", "max_group_size", "100" resValue "string", "shop_download_filename", "" buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\"" buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.\"" buildConfigField "String", "MEDIA_PATH", "\"ThreemaWorkSandbox\"" buildConfigField "boolean", "CHAT_SERVER_GROUPS", "true" 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 }" manifestPlaceholders = [ actionUrl: "work.threema.ch", uriScheme: "threemawork", contactActionUrl: "threema.id" ] } } 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.") } } sourceSets { main { assets.srcDirs = ['assets'] jniLibs.srcDirs = ['libs'] } sandbox { java { srcDir 'src/none' } } sandbox_work { setRoot 'src/store_google_work' } } buildTypes { debug { debuggable true jniDebuggable false multiDexEnabled true multiDexKeepProguard file('multidex-keep.pro') if (keystores['debug'] != null) { signingConfig signingConfigs.debug } } release { debuggable false jniDebuggable false minifyEnabled true shrinkResources false // Caused inconsistencies between local and CI builds zipAlignEnabled true multiDexEnabled true multiDexKeepProguard file('multidex-keep.pro') proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-project.txt' if (keystores['release'] != null) { signingConfig signingConfigs.release } } } externalNativeBuild { ndkBuild { path 'jni/Android.mk' } } lintOptions { // 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 // set to true to turn off analysis progress reporting by lint // quiet true // if true, stop the gradle build if errors are found abortOnError true // if true, only report errors ignoreWarnings false // if true, emit full/absolute paths to files with errors (true by default) //absolutePaths true // if true, check all issues, including those that are off by default checkAllWarnings true // if true, treat all warnings as errors warningsAsErrors false // turn off checking the given issue id's disable 'TypographyFractions', 'TypographyQuotes' // turn on the given issue id's disable 'RtlHardcoded', 'RtlCompat', 'RtlEnabled' // check *only* the given issue id's // check 'NewApi', 'InlinedApi' // 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 // if true, generate an XML report for use by for example Jenkins xmlReport true // file to write report to (if not specified, defaults to lint-results.xml) xmlOutput file("lint-report.xml") // 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 error error 'Wakelock', 'TextViewEdits', 'ResourceAsColor' // Set the severity of the given issues to warning warning 'MissingTranslation' // Set the severity of the given issues to ignore (same as disabling the check) ignore 'TypographyQuotes' } packagingOptions { exclude 'META-INF/DEPENDENCIES.txt' exclude 'META-INF/LICENSE.txt' exclude 'META-INF/NOTICE.txt' exclude 'META-INF/NOTICE' exclude 'META-INF/LICENSE' exclude 'META-INF/DEPENDENCIES' exclude 'META-INF/notice.txt' exclude 'META-INF/license.txt' exclude 'META-INF/dependencies.txt' exclude 'META-INF/LGPL2.1' // fix https://stackoverflow.com/questions/42739916/aarch64-linux-android-strip-file-missing doNotStrip '*/mips/*.so' doNotStrip '*/mips64/*.so' doNotStrip '*/armeabi/*.so' } 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' } } // 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 { targetCompatibility 1.8 sourceCompatibility 1.8 } aaptOptions { noCompress 'png' } } 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() } // Include all JARs in the "libs" directory implementation fileTree(dir: 'libs', include: ['*.jar']) // play services implementation 'com.google.android.gms:play-services-base:16.1.0' implementation 'com.google.android.gms:play-services-wearable:16.0.1' // do not upgrade to a higher version of firebase-messaging - as we do not want the Firebase Installations API in our app implementation('com.google.firebase:firebase-messaging:20.1.0') { 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' } implementation 'net.zetetic:android-database-sqlcipher:4.4.2' 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.6.4' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0' implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.2.1' // commons-io 2.6 requires android 4.4 implementation 'commons-io:commons-io:2.4' implementation 'org.slf4j:slf4j-api:1.7.25' implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.21' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' implementation 'com.datatheorem.android.trustkit:trustkit:1.1.3' implementation 'com.takisoft.preferencex:preferencex:1.1.0' implementation 'com.takisoft.preferencex:preferencex-datetimepicker:1.1.0' implementation 'com.google.mlkit:image-labeling:17.0.1' implementation 'com.google.android.material:material:1.2.1' implementation 'com.google.android.exoplayer:exoplayer-core:2.12.1' implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.1' implementation 'com.google.protobuf:protobuf-javalite:3.9.1' implementation 'com.google.zxing:core:3.3.3' // zxing 3.4 crashes on kitkat implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.13' implementation "androidx.preference:preference:1.1.1" implementation 'androidx.legacy:legacy-support-v13:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.palette:palette:1.0.0' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.biometric:biometric:1.0.1' implementation "androidx.work:work-runtime:2.4.0" implementation 'androidx.fragment:fragment:1.2.5' implementation 'androidx.activity:activity:1.1.0' implementation 'androidx.sqlite:sqlite:2.1.0' implementation "androidx.concurrent:concurrent-futures:1.1.0" implementation "androidx.camera:camera-camera2:1.0.0-rc01" implementation "androidx.camera:camera-lifecycle:1.0.0-rc01" implementation "androidx.camera:camera-view:1.0.0-alpha20" implementation 'androidx.multidex:multidex:2.0.1' // Lifecycle implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0" implementation "androidx.lifecycle:lifecycle-livedata:2.2.0" implementation "androidx.lifecycle:lifecycle-runtime:2.2.0" implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0" implementation "androidx.lifecycle:lifecycle-service:2.2.0" implementation "androidx.lifecycle:lifecycle-process:2.2.0" implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation "androidx.paging:paging-runtime:2.1.2" implementation 'com.google.android.material:material:1.2.1' implementation 'com.google.android.exoplayer:exoplayer-core:2.12.1' implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.1' implementation 'com.google.protobuf:protobuf-javalite:3.9.1' implementation 'com.google.zxing:core:3.3.3' // zxing 3.4 crashes on kitkat implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.13' implementation "android.arch.lifecycle:common-java8:2.0.0" implementation "android.arch.lifecycle:extensions:2.0.0" implementation "android.arch.paging:runtime:1.0.1" // webclient dependencies implementation 'org.msgpack:msgpack-core:0.8.20' 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.2' // Google Assistant Voice Action verification library implementation(name:'libgsaverification-client', ext:'aar') implementation('org.saltyrtc.client:saltyrtc-client:0.14.1') { exclude group: 'org.json' } implementation 'org.saltyrtc.chunked-dc:chunked-dc:1.0.0' implementation 'ch.threema.webrtc:webrtc-android:84.2.0' implementation('org.saltyrtc.tasks.webrtc:saltyrtc-task-webrtc:0.18.0') { exclude module: 'saltyrtc-client' } // Room components implementation 'androidx.room:room-runtime:2.2.6' annotationProcessor 'androidx.room:room-compiler:2.2.6' androidTestImplementation 'androidx.room:room-testing:2.2.6' // Glide components implementation 'com.github.bumptech.glide:glide:4.11.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' // use leak canary in debug builds debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5' // test dependencies testImplementation 'junit:junit:4.12' // use powermock instead of mockito. it support mocking static classes. def mockitoVersion = '2.0.7' 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:20160212' androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'tools.fastlane:screengrab:2.0.0', { exclude group: 'androidx.annotation', module: 'annotation' } androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0', { exclude group: 'androidx.annotation', module: 'annotation' } androidTestImplementation 'androidx.test:runner:1.1.0', { exclude group: 'androidx.annotation', module: 'annotation' } androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.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' } androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0', { exclude group: 'androidx.annotation', module: 'annotation' } androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' } sonarqube { properties { property "sonar.projectKey", "android-client" property "sonar.projectName", "Threema for Android" property "sonar.sources", "src/main/, ../scripts/, ../scripts-internal/" // Exclusion notes: // - Protobuf code is generated // - Java Client code (including jnacl) is already being checked by SonarQube separately property "sonar.exclusions", "src/main/java/ch/threema/protobuf/**, src/main/java/ch/threema/client/**, src/test/java/ch/threema/client/**, src/main/java/ch/threema/base/**, src/main/java/ch/threema/localcrypto/**, src/test/java/ch/threema/localcrypto/**, src/main/java/com/neilalexander/jnacl/**" property "sonar.tests", "src/test/" property "sonar.sourceEncoding", "UTF-8" property "sonar.verbose", "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 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 } } }