import com.android.build.api.variant.FilterConfiguration import com.google.android.gms.oss.licenses.plugin.DependencyTask import groovy.json.JsonOutput import java.time.format.DateTimeFormatter import static org.mozilla.conventions.BuildConfigKt.getBuildId import static org.gradle.api.tasks.testing.TestResult.ResultType buildscript { repositories { gradle.configureMavenRepositories(delegate) } dependencies { classpath libs.kotlin.serialization } } plugins { alias(libs.plugins.dependency.analysis) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.python.envs.plugin) alias(libs.plugins.protobuf.plugin) id 'ApkSizePlugin' id 'org.mozilla.conventions.zip-test-reports' id 'org.mozilla.nimbus-gradle-plugin' } apply plugin: 'com.android.application' apply plugin: 'kotlin-parcelize' apply plugin: 'jacoco' apply plugin: 'androidx.navigation.safeargs.kotlin' apply plugin: 'kotlinx-serialization' apply plugin: 'com.google.devtools.ksp' if (gradle.mozconfig.substs.MOZILLA_OFFICIAL) { apply plugin: 'com.google.android.gms.oss-licenses-plugin' } apply from: 'benchmark.gradle' ext.getAbi = { output -> // Assume that if the ABI isn't set, it's an app bundle or universal APK output.getFilter(FilterConfiguration.FilterType.ABI.name()) ?: "universal" } def isAppBundle = gradle.startParameter.taskNames.any { it.toLowerCase().contains("bundle") } android { project.maybeConfigForJetpackBenchmark(it) if (project.hasProperty("testBuildType")) { // Allowing to configure the test build type via command line flag (./gradlew -PtestBuildType=beta ..) // in order to run UI tests against other build variants than debug in automation. testBuildType project.property("testBuildType") } compileSdk { version = release(config.compileSdkMajorVersion) { minorApiLevel = config.compileSdkMinorVersion } } defaultConfig { applicationId "org.mozilla" minSdk config.minSdkVersion targetSdk config.targetSdkVersion versionCode 1 versionName Config.generateDebugVersionName() vectorDrawables.useSupportLibrary = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments "detect-leaks": 'true' resValue "bool", "IS_DEBUG", "false" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false" // Blank in debug builds so that tests are deterministic. buildConfigField "String", "VCS_HASH", "\"\"" // This should be the "public" base URL of AMO. buildConfigField "String", "AMO_BASE_URL", "\"https://addons.mozilla.org\"" buildConfigField "String", "AMO_COLLECTION_NAME", "\"Extensions-for-Android\"" buildConfigField "String", "AMO_COLLECTION_USER", "\"mozilla\"" // This should be the base URL used to call the AMO API. buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\"" def deepLinkSchemeValue = "fenix-dev" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" manifestPlaceholders = [ "deepLinkScheme": deepLinkSchemeValue ] buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", getSupportedLocales() buildConfigField "boolean", "IS_BENCHMARK_BUILD", "false" } def releaseTemplate = { // We allow disabling optimization by passing `-PdisableOptimization` to gradle. This is used // in automation for UI testing non-debug builds. shrinkResources = !project.hasProperty("disableOptimization") minifyEnabled = !project.hasProperty("disableOptimization") proguardFiles = [getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'] matchingFallbacks = ['release'] // Use on the "release" build type in dependencies (AARs) // Present in release builds. if (gradle.mozconfig.substs.MOZ_INCLUDE_SOURCE_INFO) { buildConfigField("String", "VCS_HASH", "\"" + gradle.mozconfig.source_repo.MOZ_SOURCE_STAMP + "\"") } // If building locally, just use debug signing. In official automation builds, the // files are signed using a separate service and not within gradle. // Also skip during buildconfig linting, and skip if the `disableDebugSigning` Gradle property is set. if (!project.hasProperty("disableDebugSigning") && !gradle.hasProperty("localProperties.disableDebugSigning") && !gradle.mozconfig.substs.MOZ_AUTOMATION && System.getenv("MOZ_BUILD_CONFIG_LINT") != "1") { signingConfig = signingConfigs.debug } if (gradle.hasProperty("localProperties.debuggable") || gradle.mozconfig.substs.MOZ_ANDROID_DEBUGGABLE) { debuggable true } } buildTypes { debug { shrinkResources = false minifyEnabled = false applicationIdSuffix ".fenix.debug" resValue "bool", "IS_DEBUG", "true" pseudoLocalesEnabled = true } nightly releaseTemplate >> { applicationIdSuffix ".fenix" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" def deepLinkSchemeValue = "fenix-nightly" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" manifestPlaceholders.putAll([ "deepLinkScheme": deepLinkSchemeValue ]) } beta releaseTemplate >> { buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" applicationIdSuffix ".firefox_beta" def deepLinkSchemeValue = "fenix-beta" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" manifestPlaceholders.putAll([ // This release type is meant to replace Firefox (Beta channel) and therefore needs to inherit // its sharedUserId for all eternity. See: // https://searchfox.org/mozilla-esr68/search?q=moz_android_shared_id&case=false®exp=false&path= // Shipping an app update without sharedUserId can have // fatal consequences. For example see: // - https://issuetracker.google.com/issues/36924841 // - https://issuetracker.google.com/issues/36905922 "sharedUserId": "org.mozilla.firefox.sharedID", "deepLinkScheme": deepLinkSchemeValue, ]) } release releaseTemplate >> { buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" applicationIdSuffix ".firefox" def deepLinkSchemeValue = "fenix" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" manifestPlaceholders.putAll([ // This release type is meant to replace Firefox (Release channel) and therefore needs to inherit // its sharedUserId for all eternity. See: // https://searchfox.org/mozilla-esr68/search?q=moz_android_shared_id&case=false®exp=false&path= // Shipping an app update without sharedUserId can have // fatal consequences. For example see: // - https://issuetracker.google.com/issues/36924841 // - https://issuetracker.google.com/issues/36905922 "sharedUserId": "org.mozilla.firefox.sharedID", "deepLinkScheme": deepLinkSchemeValue, ]) } benchmark releaseTemplate >> { initWith buildTypes.nightly shrinkResources = false minifyEnabled = false applicationIdSuffix ".fenix" signingConfig = signingConfigs.debug debuggable false buildConfigField "boolean", "IS_BENCHMARK_BUILD", "true" } } buildFeatures { viewBinding = true buildConfig = true } androidResources { // All JavaScript code used internally by GeckoView is packaged in a // file called omni.ja. If this file is compressed in the APK, // GeckoView must uncompress it before it can do anything else which // causes a significant delay on startup. noCompress 'ja' // manifest.template.json is converted to manifest.json at build time. // No need to package the template in the APK. ignoreAssetsPattern = "manifest.template.json" } testOptions { animationsDisabled = true execution = 'ANDROIDX_TEST_ORCHESTRATOR' testCoverage { jacocoVersion = libs.versions.jacoco.get() } unitTests { all { // We keep running into memory issues when running our tests. With this config we // reserve more memory and also create a new process after every 80 test classes. This // is a band-aid solution and eventually we should try to find and fix the leaks // instead. :) forkEvery = 80 maxHeapSize = "3072m" minHeapSize = "1024m" } includeAndroidResources = true returnDefaultValues = true } } sourceSets { androidTest { resources.srcDirs += ['src/androidTest/resources'] } if (project.hasProperty('baselineProfilePath')) { main { baselineProfiles.srcDirs(project.property('baselineProfilePath')) } } } splits { abi { enable = !isAppBundle reset() // As gradle is unable to pick the right apk to install when multiple apks are generated // while running benchmark tests or generating baseline profiles. To circumvent this, // this flag is passed to make sure only one apk is generated so gradle can pick that one. if (project.hasProperty("benchmarkTest")) { include "arm64-v8a" } else { include "armeabi-v7a", "arm64-v8a", "x86_64" if (gradle.mozconfig.substs.MOZILLA_OFFICIAL || System.getenv("MOZ_BUILD_CONFIG_LINT") == "1") { universalApk true } } } } bundle { language { // Because we have runtime language selection we will keep all strings and languages // in the base APKs. enableSplit = false } } // Exclude native libraries for unsupported architectures from third-party dependencies packagingOptions { jniLibs { excludes += ["**/armeabi/*.so", "**/mips/*.so", "**/mips64/*.so", "**/x86/*.so"] } } lint { lintConfig = file("lint.xml") baseline = file("lint-baseline.xml") sarifReport = true sarifOutput = file("../build/reports/lint/lint-report.sarif.json") abortOnError = false } packaging { if (!isAppBundle) { jniLibs { useLegacyPackaging = true } } resources { excludes += ["META-INF/LICENSE.md", "META-INF/LICENSE-notice.md"] } } buildFeatures { compose = true } namespace = 'org.mozilla.fenix' } android.applicationVariants.configureEach { variant -> // ------------------------------------------------------------------------------------------------- // Generate version codes for builds // ------------------------------------------------------------------------------------------------- def isDebug = variant.buildType.resValues['bool/IS_DEBUG']?.value ?: false def useReleaseVersioning = variant.buildType.buildConfigFields['USE_RELEASE_VERSIONING']?.value ?: false project.logger.debug("----------------------------------------------") project.logger.debug("Variant name: " + variant.name) project.logger.debug("Application ID: " + [variant.applicationId, variant.buildType.applicationIdSuffix].findAll().join()) project.logger.debug("Build type: " + variant.buildType.name) project.logger.debug("Flavor: " + variant.flavorName) project.logger.debug("Telemetry enabled: " + !isDebug) if (useReleaseVersioning) { // The Google Play Store does not allow multiple APKs for the same app that all have the // same version code. Therefore we need to have different version codes for our ARM and x86 // builds. def versionName = variant.buildType.name == 'nightly' ? "${Config.nightlyVersionName(project)}" : "${Config.releaseVersionName(project)}" project.logger.debug("versionName override: $versionName") variant.outputs.each { output -> // We use the same version code generator, that we inherited from Fennec, across all channels - even on // channels that never shipped a Fennec build. def abi = getAbi(output) def versionCodeOverride = Config.generateFennecVersionCode(abi) project.logger.debug("versionCode for $abi = $versionCodeOverride") if (versionName != null) { output.versionNameOverride = versionName } output.versionCodeOverride = versionCodeOverride } } else if (gradle.hasProperty("localProperties.branchBuild.fenix.version")) { def versionName = gradle.getProperty("localProperties.branchBuild.fenix.version") project.logger.debug("versionName override: $versionName") variant.outputs.each { output -> output.versionNameOverride = versionName } } // ------------------------------------------------------------------------------------------------- // BuildConfig: Set variables for Sentry, Crash Reporting, and Telemetry // ------------------------------------------------------------------------------------------------- buildConfigField 'String', 'SENTRY_TOKEN', 'null' if (!isDebug) { buildConfigField 'boolean', 'CRASH_REPORTING', 'true' // Reading sentry token from local file (if it exists). In a release task on taskcluster it will be available. try { def token = new File("${rootDir}/.sentry_token").text.trim() buildConfigField 'String', 'SENTRY_TOKEN', '"' + token + '"' } catch (FileNotFoundException ignored) {} } else { buildConfigField 'boolean', 'CRASH_REPORTING', 'false' } if (!isDebug) { buildConfigField 'boolean', 'TELEMETRY', 'true' } else { buildConfigField 'boolean', 'TELEMETRY', 'false' } // Setting buildDate with every build ID changes the generated BuildConfig, which slows down the // build. Only do this for non-debug builds, to speed-up builds produced during local development. if (isDebug) { buildConfigField 'String', 'BUILD_DATE', '"debug build"' } else { def buildDate = LocalDateTime.parse(getBuildId(gradle.mozconfig.topobjdir), DateTimeFormatter.ofPattern("yyyyMMddHHmmss")).toString() buildConfigField 'String', 'BUILD_DATE', '"' + buildDate + '"' } // ------------------------------------------------------------------------------------------------- // Adjust: Read token from local file if it exists (Only release builds) // ------------------------------------------------------------------------------------------------- project.logger.debug("Adjust token: ") if (!isDebug) { try { def token = new File("${rootDir}/.adjust_token").text.trim() buildConfigField 'String', 'ADJUST_TOKEN', '"' + token + '"' project.logger.debug("(Added from .adjust_token file)") } catch (FileNotFoundException ignored) { buildConfigField 'String', 'ADJUST_TOKEN', 'null' project.logger.debug("X_X") } } else { buildConfigField 'String', 'ADJUST_TOKEN', 'null' project.logger.debug("--") } // ------------------------------------------------------------------------------------------------- // MLS: Read token from local file if it exists // ------------------------------------------------------------------------------------------------- project.logger.debug("MLS token: ") try { def token = new File("${rootDir}/.mls_token").text.trim() buildConfigField 'String', 'MLS_TOKEN', '"' + token + '"' project.logger.debug("(Added from .mls_token file)") } catch (FileNotFoundException ignored) { buildConfigField 'String', 'MLS_TOKEN', '""' project.logger.debug("X_X") } // ------------------------------------------------------------------------------------------------- // Google Play Integrity: Read token from local file if it exists // ------------------------------------------------------------------------------------------------- project.logger.debug("Google Play Integrity: ") try { def token = new File("${rootDir}/.gps_integrity_token").text.trim() buildConfigField 'String', 'GPS_INTEGRITY_TOKEN', '"' + token + '"' project.logger.debug("(Added from .gps_integrity_token file)") } catch (FileNotFoundException ignored) { buildConfigField 'String', 'GPS_INTEGRITY_TOKEN', '""' project.logger.debug("X_X") } // ------------------------------------------------------------------------------------------------- // Nimbus: Read endpoint from local.properties of a local file if it exists // ------------------------------------------------------------------------------------------------- project.logger.debug("Nimbus endpoint: ") if (!isDebug) { try { def url = new File("${rootDir}/.nimbus").text.trim() buildConfigField 'String', 'NIMBUS_ENDPOINT', '"' + url + '"' project.logger.debug("(Added from .nimbus file)") } catch (FileNotFoundException ignored) { buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null' project.logger.debug("X_X") } } else if (gradle.hasProperty("localProperties.nimbus.remote-settings.url")) { def url=gradle.getProperty("localProperties.nimbus.remote-settings.url") buildConfigField 'String', 'NIMBUS_ENDPOINT', '"' + url + '"' project.logger.debug("(Added from local.properties file)") } else { buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null' project.logger.debug("--") } // ------------------------------------------------------------------------------------------------- // Glean: Read custom server URL from local.properties of a local file if it exists // ------------------------------------------------------------------------------------------------- project.logger.debug("Glean custom server URL: ") if (gradle.hasProperty("localProperties.glean.custom.server.url")) { def url=gradle.getProperty("localProperties.glean.custom.server.url") buildConfigField 'String', 'GLEAN_CUSTOM_URL', url project.logger.debug("(Added from local.properties file)") } else { buildConfigField 'String', 'GLEAN_CUSTOM_URL', 'null' project.logger.debug("--") } // ------------------------------------------------------------------------------------------------- // BuildConfig: Set flag for official builds; similar to MOZILLA_OFFICIAL in mozilla-central. // ------------------------------------------------------------------------------------------------- if (project.hasProperty("official") || gradle.hasProperty("localProperties.official")) { buildConfigField 'Boolean', 'MOZILLA_OFFICIAL', 'true' } else { buildConfigField 'Boolean', 'MOZILLA_OFFICIAL', 'false' } // ------------------------------------------------------------------------------------------------- // BuildConfig: Set remote wallpaper URL using local file if it exists // ------------------------------------------------------------------------------------------------- project.logger.debug("Wallpaper URL: ") try { def token = new File("${rootDir}/.wallpaper_url").text.trim() buildConfigField 'String', 'WALLPAPER_URL', '"' + token + '"' project.logger.debug("(Added from .wallpaper_url file)") } catch (FileNotFoundException ignored) { buildConfigField 'String', 'WALLPAPER_URL', '""' project.logger.debug("--") } // ------------------------------------------------------------------------------------------------- // BuildConfig: Set flag to disable LeakCanary in debug (on CI builds) // ------------------------------------------------------------------------------------------------- if (isDebug) { if (project.hasProperty("disableLeakCanary") || gradle.hasProperty("localProperties.disableLeakCanary")) { buildConfigField "boolean", "LEAKCANARY", "false" project.logger.debug("LeakCanary enabled in debug: false") } else { buildConfigField "boolean", "LEAKCANARY", "true" project.logger.debug("LeakCanary enabled in debug: true") } } else { buildConfigField "boolean", "LEAKCANARY", "false" } } // Generate Kotlin code for the Fenix Glean metrics. ext { // Enable expiration by major version. gleanExpireByVersion = Config.majorVersion(project) gleanNamespace = "mozilla.telemetry.glean" gleanPythonEnvDir = gradle.mozconfig.substs.GRADLE_GLEAN_PARSER_VENV } apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" nimbus { // The path to the Nimbus feature manifest file manifestFile = "nimbus.fml.yaml" // The fully qualified class name for the generated features. // Map from the variant name to the channel as experimenter and nimbus understand it. // If nimbus's channels were accurately set up well for this project, then this // shouldn't be needed. channels = [ debug: "developer", nightly: "nightly", beta: "beta", release: "release", benchmark: "nightly", ] // This is generated by the FML and should be checked into git. // It will be fetched by Experimenter (the Nimbus experiment website) // and used to inform experiment configuration. experimenterManifest = ".experimenter.yaml" applicationServicesDir = gradle.hasProperty('localProperties.autoPublish.application-services.dir') ? gradle.getProperty('localProperties.autoPublish.application-services.dir') : null } dependencies { implementation project('longfox') implementation project(':components:browser-engine-gecko') implementation project(':components:compose-awesomebar') implementation project(':components:compose-base') implementation project(':components:compose-browser-toolbar') implementation project(':components:compose-cfr') implementation project(':components:concept-awesomebar') implementation project(':components:concept-base') implementation project(':components:concept-engine') implementation project(':components:concept-menu') implementation project(':components:concept-push') implementation project(':components:concept-storage') implementation project(':components:concept-sync') implementation project(':components:concept-tabstray') implementation project(':components:concept-toolbar') implementation project(':components:browser-domains') implementation project(':components:browser-icons') implementation project(':components:browser-menu') implementation project(':components:browser-menu2') implementation project(':components:browser-session-storage') implementation project(':components:browser-state') implementation project(':components:browser-storage-sync') implementation project(':components:browser-tabstray') implementation project(':components:browser-thumbnails') implementation project(':components:browser-toolbar') implementation project(':components:feature-accounts') implementation project(':components:feature-accounts-push') implementation project(':components:feature-addons') implementation project(':components:feature-app-links') implementation project(':components:feature-autofill') implementation project(':components:feature-awesomebar') implementation project(':components:feature-contextmenu') implementation project(':components:feature-customtabs') implementation project(':components:feature-downloads') implementation project(':components:feature-findinpage') implementation project(':components:feature-fxsuggest') implementation project(':components:feature-intent') implementation project(':components:feature-logins') implementation project(':components:feature-media') implementation project(':components:feature-privatemode') implementation project(':components:feature-prompts') implementation project(':components:feature-protection-dashboard') implementation project(':components:feature-push') implementation project(':components:feature-pwa') implementation project(':components:feature-qr') implementation project(':components:feature-readerview') implementation project(':components:feature-recentlyclosed') implementation project(':components:feature-search') implementation project(':components:feature-session') implementation project(':components:feature-share') implementation project(':components:feature-sitepermissions') implementation project(':components:feature-summarize') implementation project(':components:feature-syncedtabs') implementation project(':components:feature-toolbar') implementation project(':components:feature-tab-collections') implementation project(':components:feature-tabs') implementation project(':components:feature-top-sites') implementation project(':components:feature-webauthn') implementation project(':components:feature-webcompat') implementation project(':components:feature-webcompat-reporter') implementation project(':components:feature-webnotifications') implementation project(':components:service-digitalassetlinks') implementation project(':components:service-firefox-accounts') implementation project(":components:service-firefox-relay") implementation project(':components:service-glean') implementation project(':components:service-location') implementation project(':components:service-mars') implementation project(':components:service-nimbus') implementation project(':components:service-pocket') implementation project(':components:service-sync-autofill') implementation project(':components:service-sync-logins') implementation project(':components:support-appservices') implementation project(':components:support-base') implementation project(':components:support-images') implementation project(':components:support-ktx') implementation project(':components:support-license') implementation project(':components:support-locale') implementation project(':components:support-remotesettings') implementation project(':components:support-utils') implementation project(':components:support-webextensions') implementation project(':components:lib-publicsuffixlist') implementation project(':components:ui-icons') implementation project(':components:ui-colors') implementation project(':components:ui-tabcounter') implementation project(':components:ui-widgets') implementation project(':components:lib-crash') implementation project(':components:lib-crash-sentry') implementation project(':components:lib-dataprotect') implementation project(':components:lib-push-firebase') implementation project(':components:lib-state') implementation project(':components:lib-accelerometer-sensormanager') implementation project(':components:lib-integrity-googleplay') implementation project(':components:lib-llm-mlpa') implementation project(':components:lib-shake') implementation project(':components:lib-ai-controls') implementation ComponentsDependencies.mozilla_appservices_syncmanager implementation libs.accompanist.drawablepainter implementation libs.adjust implementation libs.androidx.activity implementation libs.androidx.activity.ktx implementation libs.androidx.annotation implementation libs.androidx.appcompat implementation libs.androidx.biometric implementation platform(libs.androidx.compose.bom) implementation libs.androidx.compose.animation implementation libs.androidx.compose.foundation implementation libs.androidx.compose.material.icons implementation libs.androidx.compose.material3 implementation libs.androidx.compose.material3.adaptive implementation libs.androidx.compose.material3.adaptivelayout implementation libs.androidx.compose.material3.adaptivenavigation implementation libs.androidx.compose.material3.adaptivenavigationsuite implementation libs.androidx.compose.material3.windowsizeclass implementation libs.androidx.compose.ui implementation libs.androidx.compose.ui.tooling.preview implementation libs.androidx.constraintlayout implementation libs.androidx.coordinatorlayout implementation libs.androidx.core implementation libs.androidx.core.ktx implementation libs.androidx.core.splashscreen implementation libs.androidx.datastore implementation libs.androidx.datastore.preferences implementation libs.androidx.emoji2 implementation libs.androidx.fragment implementation libs.androidx.fragment.compose implementation libs.androidx.lifecycle.common implementation libs.androidx.lifecycle.livedata implementation libs.androidx.lifecycle.process implementation libs.androidx.lifecycle.runtime implementation libs.androidx.lifecycle.service implementation libs.androidx.lifecycle.viewmodel implementation libs.androidx.navigation.compose implementation libs.androidx.navigation.fragment implementation libs.androidx.navigation.ui implementation libs.androidx.navigation3.runtime implementation libs.androidx.navigation3.ui implementation libs.androidx.paging implementation libs.androidx.preferences implementation libs.androidx.profileinstaller implementation libs.androidx.recyclerview implementation libs.androidx.swiperefreshlayout implementation libs.androidx.transition implementation libs.androidx.viewpager2 implementation libs.androidx.work.runtime implementation libs.google.material implementation libs.installreferrer implementation libs.kotlinx.coroutines implementation libs.kotlinx.serialization.json implementation libs.mozilla.glean implementation libs.play.review implementation libs.play.review.ktx implementation libs.play.services.ads.id implementation libs.protobuf.javalite implementation libs.sentry implementation libs.androidx.documentfile implementation libs.zxing implementation libs.androidx.room.runtime ksp libs.androidx.room.compiler debugImplementation libs.androidx.compose.ui.tooling debugImplementation libs.leakcanary // Exclude Mockito to prevent classpath conflicts with MockK in tests. testImplementation (project(':components:support-test')) { exclude group: 'org.mockito' } testImplementation project(':components:support-test-fakes') testImplementation project(':components:support-test-libstate') testImplementation testFixtures(project(':components:support-utils')) testImplementation ComponentsDependencies.mozilla_appservices_full_megazord_libsForTests testImplementation libs.androidx.compose.ui.test testImplementation libs.androidx.test.core testImplementation libs.androidx.test.junit testImplementation libs.androidx.work.testing testImplementation libs.kotlin.test testImplementation libs.kotlinx.coroutines.test testImplementation libs.mockk testImplementation libs.mozilla.glean.forUnitTests testImplementation libs.robolectric androidTestImplementation project(':components:support-android-test') androidTestImplementation libs.androidx.benchmark.junit4 // We need a version constraint due to AGP silently trying to downgrade the version otherwise. // https://issuetracker.google.com/issues/349628366 constraints { implementation(libs.androidx.concurrent) } androidTestImplementation libs.androidx.concurrent androidTestImplementation platform(libs.androidx.compose.bom) androidTestImplementation libs.androidx.compose.ui.test androidTestImplementation libs.androidx.test.core androidTestImplementation(libs.androidx.test.espresso.contrib) { exclude module: 'protobuf-lite' } androidTestImplementation libs.androidx.test.espresso.core androidTestImplementation libs.androidx.test.espresso.idling.resource androidTestImplementation libs.androidx.test.espresso.intents androidTestImplementation libs.androidx.test.junit androidTestImplementation libs.androidx.test.monitor androidTestImplementation libs.androidx.test.rules androidTestImplementation libs.androidx.test.runner androidTestImplementation libs.androidx.test.uiautomator androidTestImplementation libs.androidx.work.testing androidTestImplementation libs.leakcanary.instrumentation androidTestImplementation libs.mockk.android androidTestImplementation libs.mockwebserver androidTestImplementation libs.okhttp androidTestRuntimeOnly libs.okio constraints { // Various AndroidX dependencies pull in 1.1.1 transitively; OkHttp 5 requires 1.2.0. implementation(libs.androidx.startup.runtime) // okhttp is test-only; prevents older releases being resolved on the main classpath. implementation(libs.okio) } androidTestUtil libs.androidx.test.orchestrator lintChecks project(':components:tooling-lint') } protobuf { protoc { artifact = libs.protobuf.compiler.get() } // Generates the java Protobuf-lite code for the Protobufs in this project. See // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation // for more information. generateProtoTasks { all().each { task -> task.builtins { java { option 'lite' } } } } } if (project.hasProperty("coverage")) { tasks.withType(Test).configureEach { jacoco.includeNoLocationClasses = true jacoco.excludes = ['jdk.internal.*'] } android.applicationVariants.configureEach { variant -> tasks.register("jacoco${variant.name.capitalize()}TestReport", JacocoReport) { dependsOn "test${variant.name.capitalize()}UnitTest" reports { xml.required = true html.required = true } def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*'] def kotlinDebugTree = fileTree(dir: "$project.layout.buildDirectory/tmp/kotlin-classes/${variant.name}", excludes: fileFilter) def javaDebugTree = fileTree(dir: "$project.layout.buildDirectory/intermediates/classes/${variant.flavorName}/${variant.buildType.name}", excludes: fileFilter) def mainSrc = "$project.projectDir/src/main/java" sourceDirectories.setFrom(files([mainSrc])) classDirectories.setFrom(files([kotlinDebugTree, javaDebugTree])) executionData.setFrom(fileTree(dir: project.layout.buildDirectory, includes: [ "jacoco/test${variant.name.capitalize()}UnitTest.exec", 'outputs/code-coverage/connected/*coverage.ec' ])) } } android { buildTypes { debug { testCoverageEnabled = true } } } } // ------------------------------------------------------------------------------------------------- // Task for printing APK information for the requested variant // Usage: "./gradlew printVariants // ------------------------------------------------------------------------------------------------- tasks.register('printVariants') { def variants = project.provider { android.applicationVariants.collect { variant -> [ apks: variant.outputs.collect { output -> [ abi: getAbi(output), fileName: output.outputFile.name ]}, build_type: variant.buildType.name, name: variant.name, ]}} def outputFile = new File(projectDir, "build/printVariants.json") doLast { // AndroidTest is a special case not included above variants.get().add([ apks: [[ abi: 'noarch', fileName: 'app-debug-androidTest.apk', ]], build_type: 'androidTest', name: 'androidTest', ]) def json = JsonOutput.toJson(variants.get()) outputFile.parentFile.mkdirs() outputFile.text = json logger.debug("Wrote variant info to $outputFile") } } // Work around generated SafeArgs code hitting USELESS_CAST warnings with Kotlin 2.3+ // https://issuetracker.google.com/issues/458082829 def safeArgsSrc = layout.buildDirectory .dir("generated/java") .map { dir -> fileTree(dir) { include "generateSafeArgs*/**/*.kt" } } tasks.register("suppressUselessCastInSafeArgs") { inputs.files(safeArgsSrc) outputs.upToDateWhen { false } doLast { safeArgsSrc.get().each { File f -> if (!f.text.contains('@file:Suppress("USELESS_CAST")')) { f.text = """@file:Suppress("USELESS_CAST") ${f.text}""" } } } } tasks.matching { it.name.startsWith("generateSafeArgs") }.configureEach { finalizedBy tasks.named("suppressUselessCastInSafeArgs") } afterEvaluate { // Format test output. Ported from AC #2401 tasks.withType(Test).configureEach { systemProperty "robolectric.logging", "stdout" systemProperty "logging.test-mode", "true" testLogging.events = [] beforeSuite { descriptor -> if (descriptor.getClassName() != null) { println("\nSUITE: " + descriptor.getClassName()) } } beforeTest { descriptor -> println(" TEST: " + descriptor.getName()) } onOutput { descriptor, event -> it.logger.lifecycle(" " + event.message.trim()) } afterTest { descriptor, result -> switch (result.getResultType()) { case ResultType.SUCCESS: println(" SUCCESS") break case ResultType.FAILURE: def testId = descriptor.getClassName() + "." + descriptor.getName() println(" TEST-UNEXPECTED-FAIL | " + testId + " | " + result.getException()) break case ResultType.SKIPPED: println(" SKIPPED") break } it.logger.lifecycle("") } } } if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopsrcdir')) { if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopobjdir')) { ext.topobjdir = gradle."localProperties.dependencySubstitutions.geckoviewTopobjdir" } ext.topsrcdir = gradle."localProperties.dependencySubstitutions.geckoviewTopsrcdir" apply from: "${topsrcdir}/substitute-local-geckoview.gradle" } def getSupportedLocales() { // This isn't running as a task, instead the array is build when the gradle file is parsed. // https://github.com/mozilla-mobile/fenix/issues/14175 def foundLocales = new StringBuilder() foundLocales.append("new String[]{") fileTree("src/main/res").visit { FileVisitDetails details -> if (details.file.path.endsWith("${File.separator}strings.xml")) { def languageCode = details.file.parent.tokenize(File.separator).last().replaceAll('values-', '').replaceAll('-r', '-') languageCode = (languageCode == "values") ? "en-US" : languageCode foundLocales.append("\"").append(languageCode).append("\"").append(",") } } foundLocales.append("}") def foundLocalesString = foundLocales.toString().replaceAll(',}', '}') return foundLocalesString } // Fix for Gradle 9 strictness: Ensure CleanUp runs before DependencyTask tasks.withType(DependencyTask).configureEach { task -> // The OSS plugin creates a "LicensesCleanUp" task for every "DependencyTask" // e.g., nightlyOssDependencyTask -> nightlyOssLicensesCleanUp def cleanUpTaskName = task.name.replace("DependencyTask", "LicensesCleanUp") // make the dependency task depend on its cleanup task. task.dependsOn(tasks.named(cleanUpTaskName)) } tasks.register("ktlint", Exec) { group = "verification" description = "Check Kotlin code style." workingDir file("${gradle.mozconfig.topsrcdir}") commandLine "./mach", "gradle", "-p", "mobile/android/fenix", ":ktlint" } tasks.register("ktlintFormat", Exec) { group = "formatting" description = "Fix Kotlin code style deviations." workingDir file("${gradle.mozconfig.topsrcdir}") commandLine "./mach", "gradle", "-p", "mobile/android/fenix", ":ktlintFormat" } tasks.register("detekt", Exec) { group = "verification" description = "Run detekt static analysis." workingDir file("${gradle.mozconfig.topsrcdir}") commandLine "./mach", "gradle", "-p", "mobile/android/fenix", ":detekt" }