First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions

29
plugins/build.gradle.kts Normal file
View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
`kotlin-dsl`
`kotlin-dsl-precompiled-script-plugins`
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation(libs.android.gradle.plugin)
implementation(libs.kotlin.gradle.plugin)
implementation(libs.kover.gradle.plugin)
implementation(platform(libs.google.firebase.bom))
implementation(libs.firebase.appdistribution.gradle)
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
implementation(libs.autonomousapps.dependencyanalysis.plugin)
implementation(libs.metro.gradle.plugin)
implementation(libs.ksp.gradle.plugin)
implementation(libs.compose.compiler.plugin)
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
rootProject.name = "ElementX_plugins"
dependencyResolutionManagement {
repositories {
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
import java.io.File
/**
* Are we building with the enterprise sources?
*/
val isEnterpriseBuild = File("enterprise/README.md").exists()

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
import org.gradle.api.logging.Logger
import kotlin.math.max
fun Logger.warnInBox(
text: String,
minBoxWidth: Int = 80,
padding: Int = 4,
) {
val textLength = text.length
val boxWidth = max(textLength + 2, minBoxWidth)
val textPadding = max((boxWidth - textLength) / 2, 1)
warn(
buildString {
append(" ".repeat(padding))
append("")
append("".repeat(boxWidth))
append("")
}
)
warn(
buildString {
append(" ".repeat(padding))
append("")
append(" ".repeat(textPadding))
append(text)
append(" ".repeat(textPadding))
if (textLength % 2 == 1 && boxWidth == minBoxWidth) append(" ")
append("")
}
)
warn(
buildString {
append(" ".repeat(padding))
append("")
append("".repeat(boxWidth))
append("")
}
)
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
import config.AnalyticsConfig
import config.BuildTimeConfig
import config.PushProvidersConfig
object ModulesConfig {
val pushProvidersConfig = PushProvidersConfig(
includeFirebase = BuildTimeConfig.PUSH_CONFIG_INCLUDE_FIREBASE,
includeUnifiedPush = BuildTimeConfig.PUSH_CONFIG_INCLUDE_UNIFIED_PUSH,
)
val analyticsConfig: AnalyticsConfig = if (isEnterpriseBuild) {
// Is Posthog configuration available?
val withPosthog = BuildTimeConfig.SERVICES_POSTHOG_APIKEY.isNullOrEmpty().not() &&
BuildTimeConfig.SERVICES_POSTHOG_HOST.isNullOrEmpty().not()
// Is Sentry configuration available?
val withSentry = BuildTimeConfig.SERVICES_SENTRY_DSN.isNullOrEmpty().not()
if (withPosthog || withSentry) {
println("Analytics enabled with Posthog: $withPosthog, Sentry: $withSentry")
AnalyticsConfig.Enabled(
withPosthog = withPosthog,
withSentry = withSentry,
)
} else {
println("Analytics disabled")
AnalyticsConfig.Disabled
}
} else {
println("Analytics enabled with Posthog and Sentry")
AnalyticsConfig.Enabled(
withPosthog = true,
withSentry = true,
)
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
import org.gradle.api.JavaVersion
import org.gradle.jvm.toolchain.JavaLanguageVersion
/**
* Version codes are quite sensitive, because there is a mix between bundle and APKs.
* Max versionCode allowed by the PlayStore (for information):
* 2_100_000_000
*
* Also note that the versionCode is multiplied by 10 in app/build.gradle.kts:
* ```
* output.versionCode.set((output.versionCode.orNull ?: 0) * 10 + abiCode)
* ```
* We are using a CalVer-like approach to version the application. The version code is calculated as follows:
* - 2 digits for the year
* - 2 digits for the month
* - 1 (or 2) digits for the release number
* Note that the version codes need to be greater than the ones calculated for the previous releases, so we use
* year on 4 digits for this internal value.
* So for instance, the first release of Jan 2025 will have:
* - the version name: 25.01.0
* - the version code: 20250100a (202_501_00a) where `a` stands for the architecture code
*/
/**
* Year of the version on 2 digits.
* Do not update this value. it is updated by the release script.
*/
private const val versionYear = 25
/**
* Month of the version on 2 digits. Value must be in [1,12].
* Do not update this value. it is updated by the release script.
*/
private const val versionMonth = 12
/**
* Release number in the month. Value must be in [0,99].
* Do not update this value. it is updated by the release script.
*/
private const val versionReleaseNumber = 0
object Versions {
/**
* Base version code that will be set in the Android Manifest.
* The value will be modified at build time to add the ABI code when APK are build.
* AAB will have a ABI code of 0.
* See comment above for the calculation method.
*/
const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber
val VERSION_NAME = "$versionYear.${versionMonth.toString().padStart(2, '0')}.$versionReleaseNumber"
/**
* Compile SDK version. Must be updated when a new Android version is released.
* When updating COMPILE_SDK, please also update BUILD_TOOLS_VERSION.
*/
const val COMPILE_SDK = 36
/**
* Build tools version. Must be kept in sync with COMPILE_SDK.
* The value is used by the release script.
*/
@Suppress("unused")
private const val BUILD_TOOLS_VERSION = "36.0.0"
/**
* Target SDK version. Should be kept up to date with COMPILE_SDK.
*/
const val TARGET_SDK = 36
/**
* Minimum SDK version for FOSS builds.
*/
private const val MIN_SDK_FOSS = 24
/**
* Minimum SDK version for Enterprise builds.
*/
private const val MIN_SDK_ENTERPRISE = 33
/**
* minSdkVersion that will be set in the Android Manifest.
*/
val minSdk = if (isEnterpriseBuild) MIN_SDK_ENTERPRISE else MIN_SDK_FOSS
/**
* Java version used for compilation.
* Update this value when you want to use a newer Java version.
*/
private const val JAVA_VERSION = 21
val javaVersion: JavaVersion = JavaVersion.toVersion(JAVA_VERSION)
val javaLanguageVersion: JavaLanguageVersion = JavaLanguageVersion.of(JAVA_VERSION)
// Perform some checks on the values to avoid releasing with bad values
init {
require(versionMonth in 1..12) { "versionMonth must be in [1,12]" }
require(versionReleaseNumber in 0..99) { "versionReleaseNumber must be in [0,99]" }
require(BUILD_TOOLS_VERSION.startsWith(COMPILE_SDK.toString())) { "When updating COMPILE_SDK, please also update BUILD_TOOLS_VERSION" }
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package config
sealed interface AnalyticsConfig {
data class Enabled(
val withPosthog: Boolean,
val withSentry: Boolean,
) : AnalyticsConfig {
init {
require(withPosthog || withSentry) {
"At least one analytics provider must be enabled"
}
}
}
object Disabled : AnalyticsConfig
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package config
object BuildTimeConfig {
const val APPLICATION_ID = "io.element.android.x"
const val APPLICATION_NAME = "Element X"
const val GOOGLE_APP_ID_RELEASE = "1:912726360885:android:d097de99a4c23d2700427c"
const val GOOGLE_APP_ID_DEBUG = "1:912726360885:android:def0a4e454042e9b00427c"
const val GOOGLE_APP_ID_NIGHTLY = "1:912726360885:android:e17435e0beb0303000427c"
val METADATA_HOST_REVERSED: String? = null
val URL_WEBSITE: String? = null
val URL_LOGO: String? = null
val URL_COPYRIGHT: String? = null
val URL_ACCEPTABLE_USE: String? = null
val URL_PRIVACY: String? = null
val URL_POLICY: String? = null
val SERVICES_MAPTILER_BASE_URL: String? = null
val SERVICES_MAPTILER_APIKEY: String? = null
val SERVICES_MAPTILER_LIGHT_MAPID: String? = null
val SERVICES_MAPTILER_DARK_MAPID: String? = null
val SERVICES_POSTHOG_HOST: String? = null
val SERVICES_POSTHOG_APIKEY: String? = null
val SERVICES_SENTRY_DSN: String? = null
val BUG_REPORT_URL: String? = null
val BUG_REPORT_APP_NAME: String? = null
const val PUSH_CONFIG_INCLUDE_FIREBASE = true
const val PUSH_CONFIG_INCLUDE_UNIFIED_PUSH = true
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package config
data class PushProvidersConfig(
val includeFirebase: Boolean,
val includeUnifiedPush: Boolean,
) {
init {
require(includeFirebase || includeUnifiedPush) {
"At least one push provider must be included"
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package extension
import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
@CacheableTask
abstract class AssetCopyTask : DefaultTask() {
@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFile
abstract val inputFile: RegularFileProperty
@get:Input
abstract val targetFileName: Property<String>
@TaskAction
fun action() {
println("Copying ${inputFile.get()} to ${outputDirectory.get().asFile}/${targetFileName.get()}")
inputFile.get().asFile.copyTo(
target = File(
outputDirectory.get().asFile,
targetFileName.get(),
),
overwrite = true,
)
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package extension
import Versions
import com.android.build.api.dsl.CommonExtension
import isEnterpriseBuild
import org.gradle.api.Project
import java.io.File
fun CommonExtension<*, *, *, *, *, *>.androidConfig(project: Project) {
defaultConfig {
compileSdk = Versions.COMPILE_SDK
minSdk = Versions.minSdk
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
generatedDensities()
}
}
compileOptions {
sourceCompatibility = Versions.javaVersion
targetCompatibility = Versions.javaVersion
}
testOptions {
unitTests.isReturnDefaultValues = true
}
lint {
lintConfig = File("${project.rootDir}/tools/lint/lint.xml")
if (isEnterpriseBuild) {
// Disable check on ObsoleteSdkInt for Enterprise builds
// since the min sdk is higher for Enterprise builds
disable.add("ObsoleteSdkInt")
}
checkDependencies = false
abortOnError = true
ignoreTestSources = true
ignoreTestFixturesSources = true
checkGeneratedSources = false
}
}
fun CommonExtension<*, *, *, *, *, *>.composeConfig() {
buildFeatures {
compose = true
}
packaging {
resources.excludes.apply {
add("META-INF/AL2.0")
add("META-INF/LGPL2.1")
}
}
lint {
// Extra rules for compose
// Disabled until lint stops inspecting generated ksp files...
// error.add("ComposableLambdaParameterNaming")
error.add("ComposableLambdaParameterPosition")
ignoreTestFixturesSources = true
checkGeneratedSources = false
}
}

View File

@@ -0,0 +1,176 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package extension
import ModulesConfig
import config.AnalyticsConfig
import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.artifacts.ExternalModuleDependency
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.kotlin.dsl.DependencyHandlerScope
import org.gradle.kotlin.dsl.closureOf
import org.gradle.kotlin.dsl.project
private fun DependencyHandlerScope.implementation(dependency: Any) = dependencies.add("implementation", dependency)
private fun DependencyHandlerScope.testImplementation(dependency: Any) = dependencies.add("testImplementation", dependency)
private fun DependencyHandlerScope.testReleaseImplementation(dependency: Any) = dependencies.add("testReleaseImplementation", dependency)
internal fun DependencyHandler.implementation(dependency: Any) = add("implementation", dependency)
// Implementation + config block
private fun DependencyHandlerScope.implementation(
dependency: Any,
config: Action<ExternalModuleDependency>
) = dependencies.add("implementation", dependency, closureOf<ExternalModuleDependency> { config.execute(this) })
private fun DependencyHandlerScope.androidTestImplementation(dependency: Any) = dependencies.add("androidTestImplementation", dependency)
private fun DependencyHandlerScope.debugImplementation(dependency: Any) = dependencies.add("debugImplementation", dependency)
private fun DependencyHandlerScope.releaseImplementation(dependency: Any) = dependencies.add("releaseImplementation", dependency)
/**
* Dependencies used for unit tests.
*/
fun DependencyHandlerScope.testCommonDependencies(
libs: LibrariesForLibs,
includeTestComposeView: Boolean = false,
) {
testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.appyx.junit)
testImplementation(libs.test.arch.core)
testImplementation(libs.test.junit)
testImplementation(libs.test.mockk)
testImplementation(libs.test.robolectric)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(project(":tests:testutils"))
if (includeTestComposeView) {
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}
}
/**
* Dependencies used by all the modules
*/
fun DependencyHandlerScope.commonDependencies(libs: LibrariesForLibs) {
implementation(libs.timber)
}
/**
* Dependencies used by all the modules with composable items
*/
fun DependencyHandlerScope.composeDependencies(libs: LibrariesForLibs) {
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
androidTestImplementation(composeBom)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
implementation(libs.kotlinx.collections.immutable)
}
fun DependencyHandlerScope.allLibrariesImpl() {
implementation(project(":libraries:androidutils"))
implementation(project(":libraries:deeplink:impl"))
implementation(project(":libraries:designsystem"))
implementation(project(":libraries:matrix:impl"))
implementation(project(":libraries:matrixui"))
implementation(project(":libraries:matrixmedia:impl"))
implementation(project(":libraries:network"))
implementation(project(":libraries:core"))
implementation(project(":libraries:eventformatter:impl"))
implementation(project(":libraries:indicator:impl"))
implementation(project(":libraries:permissions:impl"))
implementation(project(":libraries:audio:impl"))
implementation(project(":libraries:push:impl"))
implementation(project(":libraries:featureflag:impl"))
implementation(project(":libraries:pushstore:impl"))
implementation(project(":libraries:preferences:impl"))
implementation(project(":libraries:architecture"))
implementation(project(":libraries:dateformatter:impl"))
implementation(project(":libraries:di"))
implementation(project(":libraries:session-storage:impl"))
implementation(project(":libraries:mediapickers:impl"))
implementation(project(":libraries:mediaupload:impl"))
implementation(project(":libraries:usersearch:impl"))
implementation(project(":libraries:textcomposer:impl"))
implementation(project(":libraries:accountselect:impl"))
implementation(project(":libraries:roomselect:impl"))
implementation(project(":libraries:cryptography:impl"))
implementation(project(":libraries:voiceplayer:impl"))
implementation(project(":libraries:voicerecorder:impl"))
implementation(project(":libraries:mediaplayer:impl"))
implementation(project(":libraries:mediaviewer:impl"))
implementation(project(":libraries:troubleshoot:impl"))
implementation(project(":libraries:fullscreenintent:impl"))
implementation(project(":libraries:wellknown:impl"))
implementation(project(":libraries:oidc:impl"))
implementation(project(":libraries:workmanager:impl"))
implementation(project(":libraries:recentemojis:impl"))
}
fun DependencyHandlerScope.allServicesImpl() {
implementation(project(":services:analytics:compose"))
when (ModulesConfig.analyticsConfig) {
AnalyticsConfig.Disabled -> {
implementation(project(":services:analytics:noop"))
}
is AnalyticsConfig.Enabled -> {
implementation(project(":services:analytics:impl"))
if (ModulesConfig.analyticsConfig.withPosthog) {
implementation(project(":services:analyticsproviders:posthog"))
}
if (ModulesConfig.analyticsConfig.withSentry) {
implementation(project(":services:analyticsproviders:sentry"))
}
}
}
implementation(project(":services:apperror:impl"))
implementation(project(":services:appnavstate:impl"))
implementation(project(":services:toolbox:impl"))
}
fun DependencyHandlerScope.allEnterpriseImpl(project: Project) = addAll(
project = project,
modulePrefix = ":enterprise:features",
moduleSuffix = ":impl",
)
fun DependencyHandlerScope.allFeaturesImpl(project: Project) = addAll(
project = project,
modulePrefix = ":features",
moduleSuffix = ":impl",
)
fun DependencyHandlerScope.allFeaturesApi(project: Project) = addAll(
project = project,
modulePrefix = ":features",
moduleSuffix = ":api",
)
private fun DependencyHandlerScope.addAll(
project: Project,
modulePrefix: String,
moduleSuffix: String,
) {
val subProjects = project.rootProject.subprojects.filter { it.path.startsWith(modulePrefix) && it.path.endsWith(moduleSuffix) }
for (p in subProjects) {
add("implementation", p)
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package extension
import dev.zacsweers.metro.gradle.MetroPluginExtension
import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.gradle.kotlin.dsl.the
import org.gradle.plugin.use.PluginDependency
/**
* Setup the Metro plugin with the shared configuration.
* @param generateNodeFactories Whether to set up the KSP plugin and dependencies to generate Appyx Node factories.
*/
fun Project.setupDependencyInjection(
generateNodeFactories: Boolean = shouldApplyAppyxCodegen(),
) {
if (project.path.endsWith(":api")) {
error("api module should not use setupDependencyInjection(). Move the implementation to `:impl` module")
}
val libs = the<LibrariesForLibs>()
// Apply Metro plugin and configure it
applyPluginIfNeeded(libs.plugins.metro)
val metroExtension = extensions.getByName("metro") as MetroPluginExtension
metroExtension.contributesAsInject.value(true)
if (generateNodeFactories) {
applyPluginIfNeeded(libs.plugins.ksp)
// Annotations to generate DI code for Appyx nodes
dependencies.implementation(project.project(":annotations"))
// Code generator for the annotations above
dependencies.add("ksp", project.project(":codegen"))
}
}
// These dependencies should only be needed for compose library or application modules
private fun Project.shouldApplyAppyxCodegen(): Boolean {
return project.pluginManager.hasPlugin("io.element.android-compose-library")
|| project.pluginManager.hasPlugin("io.element.android-compose-application")
}
private fun Project.applyPluginIfNeeded(plugin: Provider<PluginDependency>) {
val pluginId = plugin.get().pluginId
if (!pluginManager.hasPlugin(pluginId)) {
pluginManager.apply(pluginId)
}
}

View File

@@ -0,0 +1,247 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package extension
import kotlinx.kover.gradle.plugin.dsl.AggregationType
import kotlinx.kover.gradle.plugin.dsl.CoverageUnit
import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType
import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension
import kotlinx.kover.gradle.plugin.dsl.KoverVariantCreateConfig
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.assign
enum class KoverVariant(val variantName: String) {
Presenters("presenters"),
States("states"),
Views("views"),
}
val koverVariants = KoverVariant.values().map { it.variantName }
val localAarProjects = listOf(
":libraries:rustsdk",
":libraries:textcomposer:lib"
)
val excludedKoverSubProjects = listOf(
":app",
":annotations",
":codegen",
":tests:testutils",
// Exclude modules which are not Android libraries
// See https://github.com/Kotlin/kotlinx-kover/issues/312
":appconfig",
":libraries:core",
":libraries:coroutines",
":libraries:di",
) + localAarProjects
private fun Project.kover(action: Action<KoverProjectExtension>) {
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("kover", action)
}
fun Project.setupKover() {
// Create verify all task joining all existing verification tasks
tasks.register("koverVerifyAll") {
group = "verification"
description = "Verifies the code coverage of all subprojects."
val dependencies = listOf(":app:koverVerifyGplayDebug") + koverVariants.map { ":app:koverVerify${it.replaceFirstChar(Char::titlecase)}" }
dependsOn(dependencies)
}
// https://kotlin.github.io/kotlinx-kover/
// Run `./gradlew :app:koverHtmlReport` to get report at ./app/build/reports/kover
// Run `./gradlew :app:koverXmlReport` to get XML report
kover {
reports {
filters {
excludes {
classes(
// Exclude generated classes.
"*_Module",
"*_AssistedFactory",
"com.airbnb.android.showkase*",
"io.element.android.libraries.designsystem.showkase.*",
"*ComposableSingletons$*",
"*BuildConfig",
// Generated by Showkase
"*Ioelementandroid*PreviewKt$*",
"*Ioelementandroid*PreviewKt",
// Other
// We do not cover Nodes (normally covered by maestro, but code coverage is not computed with maestro)
"*Node",
"*Node$*",
"*Presenter\$present\$*",
// Forked from compose
"io.element.android.libraries.designsystem.theme.components.bottomsheet.*",
// Konsist code to make test fails
"io.element.android.tests.konsist.failures",
)
annotatedBy(
"androidx.compose.ui.tooling.preview.Preview",
"io.element.android.libraries.architecture.coverage.ExcludeFromCoverage",
"io.element.android.libraries.designsystem.preview.*",
)
}
}
total {
verify {
// General rule: minimum code coverage.
rule("Global minimum code coverage.") {
groupBy = GroupingEntityType.APPLICATION
bound {
minValue = 70
// Setting a max value, so that if coverage is bigger, it means that we have to change minValue.
// For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update
// minValue to 25 and maxValue to 35.
maxValue = 80
coverageUnits = CoverageUnit.INSTRUCTION
aggregationForGroup = AggregationType.COVERED_PERCENTAGE
}
}
}
}
variant(KoverVariant.Presenters.variantName) {
verify {
// Rule to ensure that coverage of Presenters is sufficient.
rule("Check code coverage of presenters") {
groupBy = GroupingEntityType.CLASS
bound {
minValue = 85
coverageUnits = CoverageUnit.INSTRUCTION
aggregationForGroup = AggregationType.COVERED_PERCENTAGE
}
}
}
filters {
excludes.classes(
"*Fake*Presenter*",
"io.element.android.appnav.loggedin.LoggedInPresenter$*",
// Some options can't be tested at the moment
"io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*",
// Need an Activity to use rememberMultiplePermissionsState
"io.element.android.features.location.impl.common.permissions.DefaultPermissionsPresenter",
"*Presenter\$present\$*",
// Too small to be > 85% tested
"io.element.android.libraries.fullscreenintent.impl.DefaultFullScreenIntentPermissionsPresenter",
)
includes.inheritedFrom("io.element.android.libraries.architecture.Presenter")
}
}
variant(KoverVariant.States.variantName) {
verify {
// Rule to ensure that coverage of States is sufficient.
rule("Check code coverage of states") {
groupBy = GroupingEntityType.CLASS
bound {
minValue = 90
coverageUnits = CoverageUnit.INSTRUCTION
aggregationForGroup = AggregationType.COVERED_PERCENTAGE
}
}
}
filters {
excludes.classes(
"*State$*", // Exclude inner classes
"io.element.android.appnav.root.RootNavState",
"io.element.android.features.ftue.api.state.*",
"io.element.android.features.ftue.impl.welcome.state.*",
"io.element.android.features.messages.impl.timeline.model.bubble.BubbleState",
"io.element.android.libraries.designsystem.swipe.SwipeableActionsState",
"io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState",
"io.element.android.libraries.maplibre.compose.CameraPositionState",
"io.element.android.libraries.maplibre.compose.SymbolState",
"io.element.android.libraries.matrix.api.room.RoomMembershipState",
"io.element.android.libraries.matrix.api.room.RoomMembersState",
"io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*",
"io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*",
"io.element.android.libraries.mediaviewer.impl.local.pdf.PdfViewerState",
"io.element.android.libraries.mediaviewer.impl.local.player.MediaPlayerControllerState",
"io.element.android.libraries.textcomposer.model.TextEditorState",
"io.element.android.libraries.textcomposer.components.FormattingOptionState",
)
includes.classes("*State")
}
}
variant(KoverVariant.Views.variantName) {
verify {
// Rule to ensure that coverage of Views is sufficient (deactivated for now).
rule("Check code coverage of views") {
groupBy = GroupingEntityType.CLASS
bound {
// TODO Update this value, for now there are too many missing tests.
minValue = 0
coverageUnits = CoverageUnit.INSTRUCTION
aggregationForGroup = AggregationType.COVERED_PERCENTAGE
}
}
}
filters {
excludes.classes("*ViewKt$*") // Exclude inner classes
includes.classes("*ViewKt")
}
}
}
}
}
fun Project.applyKoverPluginToAllSubProjects() = rootProject.subprojects {
if (project.path !in localAarProjects) {
apply(plugin = "org.jetbrains.kotlinx.kover")
kover {
currentProject {
for (variant in koverVariants) {
createVariant(variant) {
defaultVariants(project)
}
}
}
}
project.afterEvaluate {
for (variant in koverVariants) {
// Using the cache for coverage verification seems to be flaky, so we disable it for now.
val taskName = "koverCachedVerify${variant.replaceFirstChar(Char::titlecase)}"
val cachedTask = project.tasks.findByName(taskName)
cachedTask?.let {
it.outputs.upToDateWhen { false }
}
}
}
}
}
fun KoverVariantCreateConfig.defaultVariants(project: Project) {
if (project.name == "app") {
addWithDependencies("gplayDebug")
} else {
addWithDependencies("debug", "jvm", optional = true)
}
}
fun Project.koverSubprojects() = project.rootProject.subprojects
.filter {
it.project.projectDir.resolve("build.gradle.kts").exists()
}
.map { it.path }
.sorted()
.filter {
it !in excludedKoverSubProjects
}
fun Project.koverDependencies() {
project.koverSubprojects()
.forEach {
// println("Add $it to kover")
dependencies.add("kover", project(it))
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package extension
import org.gradle.api.Project
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.gradle.process.ExecOperations
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.Properties
import javax.inject.Inject
abstract class GitRevisionValueSource : ValueSource<String, ValueSourceParameters.None> {
@get:Inject
abstract val execOperations: ExecOperations
override fun obtain(): String? {
return execOperations.runCommand("git rev-parse --short=8 HEAD")
}
}
abstract class GitBranchNameValueSource : ValueSource<String, ValueSourceParameters.None> {
@get:Inject
abstract val execOperations: ExecOperations
override fun obtain(): String? {
return execOperations.runCommand("git rev-parse --abbrev-ref HEAD")
}
}
private fun ExecOperations.runCommand(cmd: String): String {
val outputStream = ByteArrayOutputStream()
val errorStream = ByteArrayOutputStream()
exec {
commandLine = cmd.split(" ")
standardOutput = outputStream
errorOutput = errorStream
}
if (errorStream.size() > 0) {
println("Error while running command: $cmd")
throw IOException(String(errorStream.toByteArray()))
}
return String(outputStream.toByteArray()).trim()
}
fun Project.readLocalProperty(name: String): String? = Properties().apply {
try {
load(rootProject.file("local.properties").reader())
} catch (ignored: IOException) {
}
}.getProperty(name)

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package extension
import com.android.build.api.dsl.VariantDimension
fun VariantDimension.buildConfigFieldStr(
name: String,
value: String,
) {
buildConfigField(
type = "String",
name = name,
value = "\"$value\""
)
}
fun VariantDimension.buildConfigFieldBoolean(
name: String,
value: Boolean,
) {
buildConfigField(
type = "boolean",
name = name,
value = value.toString()
)
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package extension
import org.gradle.api.artifacts.VersionCatalog
private fun VersionCatalog.getVersion(alias: String) = findVersion(alias).get()
private fun VersionCatalog.getLibrary(library: String) = findLibrary(library).get()
private fun VersionCatalog.getBundle(bundle: String) = findBundle(bundle).get()
private fun VersionCatalog.getPlugin(plugin: String) = findPlugin(plugin).get()

View File

@@ -0,0 +1,42 @@
// File generated by importSupportedLocalesFromLocalazy.py, do not edit
package extension
val locales = setOf(
"be",
"bg",
"cs",
"cy",
"da",
"de",
"el",
"en",
"en-rUS",
"es",
"et",
"eu",
"fa",
"fi",
"fr",
"hu",
"in",
"it",
"ka",
"ko",
"lt",
"nb",
"nl",
"pl",
"pt",
"pt-rBR",
"ro",
"ru",
"sk",
"sv",
"tr",
"uk",
"ur",
"uz",
"zh-rCN",
"zh-rTW",
)

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
/**
* This will generate the plugin "io.element.android-compose-application" to use by app
*/
import extension.androidConfig
import extension.commonDependencies
import extension.composeConfig
import extension.composeDependencies
import org.gradle.accessors.dm.LibrariesForLibs
val libs = the<LibrariesForLibs>()
plugins {
id("com.android.application")
id("kotlin-android")
id("com.autonomousapps.dependency-analysis")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
androidConfig(project)
composeConfig()
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
kotlin {
jvmToolchain {
languageVersion = Versions.javaLanguageVersion
}
}
dependencies {
commonDependencies(libs)
composeDependencies(libs)
coreLibraryDesugaring(libs.android.desugar)
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
/**
* This will generate the plugin "io.element.android-compose-library", used in android library with compose modules.
*/
import extension.androidConfig
import extension.commonDependencies
import extension.composeConfig
import extension.composeDependencies
import org.gradle.accessors.dm.LibrariesForLibs
val libs = the<LibrariesForLibs>()
plugins {
id("com.android.library")
id("kotlin-android")
id("com.autonomousapps.dependency-analysis")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
androidConfig(project)
composeConfig()
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
kotlin {
jvmToolchain {
languageVersion = Versions.javaLanguageVersion
}
}
dependencies {
commonDependencies(libs)
composeDependencies(libs)
coreLibraryDesugaring(libs.android.desugar)
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
/**
* This will generate the plugin "io.element.android-library", used in android library without compose modules.
*/
import extension.androidConfig
import extension.commonDependencies
import org.gradle.accessors.dm.LibrariesForLibs
val libs = the<LibrariesForLibs>()
plugins {
id("com.android.library")
id("kotlin-android")
id("com.autonomousapps.dependency-analysis")
}
android {
androidConfig(project)
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
kotlin {
jvmToolchain {
languageVersion = Versions.javaLanguageVersion
}
}
dependencies {
commonDependencies(libs)
coreLibraryDesugaring(libs.android.desugar)
}

View File

@@ -0,0 +1,7 @@
import extension.applyKoverPluginToAllSubProjects
plugins {
id("org.jetbrains.kotlinx.kover") apply false
}
applyKoverPluginToAllSubProjects()