plugins { id 'com.android.application' id 'org.mozilla.conventions.zip-test-reports' id 'org.mozilla.nimbus-gradle-plugin' id 'jacoco' alias(libs.plugins.dependency.analysis) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.python.envs.plugin) } if (gradle.mozconfig.substs.MOZILLA_OFFICIAL) { apply plugin: 'com.google.android.gms.oss-licenses-plugin' } def isAppBundle = gradle.startParameter.taskNames.any { it.toLowerCase().contains("bundle") } def versionCodeGradle = "$project.rootDir/tools/gradle/versionCode.gradle" if (findProject(":geckoview") != null) { versionCodeGradle = "$project.rootDir/mobile/android/focus-android/tools/gradle/versionCode.gradle" } apply from: versionCodeGradle import com.android.build.api.variant.FilterConfiguration android { 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 11 // This versionCode is "frozen" for local builds. For "release" builds we // override this with a generated versionCode at build time. // The versionName is dynamically overridden for all the build variants at build time. versionName Config.generateDebugVersionName() testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments clearPackageData: 'true' if (gradle.mozconfig.substs.MOZ_INCLUDE_SOURCE_INFO) { buildConfigField("String", "VCS_HASH", "\"" + gradle.mozconfig.source_repo.MOZ_SOURCE_STAMP + "\"") } else { buildConfigField("String", "VCS_HASH", "\"\"") } vectorDrawables.useSupportLibrary = true } bundle { language { // Because we have runtime language selection we will keep all strings and languages // in the base APKs. enableSplit = false } } lint { lintConfig = file("lint.xml") baseline = file("lint-baseline.xml") sarifReport = true sarifOutput = file("../build/reports/lint/lint-report.sarif.json") abortOnError = false } // We have a three dimensional build configuration: // BUILD TYPE (debug, release) X PRODUCT FLAVOR (focus, klar) buildTypes { release { // 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'] // 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) { println ("All builds will be debuggable") debuggable true } } debug { applicationIdSuffix ".debug" matchingFallbacks = ['debug'] } beta { initWith release applicationIdSuffix ".beta" // This is used when the user selects text in other third-party apps. See https://github.com/mozilla-mobile/focus-android/issues/6478 manifestPlaceholders = [textSelectionSearchAction: "@string/text_selection_search_action_focus_beta"] } nightly { initWith release applicationIdSuffix ".nightly" // This is used when the user selects text in other third-party apps. See https://github.com/mozilla-mobile/focus-android/issues/6478 manifestPlaceholders = [textSelectionSearchAction: "@string/text_selection_search_action_focus_nightly"] } } testOptions { animationsDisabled = true execution = 'ANDROIDX_TEST_ORCHESTRATOR' testCoverage { jacocoVersion = libs.versions.jacoco.get() } unitTests { includeAndroidResources = true } } buildFeatures { compose = true viewBinding = true buildConfig = true } flavorDimensions.add("product") productFlavors { // In most countries we are Firefox Focus - but in some we need to be Firefox Klar focus { dimension "product" applicationIdSuffix ".focus" // This is used when the user selects text in other third-party apps. See https://github.com/mozilla-mobile/focus-android/issues/6478 manifestPlaceholders = [textSelectionSearchAction: "@string/text_selection_search_action_focus"] } klar { dimension "product" applicationIdSuffix ".klar" // This is used when the user selects text in other third-party apps. See https://github.com/mozilla-mobile/focus-android/issues/6478 manifestPlaceholders = [textSelectionSearchAction: "@string/text_selection_search_action_klar"] } } splits { abi { enable = !isAppBundle reset() include "armeabi-v7a", "arm64-v8a", "x86_64" } } sourceSets { test { resources { // Make the default asset folder available as test resource folder. Robolectric seems // to fail to read assets for our setup. With this we can just read the files directly // and do not need to rely on Robolectric. srcDir "${projectDir}/src/main/assets/" } } // Release focusRelease.root = 'src/focusRelease' klarRelease.root = 'src/klarRelease' // Debug focusDebug.root = 'src/focusDebug' klarDebug.root = 'src/klarDebug' // Nightly focusNightly.root = 'src/focusNightly' klarNightly.root = 'src/klarNightly' } // Exclude native libraries for unsupported architectures from third-party dependencies packaging { jniLibs { excludes += ["**/armeabi/*.so", "**/mips/*.so", "**/mips64/*.so", "**/x86/*.so"] if (!isAppBundle) { useLegacyPackaging = true } } } namespace = 'org.mozilla.focus' } // ------------------------------------------------------------------------------------------------- // Generate Kotlin code for the Focus Glean metrics. // ------------------------------------------------------------------------------------------------- ext { // Enable expiration by major version. gleanExpireByVersion = 1 gleanNamespace = "mozilla.telemetry.glean" gleanPythonEnvDir = gradle.mozconfig.substs.GRADLE_GLEAN_PARSER_VENV gleanYamlFiles = [ "${project.projectDir}/metrics.yaml", "${project.projectDir}/legacy_metrics.yaml", "${project.projectDir}/pings.yaml", "${project.projectDir}/legacy_pings.yaml", "${project.projectDir}/tags.yaml", ] } apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" nimbus { // The path to the Nimbus feature manifest file manifestFile = "nimbus.fml.yaml" // 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 = [ focusDebug: "debug", focusNightly: "nightly", focusBeta: "beta", focusRelease: "release", klarDebug: "debug", klarNightly: "nightly", klarBeta: "beta", klarRelease: "release", ] // 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" } dependencies { implementation project(':components:browser-domains') implementation project(':components:browser-engine-gecko') implementation project(':components:browser-errorpages') implementation project(':components:browser-icons') implementation project(':components:browser-menu') implementation project(':components:browser-state') implementation project(':components:browser-toolbar') implementation project(':components:compose-awesomebar') implementation project(':components:compose-base') implementation project(':components:compose-cfr') implementation project(':components:concept-awesomebar') implementation project(':components:concept-engine') implementation project(':components:concept-fetch') implementation project(':components:concept-menu') implementation project(':components:feature-app-links') 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-intent') implementation project(':components:feature-media') implementation project(':components:feature-prompts') implementation project(':components:feature-search') implementation project(':components:feature-session') implementation project(':components:feature-sitepermissions') implementation project(':components:feature-tabs') implementation project(':components:feature-toolbar') implementation project(':components:feature-top-sites') implementation project(':components:feature-webcompat') implementation project(':components:feature-webcompat-reporter') implementation project(':components:lib-auth') implementation project(':components:lib-crash') implementation project(':components:lib-crash-sentry') implementation project(':components:lib-publicsuffixlist') implementation project(':components:lib-state') implementation project(":components:service-glean") implementation project(':components:service-location') implementation project(':components:service-nimbus') implementation project(':components:support-ktx') implementation project(':components:support-utils') implementation project(':components:support-remotesettings') implementation project(':components:support-appservices') implementation project(':components:support-license') implementation project(':components:support-locale') implementation project(':components:support-webextensions') implementation project(':components:ui-autocomplete') implementation project(':components:ui-colors') implementation project(':components:ui-icons') implementation project(':components:ui-tabcounter') implementation project(':components:ui-widgets') implementation libs.androidx.activity implementation libs.androidx.appcompat implementation libs.androidx.browser implementation libs.androidx.cardview implementation libs.androidx.collection implementation platform(libs.androidx.compose.bom) implementation libs.androidx.compose.foundation implementation libs.androidx.compose.material.icons implementation libs.androidx.compose.material3 implementation libs.androidx.compose.runtime.livedata implementation libs.androidx.compose.ui implementation libs.androidx.compose.ui.tooling implementation libs.androidx.constraintlayout implementation libs.androidx.constraintlayout.compose implementation libs.androidx.core implementation libs.androidx.core.splashscreen implementation libs.androidx.datastore.preferences implementation libs.androidx.emoji2 implementation libs.androidx.fragment implementation libs.androidx.lifecycle.process implementation libs.androidx.lifecycle.viewmodel implementation libs.androidx.palette implementation libs.androidx.preference implementation libs.androidx.recyclerview implementation libs.androidx.savedstate implementation libs.androidx.transition implementation libs.androidx.work.runtime implementation libs.google.material implementation libs.kotlinx.coroutines implementation libs.mozilla.glean implementation libs.play.review implementation libs.play.review.ktx implementation libs.sentry debugImplementation libs.leakcanary testImplementation project(':components:lib-fetch-okhttp') testImplementation project(':components:support-test') testImplementation project(':components:support-test-libstate') testImplementation libs.androidx.arch.core.testing testImplementation libs.androidx.test.core testImplementation libs.androidx.test.rules testImplementation libs.androidx.test.runner testImplementation libs.androidx.work.testing testImplementation platform(libs.junit.bom) testImplementation libs.junit.jupiter testRuntimeOnly libs.junit.platform.launcher testImplementation libs.kotlin.test testImplementation libs.kotlinx.coroutines.test testImplementation libs.mockito testImplementation libs.mockwebserver testImplementation libs.mozilla.glean.forUnitTests testImplementation libs.robolectric // 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 project(':components:support-android-test') androidTestImplementation libs.androidx.concurrent androidTestImplementation libs.androidx.test.core androidTestImplementation libs.androidx.test.espresso.core androidTestImplementation libs.androidx.test.espresso.idling.resource androidTestImplementation libs.androidx.test.espresso.intents androidTestImplementation libs.androidx.test.espresso.web androidTestImplementation libs.androidx.test.junit androidTestImplementation libs.androidx.test.monitor androidTestImplementation libs.androidx.test.runner androidTestImplementation libs.androidx.test.uiautomator androidTestImplementation libs.kotlin.test 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') } // ------------------------------------------------------------------------------------------------- // Dynamically set versionCode (See tools/build/versionCode.gradle // ------------------------------------------------------------------------------------------------- androidComponents { onVariants(selector().all()) { variant -> def buildType = variant.buildType project.logger.debug("----------------------------------------------") project.logger.debug("Variant name: " + variant.name) project.logger.debug("Build type: " + buildType) project.logger.debug("Flavor: " + variant.flavorName) if (buildType == "release" || buildType == "nightly" || buildType == "beta") { def baseVersionCode = generatedVersionCode def versionName = buildType == "nightly" ? "${Config.nightlyVersionName(project)}" : "${Config.releaseVersionName(project)}" project.logger.debug("versionName override: $versionName") // 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. See https://developer.android.com/studio/publish/versioning // Our generated version code now has a length of 9 (See tools/gradle/versionCode.gradle). // Our x86 builds need a higher version code to avoid installing ARM builds on an x86 device // with ARM compatibility mode. // AAB builds need a version code that is distinct from any APK builds. Since AAB and APK // builds may run in parallel, AAB and APK version codes might be based on the same // (minute granularity) time of day. To avoid conflicts, we ensure the minute portion // of the version code is even for APKs and odd for AABs. variant.outputs.each { output -> def abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier if (isAppBundle) { abi = "AAB" } // We use the same version code generator, that we inherited from Fennec, across all channels - even on // channels that never shipped a Fennec build. // ensure baseVersionCode is an even number if (baseVersionCode % 2) { baseVersionCode = baseVersionCode + 1 } def versionCodeOverride = baseVersionCode if (abi == "AAB") { // AAB version code is odd versionCodeOverride = versionCodeOverride + 1 } else if (abi == "x86_64") { versionCodeOverride = versionCodeOverride + 6 } else if (abi == "x86") { versionCodeOverride = versionCodeOverride + 4 } else if (abi == "arm64-v8a") { versionCodeOverride = versionCodeOverride + 2 } else if (abi == "armeabi-v7a") { versionCodeOverride = versionCodeOverride + 0 } else { throw new RuntimeException("Unknown ABI: " + abi) } project.logger.debug("versionCode for $abi = $versionCodeOverride") output.versionName.set(versionName) output.versionCode.set(versionCodeOverride) } } } } // ------------------------------------------------------------------------------------------------- // MLS and Sentry: Read tokens from local files if they exist // ------------------------------------------------------------------------------------------------- android.defaultConfig.with { try { def token = new File("${rootDir}/.mls_token").text.trim() buildConfigField 'String', 'MLS_TOKEN', '"' + token + '"' } catch (FileNotFoundException ignored) { buildConfigField 'String', 'MLS_TOKEN', '""' } try { def token = new File("${rootDir}/.sentry_token").text.trim() buildConfigField 'String', 'SENTRY_TOKEN', '"' + token + '"' } catch (FileNotFoundException ignored) { buildConfigField 'String', 'SENTRY_TOKEN', '""' } } // ------------------------------------------------------------------------------------------------- // L10N: Generate list of locales // Focus provides its own (Android independent) locale switcher. That switcher requires a list // of locale codes. We generate that list here to avoid having to manually maintain a list of locales: // ------------------------------------------------------------------------------------------------- def getEnabledLocales() { def resDir = file('src/main/res') def potentialLanguageDirs = resDir.listFiles(new FilenameFilter() { @Override boolean accept(File dir, String name) { return name.startsWith("values-") } }) def langs = potentialLanguageDirs.findAll { // Only select locales where strings.xml exists // Some locales might only contain e.g. sumo URLS in urls.xml, and should be skipped (see es vs es-ES/es-MX/etc) return file(new File(it, "strings.xml")).exists() } .collect { // And reduce down to actual values-* names return it.name } .collect { return it.substring("values-".length()) } .collect { if (it.length() > 3 && it.contains("-r")) { // Android resource dirs add an "r" prefix to the region - we need to strip that for java usage // Add 1 to have the index of the r, without the dash def regionPrefixPosition = it.indexOf("-r") + 1 return it.substring(0, regionPrefixPosition) + it.substring(regionPrefixPosition + 1) } else { return it } }.collect { return '"' + it + '"' } // en-US is the default language (in "values") and therefore needs to be added separately langs << "\"en-US\"" return langs.sort { it } } // ------------------------------------------------------------------------------------------------- // Nimbus: Read endpoint from local.properties of a local file if it exists // ------------------------------------------------------------------------------------------------- // Non-debug builds use the .nimbus file; the debug build type uses a local.properties override. android.defaultConfig.with { try { def url = new File("${rootDir}/.nimbus").text.trim() buildConfigField 'String', 'NIMBUS_ENDPOINT', '"' + url + '"' } catch (FileNotFoundException ignored) { buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null' } } android.buildTypes.debug.with { if (gradle.hasProperty("localProperties.nimbus.remote-settings.url")) { def url = gradle.getProperty("localProperties.nimbus.remote-settings.url") buildConfigField 'String', 'NIMBUS_ENDPOINT', '"' + url + '"' } else { buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null' } } abstract class GenerateLocaleList extends DefaultTask { @Input abstract ListProperty getEnabledLocales() @OutputDirectory abstract DirectoryProperty getOutputDir() @TaskAction void generate() { def dir = new File(outputDir.get().asFile, "org/mozilla/focus/generated") dir.mkdirs() def localeList = new File(dir, "LocalesList.kt") localeList.delete() localeList.createNewFile() localeList << "/* This Source Code Form is subject to the terms of the Mozilla Public" << "\n" localeList << " * License, v. 2.0. If a copy of the MPL was not distributed with this" << "\n" localeList << " * file, You can obtain one at http://mozilla.org/MPL/2.0/. */" << "\n" << "\n" localeList << "package org.mozilla.focus.generated" << "\n" << "\n" localeList << "import java.util.Collections" << "\n" localeList << "\n" localeList << "/**" localeList << "\n" localeList << " * Provides a list of bundled locales based on the language files in the res folder." localeList << "\n" localeList << " */" localeList << "\n" localeList << "object LocalesList {" << "\n" localeList << " " << "val BUNDLED_LOCALES: List = Collections.unmodifiableList(" localeList << "\n" localeList << " " << "listOf(" localeList << "\n" localeList << " " localeList << enabledLocales.get().join(",\n" + " ") localeList << ",\n" localeList << " )," << "\n" localeList << " )" << "\n" localeList << "}" << "\n" } } // Built at script scope so the closure resolves getEnabledLocales() to the script method rather // than the task's same-named property accessor. def enabledLocalesProvider = project.provider { getEnabledLocales() } def generateLocaleList = tasks.register('generateLocaleList', GenerateLocaleList) { enabledLocales.set(enabledLocalesProvider) } androidComponents { onVariants(selector().all()) { variant -> variant.sources.java.addGeneratedSourceDirectory(generateLocaleList) { it.outputDir } } } // The variant Sources API wires compilation automatically, but DAGP's source exploder // consumes the sources separately, so make it depend on the generator. tasks.matching { it.name.startsWith("explodeCodeSource") }.configureEach { t -> t.dependsOn(generateLocaleList) } if (project.hasProperty("coverage")) { tasks.withType(Test).configureEach { jacoco.includeNoLocationClasses = true jacoco.excludes = ['jdk.internal.*'] } androidComponents { onVariants(selector().all()) { variant -> tasks.register("jacoco${variant.name.capitalize()}TestReport", JacocoReport) { dependsOn(["test${variant.name.capitalize()}UnitTest"]) reports { html.required = true xml.required = true } def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*'] def kotlinTree = fileTree(dir: "$project.layout.buildDirectory/tmp/kotlin-classes/${variant.name}", excludes: fileFilter) def javaTree = fileTree(dir: "$project.layout.buildDirectory/intermediates/classes/${variant.flavorName ?: ''}/${variant.buildType}", excludes: fileFilter) def mainSrc = "$project.projectDir/src/main/java" sourceDirectories.setFrom(files([mainSrc])) classDirectories.setFrom(files([kotlinTree, javaTree])) 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 applicationIdSuffix ".coverage" } } } } mozilla { ktlintSourcePaths = ["src/**/*.kt", "!**/build/**/*.kt"] } tasks.register("detekt", Exec) { group = "verification" description = "Run detekt static analysis." workingDir file("${gradle.mozconfig.topsrcdir}") commandLine "./mach", "gradle", "-p", "mobile/android/focus-android", ":detekt" }