/* * Copyright (c) 2017 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ import com.android.build.api.dsl.ApplicationBuildType import com.android.build.api.dsl.VariantDimension import com.android.build.gradle.internal.dsl.BaseFlavor import configuration.EnvironmentConfigSettings import groovy.json.JsonOutput import groovy.json.StringEscapeUtils import io.gitlab.arturbosch.detekt.Detekt import org.gradle.internal.extensibility.DefaultExtraPropertiesExtension buildscript { repositories { maven { url "https://plugins.gradle.org/m2/" } mavenCentral() } } plugins { alias(libs.plugins.proton.vpn.android.application) alias(libs.plugins.android.compose.screenshot) alias(libs.plugins.androidx.baselineprofile) alias(libs.plugins.androidx.room) alias(libs.plugins.dagger.hilt) alias(libs.plugins.detekt) alias(libs.plugins.jacoco) alias(libs.plugins.jaredsburrows.gradle.license) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) alias(libs.plugins.proton.core.env.config) alias(libs.plugins.sentry) alias(libs.plugins.triplet.play) } dependencyCheck { formats = ["SARIF", "HTML", "JUNIT"] } jacoco { toolVersion = libs.versions.jacoco.tools.version.get() } // There is no dependency on unit tests because coverage is run in a separate CI job and we want to // avoid building and running the tests again. tasks.register("jacocoTestReport", JacocoReport) { reports { xml.required = true html.required = true } def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'] def debugTree = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/blackGooglePlayStoreDebug", excludes: fileFilter) def mainSrc = files(["src/main/java","src/google/java"]) sourceDirectories.setFrom files([mainSrc]) classDirectories.setFrom files([debugTree]) executionData.setFrom fileTree(dir: "$project.projectDir", includes: [ "**/*.exec", "**/*.ec" ]) } tasks.register("coverageReport") { dependsOn jacocoTestReport def reportFile = project.file("build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml") inputs.file(reportFile) doLast { def slurper = new XmlSlurper() slurper.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false) slurper.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) def xml = slurper.parse(reportFile) def counter = xml.counter.find { node -> node.@type == 'INSTRUCTION' } def missed = counter.@missed.toDouble() def covered = counter.@covered.toDouble() def total = missed + covered def percentage = covered / total * 100 printf "Missed %.2f branches%n", missed printf "Covered %.2f branches%n", covered printf "Total coverage: %.2f%%%n", percentage } } tasks.withType(Test).configureEach { jacoco.includeNoLocationClasses = true jacoco.excludes = ['jdk.internal.*'] } tasks.register("updateLicensesJson") { dependsOn 'licenseProductionGooglePlayStoreReleaseReport' doLast { File json = new File("$project.projectDir/build/reports/licenses/licenseProductionGooglePlayStoreReleaseReport.json") File out = new File("$project.projectDir/src/main/assets/oss_licenses/generated_licenses.js") out.text = 'dependencies = ' + json.text } } licenseReport { generateCsvReport = false generateHtmlReport = false generateJsonReport = true copyHtmlReportToAssets = false copyJsonReportToAssets = false } def sentryDsn = System.getenv("PROTONVPN_SENTRY_DSN") ?: project.hasProperty('sentryDsn') ? sentryDsn : "" def accountSentryDsn = System.getenv("ACCOUNT_SENTRY_DSN") ?: project.hasProperty('accountSentryDsn') ? accountSentryDsn : "" def preferencesSalt = System.getenv("DEPRECATED_PREF_SALT") ?: "Salt" def preferencesKey = System.getenv("DEPRECATED_PREF_KEY") ?: "Key" def serviceAccountCredentialsPath = project.hasProperty('serviceAccountFilePath') ? serviceAccountFilePath : "service_account.json" def testAccountPassword = System.getenv("TEST_ACCOUNT_PASS") ?: project.hasProperty('testAccountPassword') ? testAccountPassword : "Pass" def appId = project.hasProperty('appId') ? appId : "ch.protonvpn.android" def supportedLocales = ['ar', 'b+es+419', 'be', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es-rES', 'es-rMX', 'fa', 'fi', 'fil', 'fr', 'hu', 'in', 'it', 'ja', 'ka', 'ko', 'nb-rNO', 'nl', 'pl', 'pt-rBR', 'pt-rPT', 'ro', 'ru', 'sk', 'sl', 'sv-rSE', 'th', 'tr', 'uk', 'vi', 'zh-rCN', 'zh-rTW'] def helpers = new Helpers(rootDir, providers) def debugKeystorePath = System.getenv("DEBUG_KEYSTORE_FILE") def mavenCacheUrl = System.getenv("MAVEN_CACHE_PKG_URL") def suppressKotlinWarnings = project.hasProperty('suppressWarnings') ? project['suppressWarnings'] : false android { namespace "com.protonvpn.android" testNamespace 'com.protonvpn' useLibrary 'org.apache.http.legacy' signingConfigs { if (debugKeystorePath) { debug { storeFile file(debugKeystorePath) storePassword 'android' keyAlias 'androiddebugkey' keyPassword 'android' } } } buildFeatures { buildConfig true compose true viewBinding true } defaultConfig { applicationId appId multiDexEnabled true versionName helpers.fullVersionName versionCode helpers.getVersionCode() archivesBaseName = helpers.archivesBaseName testInstrumentationRunner "com.protonvpn.TestsRunner" testInstrumentationRunnerArguments clearPackageData: 'true' // Clear app data between tests. testInstrumentationRunnerArguments useTestStorageService: 'true' vectorDrawables.useSupportLibrary = true resourceConfigurations = supportedLocales manifestPlaceholders = ["gwpAsanMode": "default"] def availableLocalesJava = supportedLocales.collect { "\"$it\"" }.join(", ") buildConfigField "String[]", "SUPPORTED_LOCALES", "{$availableLocalesJava}" buildConfigField "String", "Sentry_DSN", "\"${StringEscapeUtils.escapeJava(sentryDsn)}\"" buildConfigField "String", "ACCOUNT_SENTRY_DSN", "\"${StringEscapeUtils.escapeJava(accountSentryDsn)}\"" buildConfigField "String", "PREF_SALT", "\"${StringEscapeUtils.escapeJava(preferencesSalt)}\"" buildConfigField "String", "PREF_KEY", "\"${StringEscapeUtils.escapeJava(preferencesKey)}\"" buildConfigField "String", "TEST_ACCOUNT_PASSWORD", "\"Pass\"" def ciBranchName = System.getenv("CI_COMMIT_BRANCH") def commitFirstLine = System.getenv("CI_COMMIT_MESSAGE")?.split('\n')?.first()?.trim() def specialCharPass = System.getenv("SPECIAL_CHAR_PASSWORD") buildConfigField "String", "SPECIAL_CHAR_PASSWORD", "\"${StringEscapeUtils.escapeJava(specialCharPass)}\"" buildConfigField "String", "CI_BRANCH_NAME", "\"${StringEscapeUtils.escapeJava(ciBranchName)}\"" buildConfigField "String", "CI_COMMIT_MESSAGE", "\"${StringEscapeUtils.escapeJava(commitFirstLine)}\"" buildConfigField "String", "TEST_ASSET_OVERRIDE_SHA", "\"${helpers.readFileOrDefault("test_asset_override_sha", "db95210d57b90063bfa1cac9881c7a8df326e6e2")}\"" buildConfigField "String", "TEST_ASSET_OVERRIDE_KEY", "\"${helpers.readFileOrDefault("test_asset_override_key", "241b080fb0f1216e")}\"" buildConfigField "String", "TEST_SUITE_ASSET_OVERRIDE_KEY", "\"${helpers.readFileOrDefault("test_suite_asset_override_key", "3ea60f6a2fbfc015")}\"" buildConfigField "Boolean", "ALT_ROUTING_CERT_FOR_MAIN_ROUTE", "false" buildConfigField "Boolean", "ALLOW_LOGCAT", "false" buildConfigField "String[]", "API_TLS_PINS", "null" buildConfigField "String[]", "API_ALT_TLS_PINS", "null" buildConfigField "String[]", "DOH_SERVICES_URLS", "null" buildConfigField "String", "VPN_SERVER_ROOT_CERT", "null" Helpers.setAssetLinksResValue(it, "proton.me") } flavorDimensions = ["environment", "functionality", "distribution"] productFlavors { production { dimension "environment" getIsDefault().set(true) helpers.protonEnvironmentConfig(it) { // default host is 'proton.me' apiPrefix = 'vpn-api' } } black { dimension "environment" versionNameSuffix '-dev' def blackToken = System.getenv("BLACK_TOKEN") // Use either: // - TEST_ENV_DOMAIN for regular test environments, // - the BTI_* variables for the BTI environment. def testEnvHost = System.getenv("TEST_ENV_DOMAIN") def dohUrls, apiPinsString, altPinsString, altRoutingCertForMainRoute, customApiHost, customPrefix, customHv3Host if (testEnvHost != null) { dohUrls = null // Disable cert pinning: apiPinsString = "{}" altPinsString = "{}" altRoutingCertForMainRoute = false customApiHost = testEnvHost } else { def apiPins = System.getenv("BTI_API_TLS_PINNINGS") def altPins = System.getenv("BTI_ALT_ROUTE_TLS_PINNINGS") dohUrls = System.getenv("BTI_AR_DOH_URLS") apiPinsString = "{${helpers.sanitizeStringListForBuildConfig(apiPins)}}" altPinsString = altPins != null ? "{${helpers.sanitizeStringListForBuildConfig(altPins)}}" : null altRoutingCertForMainRoute = true customPrefix = System.getenv("BTI_PREFIX") customApiHost = System.getenv("BTI_HOST") customHv3Host = System.getenv("BTI_HV3_DOMAIN") } helpers.protonEnvironmentConfig(it) { if (customApiHost != null) host = StringEscapeUtils.escapeJava(customApiHost) if (customPrefix != null) apiPrefix = StringEscapeUtils.escapeJava(customPrefix) if (customHv3Host != null) hv3Host = StringEscapeUtils.escapeJava(customHv3Host) proxyToken = StringEscapeUtils.escapeJava(blackToken) } if (customApiHost != null) { Helpers.setAssetLinksResValue(it, customApiHost) } def vpnServerRootCert = System.getenv("BTI_SERVERS_ROOT_CERT") buildConfigField "Boolean", "ALT_ROUTING_CERT_FOR_MAIN_ROUTE", "$altRoutingCertForMainRoute" buildConfigField "String[]", "DOH_SERVICES_URLS", "{${helpers.sanitizeStringListForBuildConfig(dohUrls)}}" buildConfigField "String[]", "API_TLS_PINS", apiPinsString // Below are not mandatory if (altPinsString != null) { buildConfigField "String[]", "API_ALT_TLS_PINS", altPinsString } if (vpnServerRootCert != null) { buildConfigField "String", "VPN_SERVER_ROOT_CERT", "\"${StringEscapeUtils.escapeJava(vpnServerRootCert)}\"" } } def additionalProguard = "proguard-rules-logcat.pro" vanilla { dimension "functionality" } google { dimension "functionality" getIsDefault().set(true) } playStore { dimension "distribution" getIsDefault().set(true) buildConfigField "String", "STORE_SUFFIX", "\"+play\"" proguardFile additionalProguard } amazon { dimension "distribution" minSdkVersion rootProject.ext.minSdkVersionAmazon buildConfigField "String", "STORE_SUFFIX", "\"+aws\"" proguardFile additionalProguard } direct { dimension "distribution" buildConfigField "String", "STORE_SUFFIX", "\"+apk\"" proguardFile additionalProguard } openSource { dimension "distribution" buildConfigField "String", "STORE_SUFFIX", "\"+os\"" proguardFile additionalProguard } dev { dimension "distribution" applicationIdSuffix '.dev' buildConfigField "String", "STORE_SUFFIX", "\"\"" buildConfigField "Boolean", "ALLOW_LOGCAT", "true" manifestPlaceholders = ["gwpAsanMode": "always"] } } variantFilter { variant -> def environment = variant.flavors[0].name def functionality = variant.flavors[1].name def dist = variant.flavors[2].name if (dist == "dev") return if (environment == "black" && dist !in ["playStore", "direct"]) setIgnore(true) // We don't need all distributions for testing. if (functionality == "google" && dist != "playStore" || functionality != "google" && dist == "playStore") setIgnore(true) } buildTypes { debug { enableAndroidTestCoverage false enableUnitTestCoverage = true ext.alwaysUpdateBuildId = false minifyEnabled false signingConfig signingConfigs.debug proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' buildConfigField "String", "TEST_ACCOUNT_PASSWORD", "\"${StringEscapeUtils.escapeJava(testAccountPassword)}\"" manifestPlaceholders = ["gwpAsanMode": "always"] } release { minifyEnabled true shrinkResources true signingConfig signingConfigs.debug proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } testOptions { execution 'ANDROIDX_TEST_ORCHESTRATOR' animationsDisabled true unitTests { includeAndroidResources = true } unitTests.all { if (mavenCacheUrl != null) { systemProperty 'robolectric.dependency.repo.url', mavenCacheUrl } } } packagingOptions { resources { excludes += [ 'DebugProbesKt.bin', 'META-INF/DEPENDENCIES.txt', 'META-INF/NOTICE.txt', 'META-INF/NOTICE.md', 'META-INF/LICENSE*', 'META-INF/DEPENDENCIES', 'META-INF/notice.txt', 'META-INF/license.txt', 'META-INF/dependencies.txt', 'META-INF/LGPL2.1', 'META-INF/AL2.0', 'MANIFEST.MF' ] } jniLibs { useLegacyPackaging = true keepDebugSymbols.add("**/libandroidvpnrust.so") } } experimentalProperties["android.experimental.enableScreenshotTest"] = true sourceSets { main.res.srcDirs += 'src/main/res_flags' main { assets.srcDirs = ['src/main/assets', 'assets'] } // Screenshot test does not automatically include app resources. Check if it's still needed when updating screenshot test module. screenshotTest.res.srcDirs += 'src/main/res' // Adds exported schema location as test app assets. androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } compileOptions { coreLibraryDesugaringEnabled = true } kotlinOptions { freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"] suppressWarnings = suppressKotlinWarnings } lint { // In our process we might temporarily have extra translations that will get removed on // next update. disable 'ExtraTranslation', 'ImpliedQuantity', 'MissingDefaultResource', 'MissingTranslation' ignoreWarnings = true } room { schemaDirectory "$projectDir/schemas" // Using Java generation, kotlin generator generates incorrect code: https://issuetracker.google.com/issues/446633192 generateKotlin false } } androidComponents { onVariants(selector().withBuildType("debug")) { packaging.jniLibs.keepDebugSymbols.add("**/*.so") } } play { serviceAccountCredentials.set(file(serviceAccountCredentialsPath)) track.set('internal') artifactDir.set(file("signedArtifacts/")) defaultToAppBundles.set(true) releaseName.set(helpers.fullVersionName) } // The config file is downloaded by the downloadDetektConfig task. def commonDetektConfigPath = "${rootProject.projectDir}/config/detekt/config.yml" def projectDetektConfigPath = "${rootProject.projectDir}/config/detekt/custom-config.yml" detekt { config = files(commonDetektConfigPath, projectDetektConfigPath) reports { html.required.set(false) xml.required.set(false) txt.required.set(false) sarif.required.set(false) custom { reportId = "outputreport.GitlabQualityOutputReport" outputLocation.set(file("build/reports/detekt/detekt.json")) } } } sentry { // This configuration uploads mapping files to Sentry and requires credentials either in a // local file or environment variables. // To disable the upload temporarily (e.g. when building locally) add "release" to // ignoredBuildTypes (I wish there was a better way...). includeProguardMapping = true uploadNativeSymbols = true ignoredBuildTypes = ["debug", "benchmarkRelease", "nonMinifiedRelease"] ignoredFlavors = ["productionVanillaOpenSource"] // We don't want the sentry plugin to add anything to our builds: tracingInstrumentation { enabled = false } autoInstallation { enabled = false } } tasks.withType(Detekt).configureEach { dependsOn "downloadDetektConfig" dependsOn ":detekt-custom-rules:assemble" dependsOn ":detekt-gitlab-output-plugin:assemble" basePath = rootProject.projectDir.absolutePath reports { html.required.set(false) xml.required.set(false) txt.required.set(false) sarif.required.set(false) custom { reportId = "GitlabQualityOutputReport" outputLocation.set(file("build/reports/detekt/detekt.json")) } } } tasks.register("downloadDetektConfig") { doLast { def srcConfigFilePath = "config/detekt/config.yml" def dstConfigFile = file(commonDetektConfigPath) def dir = dstConfigFile.parentFile if (!dir.exists() && !dir.mkdirs()) { throw FilePermissionException("Cannot create directory ${dir.canonicalPath}") } println("Detekt rule-set file: $dstConfigFile") def fileModificationDate = new Date(dstConfigFile.lastModified()) if (fileModificationDate.plus(1) > new Date()) { println("Detekt rule-set is less than a day old, skipping download.") return } def url = "https://raw.githubusercontent.com/ProtonMail/protoncore_android/master/$srcConfigFilePath" println("Fetching Detekt rule-set from $url") try { def content = new URL(url).getText() // Checking start of the file is enough, if some part is missing we would not be able to decode it assert(content.startsWith("# Integrity check *")) : "Integrity check not passed" dstConfigFile.text = content } catch (Throwable t) { println("Cannot download Detekt configuration: ${t.message}") throw t } } } tasks.register("generateReleaseNotesForPublishing") { doLast { File releaseNotesDir = new File("$project.projectDir/src/main/play/release-notes/en-US") releaseNotesDir.mkdirs() File releaseNotesFile = new File(releaseNotesDir, "default.txt") releaseNotesFile.newWriter().withWriter { w -> // Limit of 500 chars on Google Play console for release notes w << helpers.getReleaseNotes(490) } } } tasks.withType(Test).configureEach { testLogging { events "skipped", "failed" exceptionFormat "full" showExceptions true } } class Helpers { private File workingDir private ProviderFactory providers Helpers(File workingDir, ProviderFactory providers) { this.workingDir = workingDir this.providers = providers } // Versioning: // Version name: M.m.D.R // M.m (major.minor) come from the last tag on development branch of this form (e.g. "2.1") // D - number of commits on development since M.m // R - number of commits on release branch (from development) // Version code: AMMmmDDRR as decimal integer // A - hardcoded number 6. Because abi splits were used before int getVersionCode() { def name = fullVersionName def versions = name.split("\\.") def major = versions[0].toInteger() def minor = versions[1].toInteger() def dev = versions[2].toInteger() def release = versions[3].toInteger() // Max version code allowed by android is 2_000_000_000 assert major < 100 && minor < 100 && dev < 100 && release < 100 // 6 needs to be hardcoded as abi version before ended up at 5 def code = 6 * 100_000_000 + major * 1_000_000 + minor*10_000 + dev * 100 + release // Just some sanity check because there's no turning back once we accidentally publish // some large version code assert code < 700_000_000 && major < 50 return code } String getFullVersionName() { // To be used e.g. when we don't want the version on hotfix branch to change when adding // CI-related fixes. def override = System.getenv("OVERRIDE_VERSION_NAME") if (override != null) return override def gitVersionProvider = providers.of(GitFullVersionNameSource.class) { parameters { workingDir = this.workingDir } } return gitVersionProvider.get() } String readFileOrDefault(String path, String defaultValue) { def file = new File(workingDir, path) if (!file.exists()) return defaultValue return file.text } boolean hasReleaseNotes() { def lastSha = System.getenv("CI_COMMIT_BEFORE_SHA") def branchName = System.getenv("CI_COMMIT_BRANCH") def output = exec(["git", "branch", branchName, "--contains", lastSha], false) return output != null } String getReleaseNotes(int trimAt) { def lastSha = System.getenv("CI_COMMIT_BEFORE_SHA") // CI_COMMIT_BEFORE_SHA is always 0000000000000000000000000000000000000000 for merge request // pipelines, the first commit in pipelines for branches or tags, or when manually running a // pipeline. Let's not fail the job if we ever need to run a pipeline manually to fix // publishing. if (lastSha == null || lastSha == "0000000000000000000000000000000000000000") return "(failed fetching release notes for manual pipeline)" def notes = exec(["git", "log", "${lastSha}..HEAD", "--pretty=format:- %s"]) if (notes.length() > trimAt) return notes.take(trimAt) + "\n..." return notes } void notifyPublishOnSlack() { def hook = System.getenv("SLACK_PUBLISH_HOOK") if (hook == null) return def sha = System.getenv("CI_COMMIT_SHA") def json = new JsonOutput().toJson(["text": ":android: :protonvpn: ${getFullVersionName()} released to internal (SHA ${sha.take(8)}...)\n" + "Release notes:\n" + "${getReleaseNotes(1000)}"]) exec(["curl", "-X", "POST", "-H", "'Content-type: application/json'", "--data", json, hook]) } String getArchivesBaseName() { return "ProtonVPN-" + fullVersionName + "(" + getVersionCode() + ")" } String exec(ArrayList cmd, boolean throwOnError = true) { def out = new StringBuffer() def err = new StringBuffer() def proc = cmd.execute(null, workingDir) proc.waitForProcessOutput(out, err) if (proc.exitValue() != 0) { if (throwOnError) throw new GradleScriptException("Error executing: ${cmd}", new RuntimeException(err.toString())) else return null } return out.toString() } // We receive comma-separated string lists either with or without '"', support both notations // and protects from injecting java code. static String sanitizeStringListForBuildConfig(String s) { if (s == null) return "" return s.split(",") .collect { it.trim().replace("\"", "") } .collect { "\"${StringEscapeUtils.escapeJava(it)}\""} .join(", ") } static void setAssetLinksResValue(VariantDimension variantDimension, String host) { variantDimension.resValue( /* type */ "string", /* name */ "asset_statements", /* value */ """ [{ "relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"], "target": { "namespace": "web", "site": "https://$host" } }] """.stripIndent() ) } static void protonEnvironmentConfig(BaseFlavor flavor, Closure configurator) { setEnvironmentConfigExt(flavor.ext, flavor.name, configurator) } static void protonEnvironmentConfig(ApplicationBuildType buildType, Closure configurator) { setEnvironmentConfigExt(buildType.ext, buildType.name, configurator) } private static void setEnvironmentConfigExt( DefaultExtraPropertiesExtension ext, String name, Closure configurator ) { def configSettings = new EnvironmentConfigSettings() configurator.delegate = configSettings configurator.resolveStrategy = Closure.DELEGATE_FIRST configurator.call() ext.set("""${name}ProtonEnvironmentConfig""", configSettings) } } tasks.register("getVersionName"){ doLast { println helpers.fullVersionName } } tasks.register("notifyPublishOnSlack"){ doLast { helpers.notifyPublishOnSlack() } } tasks.register("getVersionCode"){ doLast { println helpers.getVersionCode() } } tasks.register("getArchivesName"){ doLast { println archivesBaseName } } tasks.register("getReleaseNotes"){ doLast { if (helpers.hasReleaseNotes()) { println helpers.getReleaseNotes(1000) } else { println "- Branch rebased" } } } tasks.register("getExportArchiveNameProperties") { doLast { println archivesBaseName println helpers.fullVersionName println helpers.getVersionCode() } } repositories { maven { url "https://clojars.org/repo/" } google() mavenCentral() } configurations { detekt } configurations.configureEach { exclude group: "me.proton.core", module: "notification-dagger" exclude group: "me.proton.crypto", module: "android-golib" } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) googleImplementation(libs.android.installreferrer) implementation(libs.androidx.profileinstaller) baselineProfile(projects.baselineprofile) debugImplementation(libs.squareup.leakcanary.object.watcher) debugImplementation(libs.squareup.leakcanary) detektPlugins(libs.detekt.formatting) detektPlugins(projects.detektGitlabOutputPlugin) detektPlugins(projects.detektCustomRules) coreLibraryDesugaring(libs.android.tools.desugar.jdk.libs) implementation(platform(libs.kotlin.bom)) implementation(libs.dnsjava.dnsjava) implementation(libs.java.jna) { artifact { type = "aar" } } testImplementation(libs.java.jna) { artifact { type = "jar" } } implementation(libs.commons.codec) implementation(libs.android.material) implementation(libs.androidx.browser) implementation(libs.androidx.recyclerview) implementation(libs.androidx.core.ktx) implementation(libs.androidx.datastore) implementation(libs.androidx.lifecycle.livedata.ktx) implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.activity.ktx) implementation(libs.androidx.fragment.ktx) implementation(libs.androidx.fragment.compose) implementation(libs.androidx.window) implementation(libs.sentry.android) implementation(libs.androidx.legacy.support.v4) implementation(libs.androidx.vectordrawable) implementation(libs.androidx.constraintlayout) implementation(libs.scottyab.aes.crypto) implementation(libs.retrofit) implementation(libs.relex.circleindicator) { artifact { type = "aar" } } implementation(libs.androidx.work.runtime) implementation(libs.androidx.work.runtime.ktx) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.cbor) implementation(libs.tony19.logback) implementation(libs.slf4j.api) implementation(libs.androidx.preference.ktx) implementation(libs.airbnb.lottie) implementation(libs.airbnb.lottie.compose) implementation(libs.android.flexbox) implementation(libs.androidx.viewpager2) implementation(libs.androidx.core.splashscreen) implementation(libs.seancfoley.ipaddress) implementation(libs.caverock.androidsvg) // Compose implementation(platform(libs.androidx.compose.bom)) androidTestImplementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.animation.android) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3.windowsizeclass) implementation(libs.androidx.compose.runtime.livedata) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.viewbinding) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.navigation.compose) implementation(libs.calvin.reorderable) implementation(libs.androidx.constraintlayout.compose) implementation(libs.vico.core) implementation(libs.vico.compose) implementation(libs.androidx.compose.ui.tooling.preview) debugImplementation(libs.androidx.compose.ui.tooling) screenshotTestImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) screenshotTestImplementation(libs.androidx.compose.screenshot.validation.api) { // We don't want this library to force newer versions of the libraries, exclude them. exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jdk8" } // This dependency is included transitively by libs.androidx.compose.screenshot.validation.api // and can be removed once we no longer need to exclude kotlin-stdlib-jdk8. screenshotTestImplementation(libs.androidx.compose.screenshot.validation.junit.engine) { // We don't want this library to force newer versions of the libraries, exclude them. exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jdk8" } testImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.androidx.compose.ui.test.junit4) testImplementation(libs.androidx.navigation.testing) androidTestImplementation(libs.androidx.navigation.testing) // Play core googleImplementation(libs.google.play.app.update) googleImplementation(libs.google.play.app.update.ktx) googleImplementation(libs.google.play.review) googleImplementation(libs.google.play.review.ktx) // Glide implementation(libs.glide) implementation(libs.glide.compose) ksp(libs.glide.ksp) // Widget implementation(libs.androidx.glance.appwidget) implementation(libs.androidx.glance.material3) implementation(libs.androidx.glance.preview) implementation(libs.androidx.glance.appwidget.preview) testImplementation(libs.androidx.glance.testing) testImplementation(libs.androidx.glance.appwidget.testing) // Room implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) annotationProcessor(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler) testImplementation(libs.androidx.room.testing) //TODO: remove when updated in core implementation(libs.androidx.tv.foundation) implementation(libs.androidx.tv.material) // Proton libs implementation(libs.proton.vpn.core.internal) implementation(libs.proton.vpn.govpnlib) implementation(libs.proton.core.domain) implementation(libs.proton.core.network.data) implementation(libs.proton.core.network.domain) implementation(libs.proton.core.network.presentation) implementation(libs.proton.core.notification.data) implementation(libs.proton.core.notification.domain) implementation(libs.proton.core.notification.presentation) implementation(libs.proton.core.util.kotlin) implementation(libs.proton.core.presentation) implementation(libs.proton.core.presentation.compose) implementation(libs.proton.core.presentation.compose.tv) implementation(libs.proton.core.push) implementation(libs.proton.core.human.verification.data) implementation(libs.proton.core.human.verification.domain) implementation(libs.proton.core.human.verification.presentation) implementation(libs.proton.core.country) implementation(libs.proton.core.user) implementation(libs.proton.core.user.data) implementation(libs.proton.core.user.recovery) implementation(libs.proton.core.user.settings) implementation(libs.proton.core.account) implementation(libs.proton.core.account.manager.data) implementation(libs.proton.core.account.manager.domain) implementation(libs.proton.core.account.manager.presentation) implementation(libs.proton.core.account.manager.presentation.compose) implementation(libs.proton.core.account.recovery) implementation(libs.proton.core.auth) implementation(libs.proton.core.auth.fido.dagger) implementation(libs.proton.core.auth.fido.domain) googleImplementation(libs.proton.core.auth.fido.play) implementation(libs.proton.core.challenge) implementation(libs.proton.core.crypto) implementation(libs.proton.core.crypto.validator) implementation(libs.proton.core.event.manager) implementation(libs.proton.core.feature.flag) implementation(libs.proton.core.observability) implementation(libs.proton.core.proguard.rules) implementation(libs.proton.core.telemetry.data) implementation(libs.proton.core.telemetry.domain) implementation(libs.proton.core.payment) implementation(libs.proton.core.plan) implementation(libs.proton.core.key) implementation(libs.proton.core.data) implementation(libs.proton.core.data.room) implementation(libs.proton.core.util.android.dagger) implementation(libs.proton.core.util.android.datetime) implementation(libs.proton.core.util.android.sentry) implementation(libs.proton.core.util.android.shared.preferences) implementation(libs.proton.core.configuration.data) implementation(libs.proton.core.biometric) implementation(libs.proton.core.device.migration) implementation(libs.proton.core.pass.validator) // Payments implementation(libs.proton.payment) googleImplementation(libs.proton.payment.billing.google) testImplementation(testFixtures(libs.proton.payment)) // Proton configuration debugImplementation(libs.proton.core.configuration.dagger.content.resolver) releaseImplementation(libs.proton.core.configuration.dagger.staticdefaults) testImplementation(libs.proton.core.auth.test) androidTestImplementation(libs.proton.core.account.recovery.test) androidTestImplementation(libs.proton.core.auth.test) androidTestImplementation(libs.proton.core.human.verification.test) androidTestImplementation(libs.proton.core.notification.test) androidTestImplementation(libs.proton.core.plan.test) androidTestImplementation(libs.proton.core.test.quark) androidTestImplementation(libs.proton.core.user.recovery.test) testImplementation(libs.proton.core.test.kotlin) { // https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-debug#debug-agent-and-android exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" } androidTestImplementation(libs.proton.core.test.kotlin) { // https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-debug#debug-agent-and-android exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" // Use mockk-android from our own dependencies. exclude group: "io.mockk", module: "mockk" } androidTestImplementation(libs.proton.core.test.android.instrumented) { exclude group: "me.proton.core" } // Observability implementation(projects.observability.domain) // TV implementation(libs.androidx.leanback) implementation(libs.androidx.leanback.preference) // Wireguard implementation(libs.proton.vpn.wireguard) // Hilt ksp(libs.dagger.hilt.compiler) implementation(libs.dagger.hilt.android) testImplementation(libs.dagger.hilt.android.testing) androidTestImplementation(libs.dagger.hilt.android.testing) kspAndroidTest(libs.dagger.hilt.android.compiler) // Hilt for WorkManager implementation(libs.androidx.hilt.work) ksp(libs.androidx.hilt.compiler) testImplementation(libs.junit) testImplementation(libs.mockk) testImplementation(libs.turbine) testImplementation(libs.robolectric) androidTestImplementation(libs.mockk.android) androidTestImplementation(libs.kotlinx.coroutines.test) { // https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-debug#debug-agent-and-android exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" } androidTestImplementation(libs.androidx.lifecycle.runtime.testing) // Core library testImplementation(libs.androidx.arch.core.testing) androidTestImplementation(libs.androidx.arch.core.testing) testImplementation(libs.kotlinx.coroutines.test) // debugImplementation as a workaround for ActivityScenario problem: // https://github.com/android/android-test/issues/940#issuecomment-934406022 debugImplementation(libs.androidx.test.core) testImplementation(projects.sharedTestCode) androidTestImplementation(projects.sharedTestCode) androidTestImplementation(libs.okhttp3.tls) // AndroidJUnitRunner and JUnit Rules androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.test.rules) androidTestUtil(libs.androidx.test.orchestrator) androidTestUtil(libs.androidx.test.services) // Assertions androidTestImplementation(libs.androidx.test.junit) androidTestImplementation(libs.androidx.test.truth) androidTestImplementation(libs.google.truth) testImplementation(libs.kotlin.test) testImplementation(libs.kotlin.test.junit) androidTestImplementation(libs.kotlin.test) androidTestImplementation(libs.kotlin.test.junit) // Espresso dependencies testImplementation(libs.androidx.test.espresso.core) androidTestImplementation(libs.androidx.test.espresso.core) androidTestImplementation(libs.androidx.test.espresso.contrib) androidTestImplementation(libs.androidx.test.espresso.intents) androidTestImplementation(libs.androidx.test.espresso.accessibility) androidTestImplementation(libs.androidx.test.espresso.web) implementation(libs.androidx.test.espresso.idling.resource) androidTestImplementation(libs.androidx.test.espresso.idling.resource) { exclude module: 'support-annotations' } androidTestImplementation(libs.androidx.test.uiautomator) // Fusion testImplementation(libs.proton.test.fusion) androidTestImplementation(libs.proton.test.fusion) }