Source code
Revision control
Copy as Markdown
Other Tools
plugins {
id "com.jetbrains.python.envs" version "$python_envs_plugin"
}
if (findProject(":geckoview") != null) {
buildDir "${topobjdir}/gradle/build/mobile/android/focus-android"
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'jacoco'
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
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.gradle.internal.logging.text.StyledTextOutput.Style
import org.gradle.internal.logging.text.StyledTextOutputFactory
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'
// See override in release builds for why it's blank.
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")
}
// 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.ext.vcsHashFileContent) {
buildConfigField "String", "VCS_HASH", "\"hg-${gradle.ext.vcsHashFileContent}\""
} else {
buildConfigField "String", "VCS_HASH", "\"${Config.vcsHash}\""
}
if (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey")) {
println ("All builds will be automatically signed with the debug key")
signingConfig signingConfigs.debug
}
if (gradle.hasProperty("localProperties.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 {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
animationsDisabled = true
unitTests {
includeAndroidResources = true
}
}
buildFeatures {
compose true
viewBinding true
buildConfig true
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.compose_compiler
}
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 true
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'
}
packagingOptions {
resources {
pickFirsts += ['META-INF/atomicfu.kotlin_module', 'META-INF/proguard/coroutines.pro']
}
jniLibs {
useLegacyPackaging true
}
}
namespace 'org.mozilla.focus'
}
tasks.withType(KotlinCompile).configureEach {
kotlinOptions.allWarningsAsErrors = true
kotlinOptions.freeCompilerArgs += [
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlin.RequiresOptIn",
"-Xjvm-default=all"
]
}
// -------------------------------------------------------------------------------------------------
// 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
}
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 {
implementation ComponentsDependencies.androidx_activity
implementation ComponentsDependencies.androidx_appcompat
implementation ComponentsDependencies.androidx_browser
implementation ComponentsDependencies.androidx_cardview
implementation ComponentsDependencies.androidx_collection
implementation platform(ComponentsDependencies.androidx_compose_bom)
androidTestImplementation platform(ComponentsDependencies.androidx_compose_bom)
implementation ComponentsDependencies.androidx_compose_foundation
implementation ComponentsDependencies.androidx_compose_material
implementation ComponentsDependencies.androidx_compose_material_icons
implementation ComponentsDependencies.androidx_compose_runtime_livedata
implementation ComponentsDependencies.androidx_compose_ui
implementation ComponentsDependencies.androidx_compose_ui_tooling
implementation ComponentsDependencies.androidx_constraintlayout
implementation ComponentsDependencies.androidx_constraintlayout_compose
implementation ComponentsDependencies.androidx_core_ktx
implementation ComponentsDependencies.androidx_core_splashscreen
implementation ComponentsDependencies.androidx_datastore_preferences
implementation ComponentsDependencies.androidx_fragment
implementation ComponentsDependencies.androidx_lifecycle_process
implementation ComponentsDependencies.androidx_lifecycle_viewmodel
implementation ComponentsDependencies.androidx_palette
implementation ComponentsDependencies.androidx_preferences
implementation ComponentsDependencies.androidx_recyclerview
implementation ComponentsDependencies.androidx_savedstate
implementation ComponentsDependencies.androidx_transition
implementation ComponentsDependencies.androidx_work_runtime
// Required for in-app reviews
implementation ComponentsDependencies.play_review
implementation ComponentsDependencies.play_review_ktx
implementation ComponentsDependencies.google_material
implementation ComponentsDependencies.thirdparty_sentry
implementation project(':browser-engine-gecko')
implementation project(':browser-domains')
implementation project(':browser-errorpages')
implementation project(':browser-icons')
implementation project(':browser-menu')
implementation project(':browser-state')
implementation project(':browser-toolbar')
implementation project(':concept-awesomebar')
implementation project(':concept-engine')
implementation project(':concept-fetch')
implementation project(':concept-menu')
implementation project(':compose-awesomebar')
implementation project(':feature-awesomebar')
implementation project(':feature-app-links')
implementation project(':feature-customtabs')
implementation project(':feature-contextmenu')
implementation project(':feature-downloads')
implementation project(':feature-findinpage')
implementation project(':feature-intent')
implementation project(':feature-prompts')
implementation project(':feature-session')
implementation project(':feature-search')
implementation project(':feature-tabs')
implementation project(':feature-toolbar')
implementation project(':feature-top-sites')
implementation project(':feature-sitepermissions')
implementation project(':lib-crash')
implementation project(':lib-crash-sentry')
implementation project(':lib-state')
implementation project(':feature-media')
implementation project(':lib-auth')
implementation project(':lib-publicsuffixlist')
implementation project(':service-location')
implementation project(':service-nimbus')
implementation project(':support-ktx')
implementation project(':support-utils')
implementation project(':support-rusthttp')
implementation project(':support-rustlog')
implementation project(':support-license')
implementation project(':ui-autocomplete')
implementation project(':ui-colors')
implementation project(':ui-icons')
implementation project(':ui-tabcounter')
implementation project(':ui-widgets')
implementation project(':feature-webcompat')
implementation project(':feature-webcompat-reporter')
implementation project(':support-webextensions')
implementation project(':support-locale')
implementation project(':compose-cfr')
implementation project(":service-glean")
implementation ComponentsDependencies.mozilla_glean, {
exclude group: 'org.mozilla.telemetry', module: 'glean-native'
}
implementation ComponentsDependencies.kotlin_coroutines
debugImplementation ComponentsDependencies.leakcanary
focusImplementation FocusDependencies.adjust
focusImplementation FocusDependencies.install_referrer // Required by Adjust
testImplementation "org.mozilla.telemetry:glean-native-forUnitTests:${project.ext.glean_version}"
testImplementation ComponentsDependencies.junit_api
testImplementation ComponentsDependencies.junit_params
testRuntimeOnly ComponentsDependencies.junit_engine
testImplementation ComponentsDependencies.testing_robolectric
testImplementation ComponentsDependencies.testing_mockito
testImplementation ComponentsDependencies.testing_coroutines
testImplementation ComponentsDependencies.androidx_work_testing
testImplementation ComponentsDependencies.androidx_arch_core_testing
testImplementation project(':support-test')
testImplementation project(':support-test-libstate')
androidTestImplementation ComponentsDependencies.testing_mockwebserver
testImplementation ComponentsDependencies.testing_mockwebserver
testImplementation project(':lib-fetch-okhttp')
androidTestImplementation ComponentsDependencies.testing_fastlane
androidTestImplementation ComponentsDependencies.testing_falcon
testImplementation ComponentsDependencies.androidx_test_core
testImplementation ComponentsDependencies.androidx_test_runner
testImplementation ComponentsDependencies.androidx_test_rules
androidTestImplementation ComponentsDependencies.androidx_espresso_core
androidTestImplementation ComponentsDependencies.androidx_espresso_idling_resource
androidTestImplementation ComponentsDependencies.androidx_espresso_intents
androidTestImplementation ComponentsDependencies.androidx_espresso_web
androidTestImplementation ComponentsDependencies.androidx_test_core
androidTestImplementation ComponentsDependencies.androidx_test_junit
androidTestImplementation ComponentsDependencies.androidx_test_monitor
androidTestImplementation ComponentsDependencies.androidx_test_runner
androidTestImplementation ComponentsDependencies.androidx_test_uiautomator
androidTestUtil ComponentsDependencies.androidx_test_orchestrator
lintChecks project(':tooling-lint')
}
// -------------------------------------------------------------------------------------------------
// Dynamically set versionCode (See tools/build/versionCode.gradle
// -------------------------------------------------------------------------------------------------
android.applicationVariants.configureEach { variant ->
def buildType = variant.buildType.name
println("----------------------------------------------")
println("Variant name: " + variant.name)
println("Application ID: " + [variant.applicationId, variant.buildType.applicationIdSuffix].findAll().join())
println("Build type: " + variant.buildType.name)
println("Flavor: " + variant.flavorName)
if (buildType == "release" || buildType == "nightly" || buildType == "beta") {
def baseVersionCode = generatedVersionCode
def versionName = buildType == "nightly" ?
"${Config.nightlyVersionName(project)}" :
"${Config.releaseVersionName(project)}"
println("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())
def aab = project.hasProperty("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 (aab) {
// AAB version code is odd
versionCodeOverride = versionCodeOverride + 1
println("versionCode for AAB = $versionCodeOverride")
} 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)
}
println("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 {
print("MLS token: ")
try {
def token = new File("${rootDir}/.mls_token").text.trim()
buildConfigField 'String', 'MLS_TOKEN', '"' + token + '"'
println "(Added from .mls_token file)"
} catch (FileNotFoundException ignored) {
buildConfigField 'String', 'MLS_TOKEN', '""'
println("X_X")
}
}
// -------------------------------------------------------------------------------------------------
// Adjust: Read token from local file if it exists (Only release builds)
// -------------------------------------------------------------------------------------------------
android.applicationVariants.configureEach { variant ->
def variantName = variant.getName()
print("Adjust token: ")
if (variantName.contains("Release") && variantName.contains("focus")) {
try {
def token = new File("${rootDir}/.adjust_token").text.trim()
buildConfigField 'String', 'ADJUST_TOKEN', '"' + token + '"'
println "(Added from .adjust_token file)"
} catch (FileNotFoundException ignored) {
if (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey")) {
buildConfigField 'String', 'ADJUST_TOKEN', '"fake"'
println("fake - only for local development")
} else {
buildConfigField 'String', 'ADJUST_TOKEN', 'null'
println("X_X")
}
}
} else {
buildConfigField 'String', 'ADJUST_TOKEN', 'null'
println("--")
}
}
// -------------------------------------------------------------------------------------------------
// Sentry: Read token from local file if it exists (Only release builds)
// -------------------------------------------------------------------------------------------------
android.applicationVariants.configureEach {
print("Sentry token: ")
try {
def token = new File("${rootDir}/.sentry_token").text.trim()
buildConfigField 'String', 'SENTRY_TOKEN', '"' + token + '"'
println "(Added from .sentry_token file)"
} catch (FileNotFoundException ignored) {
buildConfigField 'String', 'SENTRY_TOKEN', '""'
println("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
// -------------------------------------------------------------------------------------------------
print("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 + '"'
println "(Added from .nimbus file)"
} catch (FileNotFoundException ignored) {
buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null'
println("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 + '"'
println "(Added from local.properties file)"
} else {
buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null'
println("--")
}
}
def generatedLocaleListDir = 'src/main/java/org/mozilla/focus/generated'
def generatedLocaleListFilename = 'LocalesList.kt'
tasks.register('generateLocaleList') {
doLast {
def dir = file(generatedLocaleListDir)
dir.mkdir()
def localeList = file(new File(dir, generatedLocaleListFilename))
localeList.delete()
localeList.createNewFile()
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 << getEnabledLocales().join(",\n" + " ")
localeList << ",\n"
localeList << " )," << "\n"
localeList << " )" << "\n"
localeList << "}" << "\n"
}
}
tasks.configureEach { task ->
if (name.contains("compile")) {
task.dependsOn generateLocaleList
}
}
clean.doLast {
file(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') {
doLast {
def variants = 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,
]}
// AndroidTest is a special case not included above
variants.add([
apks: [[
abi: 'noarch',
fileName: 'app-debug-androidTest.apk',
]],
build_type: 'androidTest',
name: 'androidTest',
])
println 'variants: ' + JsonOutput.toJson(variants)
}
}
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 = []
def out = services.get(StyledTextOutputFactory).create("tests")
beforeSuite { descriptor ->
if (descriptor.getClassName() != null) {
out.style(Style.Header).println("\nSUITE: " + descriptor.getClassName())
}
}
beforeTest { descriptor ->
out.style(Style.Description).println(" TEST: " + descriptor.getName())
}
onOutput { descriptor, event ->
logger.lifecycle(" " + event.message.trim())
}
afterTest { descriptor, result ->
switch (result.getResultType()) {
case ResultType.SUCCESS:
out.style(Style.Success).println(" SUCCESS")
break
case ResultType.FAILURE:
def testId = descriptor.getClassName() + "." + descriptor.getName()
out.style(Style.Failure).println(" TEST-UNEXPECTED-FAIL | " + testId + " | " + result.getException())
break
case ResultType.SKIPPED:
out.style(Style.Info).println(" SKIPPED")
break
}
logger.lifecycle("")
}
}
}