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.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.getVcsHash(project)}\"" } 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 = libs.versions.compose.compiler.get() } 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 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.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(':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 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 libs.junit.api testImplementation libs.junit.params testRuntimeOnly libs.junit.engine 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(':support-test') testImplementation project(':support-test-libstate') androidTestImplementation libs.testing.mockwebserver testImplementation libs.testing.mockwebserver testImplementation project(':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(':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 // 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.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 project.logger.debug("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) } 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 << "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" } } 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("") } } }