tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }