Source code
Revision control
Copy as Markdown
Other Tools
plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.python.envs.plugin)
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-parcelize'
apply plugin: 'jacoco'
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
import groovy.json.JsonOutput
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import static org.gradle.api.tasks.testing.TestResult.ResultType
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")
}
defaultConfig {
applicationId "org.mozilla"
minSdkVersion config.minSdkVersion
compileSdk = config.compileSdkVersion
targetSdkVersion 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 (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey") ||
gradle.mozconfig.substs.DEVELOPER_OPTIONS) {
println ("All builds will be automatically signed with the debug key")
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 "x86", "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'
}
packaging {
if (!isAppBundle) {
jniLibs {
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"
apply plugin: "org.mozilla.appservices.nimbus-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 {
// We pick up JNA transitively by way of Glean, which is currently on version 5.14.0.
// However, we need to force version 5.17.0 due to Google Play's 16KB page size requirement.
// JNA 5.15.0+ crashes on Android <7, however, so it can't be bumped in Glean at this time.
// Therefore, manually force the use of version 5.17.0 at the app level since we only support
// running on Android 8+ now anyway. This can be removed once Glean updates.
constraints {
implementation(libs.jna)
}
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)
androidTestImplementation platform(libs.androidx.compose.bom)
implementation libs.androidx.compose.foundation
implementation libs.androidx.compose.material
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.ktx
implementation libs.androidx.core.splashscreen
implementation libs.androidx.datastore.preferences
implementation libs.androidx.fragment
implementation libs.androidx.lifecycle.process
implementation libs.androidx.lifecycle.viewmodel
implementation libs.androidx.palette
implementation libs.androidx.preferences
implementation libs.androidx.recyclerview
implementation libs.androidx.savedstate
implementation libs.androidx.transition
implementation libs.androidx.work.runtime
// Required for in-app reviews
implementation libs.play.review
implementation libs.play.review.ktx
implementation libs.google.material
implementation libs.thirdparty.sentry
implementation project(':components:browser-engine-gecko')
implementation project(':components:browser-domains')
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:concept-awesomebar')
implementation project(':components:concept-engine')
implementation project(':components:concept-fetch')
implementation project(':components:concept-menu')
implementation project(':components:compose-awesomebar')
implementation project(':components:feature-awesomebar')
implementation project(':components:feature-app-links')
implementation project(':components:feature-customtabs')
implementation project(':components:feature-contextmenu')
implementation project(':components:feature-downloads')
implementation project(':components:feature-findinpage')
implementation project(':components:feature-intent')
implementation project(':components:feature-prompts')
implementation project(':components:feature-session')
implementation project(':components:feature-search')
implementation project(':components:feature-tabs')
implementation project(':components:feature-toolbar')
implementation project(':components:feature-top-sites')
implementation project(':components:feature-sitepermissions')
implementation project(':components:lib-crash')
implementation project(':components:lib-crash-sentry')
implementation project(':components:lib-state')
implementation project(':components:feature-media')
implementation project(':components:lib-auth')
implementation project(':components:lib-publicsuffixlist')
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:ui-autocomplete')
implementation project(':components:ui-colors')
implementation project(':components:ui-icons')
implementation project(':components:ui-tabcounter')
implementation project(':components:ui-widgets')
implementation project(':components:feature-webcompat')
implementation project(':components:feature-webcompat-reporter')
implementation project(':components:support-webextensions')
implementation project(':components:support-locale')
implementation project(':components:compose-cfr')
implementation project(":components:service-glean")
implementation libs.mozilla.glean, {
exclude group: 'org.mozilla.telemetry', module: 'glean-native'
}
implementation libs.kotlin.coroutines
debugImplementation libs.leakcanary
testImplementation "org.mozilla.telemetry:glean-native-forUnitTests:${project.ext.glean_version}"
testImplementation platform(libs.junit.bom)
testImplementation libs.junit.jupiter
testRuntimeOnly libs.junit.platform.launcher
testImplementation libs.testing.robolectric
testImplementation libs.testing.mockito
testImplementation libs.testing.coroutines
testImplementation libs.androidx.work.testing
testImplementation libs.androidx.arch.core.testing
testImplementation project(':components:support-test')
testImplementation project(':components:support-test-libstate')
androidTestImplementation libs.testing.mockwebserver
testImplementation libs.testing.mockwebserver
testImplementation project(':components:lib-fetch-okhttp')
testImplementation libs.androidx.test.core
testImplementation libs.androidx.test.runner
testImplementation libs.androidx.test.rules
androidTestImplementation libs.androidx.espresso.core
androidTestImplementation libs.androidx.espresso.idling.resource
androidTestImplementation libs.androidx.espresso.intents
androidTestImplementation libs.androidx.espresso.web
androidTestImplementation libs.androidx.test.core
androidTestImplementation libs.androidx.test.junit
androidTestImplementation libs.androidx.test.monitor
androidTestImplementation libs.androidx.test.runner
androidTestImplementation libs.androidx.test.uiautomator
androidTestUtil libs.androidx.test.orchestrator
lintChecks project(':components:tooling-lint')
}
// -------------------------------------------------------------------------------------------------
// Dynamically set versionCode (See tools/build/versionCode.gradle
// -------------------------------------------------------------------------------------------------
android.applicationVariants.configureEach { variant ->
def buildType = variant.buildType.name
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)
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
// 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.getFilter(FilterConfiguration.FilterType.ABI.name())
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")
if (versionName != null) {
output.versionNameOverride = versionName
}
output.versionCodeOverride = versionCodeOverride
}
}
}
// -------------------------------------------------------------------------------------------------
// MLS: Read token from local file if it exists (Only release builds)
// -------------------------------------------------------------------------------------------------
android.applicationVariants.configureEach {
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")
}
}
// -------------------------------------------------------------------------------------------------
// Sentry: Read token from local file if it exists (Only release builds)
// -------------------------------------------------------------------------------------------------
android.applicationVariants.configureEach {
project.logger.debug("Sentry token: ")
try {
def token = new File("${rootDir}/.sentry_token").text.trim()
buildConfigField 'String', 'SENTRY_TOKEN', '"' + token + '"'
project.logger.debug("(Added from .sentry_token file)")
} catch (FileNotFoundException ignored) {
buildConfigField 'String', 'SENTRY_TOKEN', '""'
project.logger.debug("X_X")
}
}
// -------------------------------------------------------------------------------------------------
// 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
// -------------------------------------------------------------------------------------------------
project.logger.debug("Nimbus endpoint: ")
android.applicationVariants.configureEach { variant ->
def variantName = variant.getName()
if (!variantName.contains("Debug")) {
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("--")
}
}
def generatedLocaleListDir = file('src/main/java/org/mozilla/focus/generated')
def generatedLocaleListFilename = 'LocalesList.kt'
def enabledLocales = project.provider { getEnabledLocales() }
tasks.register('generateLocaleList') {
def localeList = file(new File(generatedLocaleListDir, generatedLocaleListFilename))
doLast {
generatedLocaleListDir.mkdir()
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 << "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<String> = Collections.unmodifiableList("
localeList << "\n"
localeList << " " << "listOf("
localeList << "\n"
localeList << " "
localeList << enabledLocales.get().join(",\n" + " ")
localeList << ",\n"
localeList << " )," << "\n"
localeList << " )" << "\n"
localeList << "}" << "\n"
}
}
tasks.configureEach { task ->
if (name.contains("compile")) {
task.dependsOn generateLocaleList
}
}
clean.doLast {
generatedLocaleListDir.deleteDir()
}
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 {
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.name}",
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"
}
}
}
}
// -------------------------------------------------------------------------------------------------
// Task for printing APK information for the requested variant
// Taskgraph Usage: "./gradlew printVariants
// -------------------------------------------------------------------------------------------------
tasks.register('printVariants') {
def variants = project.provider { android.applicationVariants.collect { variant -> [
apks: variant.outputs.collect { output -> [
abi: output.getFilter(FilterConfiguration.FilterType.ABI.name()),
fileName: output.outputFile.name
]},
build_type: variant.buildType.name,
name: variant.name,
]}}
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',
])
println 'variants: ' + JsonOutput.toJson(variants.get())
}
}
afterEvaluate {
// Format test output. Copied from Fenix, which was 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("")
}
}
}