build.gradle (29630B)
1 plugins { 2 alias(libs.plugins.kotlin.android) 3 alias(libs.plugins.kotlin.compose) 4 alias(libs.plugins.python.envs.plugin) 5 id 'org.mozilla.nimbus-gradle-plugin' 6 } 7 8 apply plugin: 'com.android.application' 9 apply plugin: 'kotlin-parcelize' 10 apply plugin: 'jacoco' 11 12 if (gradle.mozconfig.substs.MOZILLA_OFFICIAL) { 13 apply plugin: 'com.google.android.gms.oss-licenses-plugin' 14 } 15 16 def isAppBundle = gradle.startParameter.taskNames.any { it.toLowerCase().contains("bundle") } 17 18 def versionCodeGradle = "$project.rootDir/tools/gradle/versionCode.gradle" 19 if (findProject(":geckoview") != null) { 20 versionCodeGradle = "$project.rootDir/mobile/android/focus-android/tools/gradle/versionCode.gradle" 21 } 22 apply from: versionCodeGradle 23 24 import com.android.build.api.variant.FilterConfiguration 25 import com.google.android.gms.oss.licenses.plugin.DependencyTask 26 import groovy.json.JsonOutput 27 28 import static org.gradle.api.tasks.testing.TestResult.ResultType 29 30 android { 31 if (project.hasProperty("testBuildType")) { 32 // Allowing to configure the test build type via command line flag (./gradlew -PtestBuildType=beta ..) 33 // in order to run UI tests against other build variants than debug in automation. 34 testBuildType project.property("testBuildType") 35 } 36 37 compileSdk { version = release(config.compileSdkMajorVersion) { minorApiLevel = config.compileSdkMinorVersion } } 38 39 defaultConfig { 40 applicationId "org.mozilla" 41 minSdk config.minSdkVersion 42 targetSdk config.targetSdkVersion 43 versionCode 11 // This versionCode is "frozen" for local builds. For "release" builds we 44 // override this with a generated versionCode at build time. 45 // The versionName is dynamically overridden for all the build variants at build time. 46 versionName Config.generateDebugVersionName() 47 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 48 testInstrumentationRunnerArguments clearPackageData: 'true' 49 if (gradle.mozconfig.substs.MOZ_INCLUDE_SOURCE_INFO) { 50 buildConfigField("String", "VCS_HASH", "\"" + gradle.mozconfig.source_repo.MOZ_SOURCE_STAMP + "\"") 51 } else { 52 buildConfigField("String", "VCS_HASH", "\"\"") 53 } 54 55 vectorDrawables.useSupportLibrary = true 56 } 57 58 bundle { 59 language { 60 // Because we have runtime language selection we will keep all strings and languages 61 // in the base APKs. 62 enableSplit = false 63 } 64 } 65 66 // Exclude native libraries for unsupported architectures from third-party dependencies 67 packagingOptions { 68 jniLibs { 69 excludes += ["**/armeabi/*.so", "**/mips/*.so", "**/mips64/*.so", "**/x86/*.so"] 70 } 71 } 72 73 lint { 74 lintConfig = file("lint.xml") 75 baseline = file("lint-baseline.xml") 76 sarifReport = true 77 sarifOutput = file("../build/reports/lint/lint-report.sarif.json") 78 abortOnError = false 79 } 80 81 // We have a three dimensional build configuration: 82 // BUILD TYPE (debug, release) X PRODUCT FLAVOR (focus, klar) 83 84 buildTypes { 85 release { 86 // We allow disabling optimization by passing `-PdisableOptimization` to gradle. This is used 87 // in automation for UI testing non-debug builds. 88 shrinkResources = !project.hasProperty("disableOptimization") 89 minifyEnabled = !project.hasProperty("disableOptimization") 90 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 91 matchingFallbacks = ['release'] 92 93 // If building locally, just use debug signing. In official automation builds, the 94 // files are signed using a separate service and not within gradle. 95 if (!gradle.mozconfig.substs.MOZ_AUTOMATION) { 96 signingConfig = signingConfigs.debug 97 } 98 99 if (gradle.hasProperty("localProperties.debuggable") || 100 gradle.mozconfig.substs.MOZ_ANDROID_DEBUGGABLE) { 101 println ("All builds will be debuggable") 102 debuggable true 103 } 104 } 105 debug { 106 applicationIdSuffix ".debug" 107 matchingFallbacks = ['debug'] 108 } 109 beta { 110 initWith release 111 applicationIdSuffix ".beta" 112 // This is used when the user selects text in other third-party apps. See https://github.com/mozilla-mobile/focus-android/issues/6478 113 manifestPlaceholders = [textSelectionSearchAction: "@string/text_selection_search_action_focus_beta"] 114 } 115 nightly { 116 initWith release 117 applicationIdSuffix ".nightly" 118 // This is used when the user selects text in other third-party apps. See https://github.com/mozilla-mobile/focus-android/issues/6478 119 manifestPlaceholders = [textSelectionSearchAction: "@string/text_selection_search_action_focus_nightly"] 120 } 121 } 122 testOptions { 123 animationsDisabled = true 124 execution = 'ANDROIDX_TEST_ORCHESTRATOR' 125 testCoverage { 126 jacocoVersion = libs.versions.jacoco.get() 127 } 128 unitTests { 129 includeAndroidResources = true 130 } 131 } 132 133 buildFeatures { 134 compose = true 135 viewBinding = true 136 buildConfig = true 137 } 138 139 flavorDimensions.add("product") 140 141 productFlavors { 142 // In most countries we are Firefox Focus - but in some we need to be Firefox Klar 143 focus { 144 dimension "product" 145 146 applicationIdSuffix ".focus" 147 148 // This is used when the user selects text in other third-party apps. See https://github.com/mozilla-mobile/focus-android/issues/6478 149 manifestPlaceholders = [textSelectionSearchAction: "@string/text_selection_search_action_focus"] 150 } 151 klar { 152 dimension "product" 153 154 applicationIdSuffix ".klar" 155 156 // This is used when the user selects text in other third-party apps. See https://github.com/mozilla-mobile/focus-android/issues/6478 157 manifestPlaceholders = [textSelectionSearchAction: "@string/text_selection_search_action_klar"] 158 } 159 } 160 161 splits { 162 abi { 163 enable = !isAppBundle 164 165 reset() 166 167 include "armeabi-v7a", "arm64-v8a", "x86_64" 168 } 169 } 170 171 sourceSets { 172 test { 173 resources { 174 // Make the default asset folder available as test resource folder. Robolectric seems 175 // to fail to read assets for our setup. With this we can just read the files directly 176 // and do not need to rely on Robolectric. 177 srcDir "${projectDir}/src/main/assets/" 178 } 179 } 180 181 // Release 182 focusRelease.root = 'src/focusRelease' 183 klarRelease.root = 'src/klarRelease' 184 185 // Debug 186 focusDebug.root = 'src/focusDebug' 187 klarDebug.root = 'src/klarDebug' 188 189 // Nightly 190 focusNightly.root = 'src/focusNightly' 191 klarNightly.root = 'src/klarNightly' 192 } 193 194 packaging { 195 if (!isAppBundle) { 196 jniLibs { 197 useLegacyPackaging = true 198 } 199 } 200 } 201 202 namespace = 'org.mozilla.focus' 203 } 204 205 // ------------------------------------------------------------------------------------------------- 206 // Generate Kotlin code for the Focus Glean metrics. 207 // ------------------------------------------------------------------------------------------------- 208 ext { 209 // Enable expiration by major version. 210 gleanExpireByVersion = 1 211 gleanNamespace = "mozilla.telemetry.glean" 212 gleanPythonEnvDir = gradle.mozconfig.substs.GRADLE_GLEAN_PARSER_VENV 213 gleanYamlFiles = [ 214 "${project.projectDir}/metrics.yaml", 215 "${project.projectDir}/legacy_metrics.yaml", 216 "${project.projectDir}/pings.yaml", 217 "${project.projectDir}/legacy_pings.yaml", 218 "${project.projectDir}/tags.yaml", 219 ] 220 } 221 apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" 222 223 nimbus { 224 // The path to the Nimbus feature manifest file 225 manifestFile = "nimbus.fml.yaml" 226 // Map from the variant name to the channel as experimenter and nimbus understand it. 227 // If nimbus's channels were accurately set up well for this project, then this 228 // shouldn't be needed. 229 channels = [ 230 focusDebug: "debug", 231 focusNightly: "nightly", 232 focusBeta: "beta", 233 focusRelease: "release", 234 klarDebug: "debug", 235 klarNightly: "nightly", 236 klarBeta: "beta", 237 klarRelease: "release", 238 ] 239 // This is generated by the FML and should be checked into git. 240 // It will be fetched by Experimenter (the Nimbus experiment website) 241 // and used to inform experiment configuration. 242 experimenterManifest = ".experimenter.yaml" 243 } 244 245 dependencies { 246 implementation project(':components:browser-domains') 247 implementation project(':components:browser-engine-gecko') 248 implementation project(':components:browser-errorpages') 249 implementation project(':components:browser-icons') 250 implementation project(':components:browser-menu') 251 implementation project(':components:browser-state') 252 implementation project(':components:browser-toolbar') 253 implementation project(':components:compose-awesomebar') 254 implementation project(':components:compose-cfr') 255 implementation project(':components:concept-awesomebar') 256 implementation project(':components:concept-engine') 257 implementation project(':components:concept-fetch') 258 implementation project(':components:concept-menu') 259 implementation project(':components:feature-app-links') 260 implementation project(':components:feature-awesomebar') 261 implementation project(':components:feature-contextmenu') 262 implementation project(':components:feature-customtabs') 263 implementation project(':components:feature-downloads') 264 implementation project(':components:feature-findinpage') 265 implementation project(':components:feature-intent') 266 implementation project(':components:feature-media') 267 implementation project(':components:feature-prompts') 268 implementation project(':components:feature-search') 269 implementation project(':components:feature-session') 270 implementation project(':components:feature-sitepermissions') 271 implementation project(':components:feature-tabs') 272 implementation project(':components:feature-toolbar') 273 implementation project(':components:feature-top-sites') 274 implementation project(':components:feature-webcompat') 275 implementation project(':components:feature-webcompat-reporter') 276 implementation project(':components:lib-auth') 277 implementation project(':components:lib-crash') 278 implementation project(':components:lib-crash-sentry') 279 implementation project(':components:lib-publicsuffixlist') 280 implementation project(':components:lib-state') 281 implementation project(":components:service-glean") 282 implementation project(':components:service-location') 283 implementation project(':components:service-nimbus') 284 implementation project(':components:support-ktx') 285 implementation project(':components:support-utils') 286 implementation project(':components:support-remotesettings') 287 implementation project(':components:support-appservices') 288 implementation project(':components:support-license') 289 implementation project(':components:support-locale') 290 implementation project(':components:support-webextensions') 291 implementation project(':components:ui-autocomplete') 292 implementation project(':components:ui-colors') 293 implementation project(':components:ui-icons') 294 implementation project(':components:ui-tabcounter') 295 implementation project(':components:ui-widgets') 296 297 implementation libs.androidx.activity 298 implementation libs.androidx.appcompat 299 implementation libs.androidx.browser 300 implementation libs.androidx.cardview 301 implementation libs.androidx.collection 302 implementation platform(libs.androidx.compose.bom) 303 implementation libs.androidx.compose.foundation 304 implementation libs.androidx.compose.material.icons 305 implementation libs.androidx.compose.material3 306 implementation libs.androidx.compose.runtime.livedata 307 implementation libs.androidx.compose.ui 308 implementation libs.androidx.compose.ui.tooling 309 implementation libs.androidx.constraintlayout 310 implementation libs.androidx.constraintlayout.compose 311 implementation libs.androidx.core.ktx 312 implementation libs.androidx.core.splashscreen 313 implementation libs.androidx.datastore.preferences 314 implementation libs.androidx.fragment 315 implementation libs.androidx.lifecycle.process 316 implementation libs.androidx.lifecycle.viewmodel 317 implementation libs.androidx.palette 318 implementation libs.androidx.preferences 319 implementation libs.androidx.recyclerview 320 implementation libs.androidx.savedstate 321 implementation libs.androidx.transition 322 implementation libs.androidx.work.runtime 323 implementation libs.google.material 324 implementation libs.kotlinx.coroutines 325 implementation libs.mozilla.glean 326 implementation libs.play.review 327 implementation libs.play.review.ktx 328 implementation libs.sentry 329 330 debugImplementation libs.leakcanary 331 332 testImplementation project(':components:lib-fetch-okhttp') 333 testImplementation project(':components:support-test') 334 testImplementation project(':components:support-test-libstate') 335 336 testImplementation libs.androidx.arch.core.testing 337 testImplementation libs.androidx.test.core 338 testImplementation libs.androidx.test.rules 339 testImplementation libs.androidx.test.runner 340 testImplementation libs.androidx.work.testing 341 testImplementation platform(libs.junit.bom) 342 testImplementation libs.junit.jupiter 343 testRuntimeOnly libs.junit.platform.launcher 344 testImplementation libs.kotlinx.coroutines.test 345 testImplementation libs.mockito 346 testImplementation libs.mockwebserver 347 testImplementation libs.mozilla.glean.forUnitTests 348 testImplementation libs.robolectric 349 350 // We need a version constraint due to AGP silently trying to downgrade the version otherwise. 351 // https://issuetracker.google.com/issues/349628366 352 constraints { 353 implementation(libs.androidx.concurrent) 354 } 355 androidTestImplementation libs.androidx.concurrent 356 androidTestImplementation libs.androidx.test.core 357 androidTestImplementation libs.androidx.test.espresso.core 358 androidTestImplementation libs.androidx.test.espresso.idling.resource 359 androidTestImplementation libs.androidx.test.espresso.intents 360 androidTestImplementation libs.androidx.test.espresso.web 361 androidTestImplementation libs.androidx.test.junit 362 androidTestImplementation libs.androidx.test.monitor 363 androidTestImplementation libs.androidx.test.runner 364 androidTestImplementation libs.androidx.test.uiautomator 365 androidTestImplementation libs.mockwebserver 366 androidTestUtil libs.androidx.test.orchestrator 367 368 lintChecks project(':components:tooling-lint') 369 } 370 // ------------------------------------------------------------------------------------------------- 371 // Dynamically set versionCode (See tools/build/versionCode.gradle 372 // ------------------------------------------------------------------------------------------------- 373 374 android.applicationVariants.configureEach { variant -> 375 def buildType = variant.buildType.name 376 377 project.logger.debug("----------------------------------------------") 378 project.logger.debug("Variant name: " + variant.name) 379 project.logger.debug("Application ID: " + [variant.applicationId, variant.buildType.applicationIdSuffix].findAll().join()) 380 project.logger.debug("Build type: " + variant.buildType.name) 381 project.logger.debug("Flavor: " + variant.flavorName) 382 383 if (buildType == "release" || buildType == "nightly" || buildType == "beta") { 384 def baseVersionCode = generatedVersionCode 385 def versionName = buildType == "nightly" ? 386 "${Config.nightlyVersionName(project)}" : 387 "${Config.releaseVersionName(project)}" 388 project.logger.debug("versionName override: $versionName") 389 390 // The Google Play Store does not allow multiple APKs for the same app that all have the 391 // same version code. Therefore we need to have different version codes for our ARM and x86 392 // builds. See https://developer.android.com/studio/publish/versioning 393 394 // Our generated version code now has a length of 9 (See tools/gradle/versionCode.gradle). 395 // Our x86 builds need a higher version code to avoid installing ARM builds on an x86 device 396 // with ARM compatibility mode. 397 398 // AAB builds need a version code that is distinct from any APK builds. Since AAB and APK 399 // builds may run in parallel, AAB and APK version codes might be based on the same 400 // (minute granularity) time of day. To avoid conflicts, we ensure the minute portion 401 // of the version code is even for APKs and odd for AABs. 402 403 variant.outputs.each { output -> 404 def abi = output.getFilter(FilterConfiguration.FilterType.ABI.name()) 405 if (isAppBundle) { 406 abi = "AAB" 407 } 408 409 // We use the same version code generator, that we inherited from Fennec, across all channels - even on 410 // channels that never shipped a Fennec build. 411 412 // ensure baseVersionCode is an even number 413 if (baseVersionCode % 2) { 414 baseVersionCode = baseVersionCode + 1 415 } 416 417 def versionCodeOverride = baseVersionCode 418 419 if (abi == "AAB") { 420 // AAB version code is odd 421 versionCodeOverride = versionCodeOverride + 1 422 } else if (abi == "x86_64") { 423 versionCodeOverride = versionCodeOverride + 6 424 } else if (abi == "x86") { 425 versionCodeOverride = versionCodeOverride + 4 426 } else if (abi == "arm64-v8a") { 427 versionCodeOverride = versionCodeOverride + 2 428 } else if (abi == "armeabi-v7a") { 429 versionCodeOverride = versionCodeOverride + 0 430 } else { 431 throw new RuntimeException("Unknown ABI: " + abi) 432 } 433 project.logger.debug("versionCode for $abi = $versionCodeOverride") 434 435 if (versionName != null) { 436 output.versionNameOverride = versionName 437 } 438 output.versionCodeOverride = versionCodeOverride 439 440 } 441 442 } 443 } 444 445 // ------------------------------------------------------------------------------------------------- 446 // MLS: Read token from local file if it exists (Only release builds) 447 // ------------------------------------------------------------------------------------------------- 448 449 android.applicationVariants.configureEach { 450 project.logger.debug("MLS token: ") 451 try { 452 def token = new File("${rootDir}/.mls_token").text.trim() 453 buildConfigField 'String', 'MLS_TOKEN', '"' + token + '"' 454 project.logger.debug("(Added from .mls_token file)") 455 } catch (FileNotFoundException ignored) { 456 buildConfigField 'String', 'MLS_TOKEN', '""' 457 project.logger.debug("X_X") 458 } 459 } 460 461 // ------------------------------------------------------------------------------------------------- 462 // Sentry: Read token from local file if it exists (Only release builds) 463 // ------------------------------------------------------------------------------------------------- 464 465 android.applicationVariants.configureEach { 466 project.logger.debug("Sentry token: ") 467 try { 468 def token = new File("${rootDir}/.sentry_token").text.trim() 469 buildConfigField 'String', 'SENTRY_TOKEN', '"' + token + '"' 470 project.logger.debug("(Added from .sentry_token file)") 471 } catch (FileNotFoundException ignored) { 472 buildConfigField 'String', 'SENTRY_TOKEN', '""' 473 project.logger.debug("X_X") 474 } 475 } 476 477 // ------------------------------------------------------------------------------------------------- 478 // L10N: Generate list of locales 479 // Focus provides its own (Android independent) locale switcher. That switcher requires a list 480 // of locale codes. We generate that list here to avoid having to manually maintain a list of locales: 481 // ------------------------------------------------------------------------------------------------- 482 483 def getEnabledLocales() { 484 def resDir = file('src/main/res') 485 486 def potentialLanguageDirs = resDir.listFiles(new FilenameFilter() { 487 @Override 488 boolean accept(File dir, String name) { 489 return name.startsWith("values-") 490 } 491 }) 492 493 def langs = potentialLanguageDirs.findAll { 494 // Only select locales where strings.xml exists 495 // Some locales might only contain e.g. sumo URLS in urls.xml, and should be skipped (see es vs es-ES/es-MX/etc) 496 return file(new File(it, "strings.xml")).exists() 497 } .collect { 498 // And reduce down to actual values-* names 499 return it.name 500 } .collect { 501 return it.substring("values-".length()) 502 } .collect { 503 if (it.length() > 3 && it.contains("-r")) { 504 // Android resource dirs add an "r" prefix to the region - we need to strip that for java usage 505 // Add 1 to have the index of the r, without the dash 506 def regionPrefixPosition = it.indexOf("-r") + 1 507 508 return it.substring(0, regionPrefixPosition) + it.substring(regionPrefixPosition + 1) 509 } else { 510 return it 511 } 512 }.collect { 513 return '"' + it + '"' 514 } 515 516 // en-US is the default language (in "values") and therefore needs to be added separately 517 langs << "\"en-US\"" 518 519 return langs.sort { it } 520 } 521 522 // ------------------------------------------------------------------------------------------------- 523 // Nimbus: Read endpoint from local.properties of a local file if it exists 524 // ------------------------------------------------------------------------------------------------- 525 526 project.logger.debug("Nimbus endpoint: ") 527 android.applicationVariants.configureEach { variant -> 528 def variantName = variant.getName() 529 530 if (!variantName.contains("Debug")) { 531 try { 532 def url = new File("${rootDir}/.nimbus").text.trim() 533 buildConfigField 'String', 'NIMBUS_ENDPOINT', '"' + url + '"' 534 project.logger.debug("(Added from .nimbus file)") 535 } catch (FileNotFoundException ignored) { 536 buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null' 537 project.logger.debug("X_X") 538 } 539 } else if (gradle.hasProperty("localProperties.nimbus.remote-settings.url")) { 540 def url = gradle.getProperty("localProperties.nimbus.remote-settings.url") 541 buildConfigField 'String', 'NIMBUS_ENDPOINT', '"' + url + '"' 542 project.logger.debug("(Added from local.properties file)") 543 } else { 544 buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null' 545 project.logger.debug("--") 546 } 547 } 548 549 def generatedLocaleListDir = file('src/main/java/org/mozilla/focus/generated') 550 def generatedLocaleListFilename = 'LocalesList.kt' 551 552 def enabledLocales = project.provider { getEnabledLocales() } 553 554 tasks.register('generateLocaleList') { 555 def localeList = file(new File(generatedLocaleListDir, generatedLocaleListFilename)) 556 557 inputs.property("enabledLocales", enabledLocales) 558 outputs.file(localeList) 559 560 doLast { 561 generatedLocaleListDir.mkdir() 562 localeList.delete() 563 localeList.createNewFile() 564 localeList << "/* This Source Code Form is subject to the terms of the Mozilla Public" << "\n" 565 localeList << " * License, v. 2.0. If a copy of the MPL was not distributed with this" << "\n" 566 localeList << " * file, You can obtain one at http://mozilla.org/MPL/2.0/. */" << "\n" << "\n" 567 localeList << "package org.mozilla.focus.generated" << "\n" << "\n" 568 localeList << "import java.util.Collections" << "\n" 569 localeList << "\n" 570 localeList << "/**" 571 localeList << "\n" 572 localeList << " * Provides a list of bundled locales based on the language files in the res folder." 573 localeList << "\n" 574 localeList << " */" 575 localeList << "\n" 576 localeList << "object LocalesList {" << "\n" 577 localeList << " " << "val BUNDLED_LOCALES: List<String> = Collections.unmodifiableList(" 578 localeList << "\n" 579 localeList << " " << "listOf(" 580 localeList << "\n" 581 localeList << " " 582 localeList << enabledLocales.get().join(",\n" + " ") 583 localeList << ",\n" 584 localeList << " )," << "\n" 585 localeList << " )" << "\n" 586 localeList << "}" << "\n" 587 } 588 } 589 590 tasks.configureEach { task -> 591 if (name.contains("compile")) { 592 task.dependsOn generateLocaleList 593 } 594 } 595 596 clean.doLast { 597 generatedLocaleListDir.deleteDir() 598 } 599 600 if (project.hasProperty("coverage")) { 601 tasks.withType(Test).configureEach { 602 jacoco.includeNoLocationClasses = true 603 jacoco.excludes = ['jdk.internal.*'] 604 } 605 606 android.applicationVariants.configureEach { variant -> 607 tasks.register("jacoco${variant.name.capitalize()}TestReport", JacocoReport) { 608 609 dependsOn(["test${variant.name.capitalize()}UnitTest"]) 610 reports { 611 html.required = true 612 xml.required = true 613 } 614 615 def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', 616 '**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*'] 617 def kotlinTree = fileTree(dir: "$project.layout.buildDirectory/tmp/kotlin-classes/${variant.name}", excludes: fileFilter) 618 def javaTree = fileTree(dir: "$project.layout.buildDirectory/intermediates/classes/${variant.flavorName}/${variant.buildType.name}", 619 excludes: fileFilter) 620 def mainSrc = "$project.projectDir/src/main/java" 621 sourceDirectories.setFrom(files([mainSrc])) 622 classDirectories.setFrom(files([kotlinTree, javaTree])) 623 executionData.setFrom(fileTree(dir: project.layout.buildDirectory, includes: [ 624 "jacoco/test${variant.name.capitalize()}UnitTest.exec", 'outputs/code-coverage/connected/*coverage.ec' 625 ])) 626 } 627 } 628 629 android { 630 buildTypes { 631 debug { 632 testCoverageEnabled = true 633 applicationIdSuffix ".coverage" 634 } 635 } 636 } 637 } 638 639 // ------------------------------------------------------------------------------------------------- 640 // Task for printing APK information for the requested variant 641 // Taskgraph Usage: "./gradlew printVariants 642 // ------------------------------------------------------------------------------------------------- 643 tasks.register('printVariants') { 644 def variants = project.provider { android.applicationVariants.collect { variant -> [ 645 apks: variant.outputs.collect { output -> [ 646 abi: output.getFilter(FilterConfiguration.FilterType.ABI.name()), 647 fileName: output.outputFile.name 648 ]}, 649 build_type: variant.buildType.name, 650 name: variant.name, 651 ]}} 652 doLast { 653 // AndroidTest is a special case not included above 654 variants.get().add([ 655 apks: [[ 656 abi: 'noarch', 657 fileName: 'app-debug-androidTest.apk', 658 ]], 659 build_type: 'androidTest', 660 name: 'androidTest', 661 ]) 662 println 'variants: ' + JsonOutput.toJson(variants.get()) 663 } 664 } 665 666 // Fix for Gradle 9 strictness: Ensure CleanUp runs before DependencyTask 667 tasks.withType(DependencyTask).configureEach { task -> 668 // The OSS plugin creates a "LicensesCleanUp" task for every "DependencyTask" 669 // e.g., nightlyOssDependencyTask -> nightlyOssLicensesCleanUp 670 def cleanUpTaskName = task.name.replace("DependencyTask", "LicensesCleanUp") 671 672 // make the dependency task depend on its cleanup task. 673 task.dependsOn(tasks.named(cleanUpTaskName)) 674 } 675 676 afterEvaluate { 677 678 // Format test output. Copied from Fenix, which was ported from AC #2401 679 tasks.withType(Test).configureEach { 680 systemProperty "robolectric.logging", "stdout" 681 systemProperty "logging.test-mode", "true" 682 683 testLogging.events = [] 684 685 beforeSuite { descriptor -> 686 if (descriptor.getClassName() != null) { 687 println("\nSUITE: " + descriptor.getClassName()) 688 } 689 } 690 691 beforeTest { descriptor -> 692 println(" TEST: " + descriptor.getName()) 693 } 694 695 onOutput { descriptor, event -> 696 it.logger.lifecycle(" " + event.message.trim()) 697 } 698 699 afterTest { descriptor, result -> 700 switch (result.getResultType()) { 701 case ResultType.SUCCESS: 702 println(" SUCCESS") 703 break 704 705 case ResultType.FAILURE: 706 def testId = descriptor.getClassName() + "." + descriptor.getName() 707 println(" TEST-UNEXPECTED-FAIL | " + testId + " | " + result.getException()) 708 break 709 710 case ResultType.SKIPPED: 711 println(" SKIPPED") 712 break 713 } 714 it.logger.lifecycle("") 715 } 716 } 717 }