plugins { alias(libs.plugins.dependency.analysis) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.python.envs.plugin) id 'org.mozilla.conventions.zip-test-reports' id 'org.mozilla.nimbus-gradle-plugin' } apply plugin: 'com.android.application' apply plugin: 'kotlin-parcelize' apply plugin: 'jacoco' 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 // Generated sources location for LocalesList.kt def generatedLocaleListBaseDir = project.layout.buildDirectory.dir("generated/source/locales").get().asFile def generatedLocaleListDir = new File(generatedLocaleListBaseDir, "org/mozilla/focus/generated") def generatedLocaleListFilename = "LocalesList.kt" import com.android.build.api.variant.FilterConfiguration import com.google.android.gms.oss.licenses.plugin.DependencyTask import groovy.json.JsonOutput 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") } 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 } } // Exclude native libraries for unsupported architectures from third-party dependencies packagingOptions { jniLibs { excludes += ["**/armeabi/*.so", "**/mips/*.so", "**/mips64/*.so", "**/x86/*.so"] } } lint { lintConfig = file("lint.xml") baseline = file("lint-baseline.xml") sarifReport = true sarifOutput = file("../build/reports/lint/lint-report.sarif.json") abortOnError = false } // 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/" } } main { java.srcDir generatedLocaleListBaseDir } // 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" 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-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.ktx 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.preferences 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.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.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 // ------------------------------------------------------------------------------------------------- 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()) 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 enabledLocales = project.provider { getEnabledLocales() } tasks.register('generateLocaleList') { def localeList = file(new File(generatedLocaleListDir, generatedLocaleListFilename)) inputs.property("enabledLocales", enabledLocales) outputs.file(localeList) 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 << " * 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" } } tasks.configureEach { task -> if (task.name.contains("compile")) { task.dependsOn(tasks.named("generateLocaleList")) } } // Make sure DAGP code-source exploding happens after we generate LocalesList.kt tasks.matching { it.name.startsWith("explodeCodeSource") }.configureEach { t -> t.dependsOn(tasks.named("generateLocaleList")) } 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, ]}} def outputFile = new File(projectDir, "build/printVariants.json") doLast { // AndroidTest is a special case not included above variants.get().add([ apks: [[ abi: 'noarch', fileName: 'app-debug-androidTest.apk', ]], build_type: 'androidTest', name: 'androidTest', ]) def json = JsonOutput.toJson(variants.get()) outputFile.parentFile.mkdirs() outputFile.text = json logger.debug("Wrote variant info to $outputFile") } } // Fix for Gradle 9 strictness: Ensure CleanUp runs before DependencyTask tasks.withType(DependencyTask).configureEach { task -> // The OSS plugin creates a "LicensesCleanUp" task for every "DependencyTask" // e.g., nightlyOssDependencyTask -> nightlyOssLicensesCleanUp def cleanUpTaskName = task.name.replace("DependencyTask", "LicensesCleanUp") // make the dependency task depend on its cleanup task. task.dependsOn(tasks.named(cleanUpTaskName)) } 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("") } } } tasks.register("ktlint", Exec) { group = "verification" description = "Check Kotlin code style." workingDir file("${gradle.mozconfig.topsrcdir}") commandLine "./mach", "gradle", "-p", "mobile/android/focus-android", ":ktlint" } tasks.register("ktlintFormat", Exec) { group = "formatting" description = "Fix Kotlin code style deviations." workingDir file("${gradle.mozconfig.topsrcdir}") commandLine "./mach", "gradle", "-p", "mobile/android/focus-android", ":ktlintFormat" } 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" }