Use Anvil KSP instead of the Square KAPT one (#3564)

* Use Anvil KSP instead of the Square KAPT one

* Fix several configuration cache, lint and test issues

* Allow incremental kotlin compilation in the CI

* Workaround Robolectric + Compose issue that caused `AppNotIdleException`

* Update the `enterprise` commit hash

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa
2024-10-02 13:52:17 +02:00
committed by GitHub
parent 5fcc80a383
commit 4a43fcb69a
54 changed files with 463 additions and 348 deletions

View File

@@ -9,7 +9,7 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
jobs:

View File

@@ -9,7 +9,7 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
jobs:

View File

@@ -8,7 +8,7 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
jobs:

View File

@@ -7,7 +7,7 @@ on:
- cron: "0 4 * * *"
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
jobs:

View File

@@ -8,7 +8,7 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
jobs:

View File

@@ -7,7 +7,7 @@ on:
- cron: "0 4 * * *"
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
jobs:

View File

@@ -9,7 +9,7 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
jobs:

View File

@@ -7,7 +7,7 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false -Dsonar.gradle.skipCompile=true
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dsonar.gradle.skipCompile=true
jobs:
record:

View File

@@ -7,7 +7,7 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
jobs:

View File

@@ -9,7 +9,7 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dkotlin.incremental=false
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx9g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --warn -Dsonar.gradle.skipCompile=true
GROUP: ${{ format('sonar-{0}', github.ref) }}

View File

@@ -9,7 +9,7 @@ on:
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
jobs:

View File

@@ -7,7 +7,6 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kapt)
}
dependencies {
@@ -16,6 +15,6 @@ dependencies {
implementation(libs.anvil.compiler.utils)
implementation(libs.kotlinpoet)
implementation(libs.dagger)
compileOnly(libs.google.autoservice.annotations)
kapt(libs.google.autoservice)
implementation(libs.ksp.plugin)
implementation(libs.kotlinpoet.ksp)
}

View File

@@ -1,144 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
@file:OptIn(ExperimentalAnvilApi::class)
package io.element.android.anvilcodegen
import com.google.auto.service.AutoService
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.ExperimentalAnvilApi
import com.squareup.anvil.compiler.api.AnvilCompilationException
import com.squareup.anvil.compiler.api.AnvilContext
import com.squareup.anvil.compiler.api.CodeGenerator
import com.squareup.anvil.compiler.api.GeneratedFile
import com.squareup.anvil.compiler.api.createGeneratedFile
import com.squareup.anvil.compiler.internal.asClassName
import com.squareup.anvil.compiler.internal.buildFile
import com.squareup.anvil.compiler.internal.fqName
import com.squareup.anvil.compiler.internal.reference.ClassReference
import com.squareup.anvil.compiler.internal.reference.asClassName
import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeSpec
import dagger.Binds
import dagger.Module
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.multibindings.IntoMap
import io.element.android.anvilannotations.ContributesNode
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
import java.io.File
/**
* This is an anvil plugin that allows Node to use [ContributesNode] alone and let this plugin automatically
* handle the rest of the Dagger wiring required for constructor injection.
*/
@AutoService(CodeGenerator::class)
class ContributesNodeCodeGenerator : CodeGenerator {
override fun isApplicable(context: AnvilContext): Boolean = true
override fun generateCode(codeGenDir: File, module: ModuleDescriptor, projectFiles: Collection<KtFile>): Collection<GeneratedFile> {
return projectFiles.classAndInnerClassReferences(module)
.filter { it.isAnnotatedWith(ContributesNode::class.fqName) }
.flatMap { listOf(generateModule(it, codeGenDir, module), generateAssistedFactory(it, codeGenDir, module)) }
.toList()
}
private fun generateModule(nodeClass: ClassReference.Psi, codeGenDir: File, module: ModuleDescriptor): GeneratedFile {
val generatedPackage = nodeClass.packageFqName.toString()
val moduleClassName = "${nodeClass.shortName}_Module"
val scope = nodeClass.annotations.single { it.fqName == ContributesNode::class.fqName }.scope()
val content = FileSpec.buildFile(generatedPackage, moduleClassName) {
addType(
TypeSpec.classBuilder(moduleClassName)
.addModifiers(KModifier.ABSTRACT)
.addAnnotation(Module::class)
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.asClassName()).build())
.addFunction(
FunSpec.builder("bind${nodeClass.shortName}Factory")
.addModifiers(KModifier.ABSTRACT)
.addParameter("factory", ClassName(generatedPackage, "${nodeClass.shortName}_AssistedFactory"))
.returns(assistedNodeFactoryFqName.asClassName(module).parameterizedBy(STAR))
.addAnnotation(Binds::class)
.addAnnotation(IntoMap::class)
.addAnnotation(
AnnotationSpec.Companion.builder(nodeKeyFqName.asClassName(module)).addMember(
"%T::class",
nodeClass.asClassName()
).build()
)
.build(),
)
.build(),
)
}
return createGeneratedFile(codeGenDir, generatedPackage, moduleClassName, content)
}
private fun generateAssistedFactory(nodeClass: ClassReference.Psi, codeGenDir: File, module: ModuleDescriptor): GeneratedFile {
val generatedPackage = nodeClass.packageFqName.toString()
val assistedFactoryClassName = "${nodeClass.shortName}_AssistedFactory"
val constructor = nodeClass.constructors.singleOrNull { it.isAnnotatedWith(AssistedInject::class.fqName) }
val assistedParameters = constructor?.parameters?.filter { it.isAnnotatedWith(Assisted::class.fqName) }.orEmpty()
if (constructor == null || assistedParameters.size != 2) {
throw AnvilCompilationException(
"${nodeClass.fqName} must have an @AssistedInject constructor with 2 @Assisted parameters",
element = nodeClass.clazz,
)
}
val contextAssistedParam = assistedParameters[0]
if (contextAssistedParam.name != "buildContext") {
throw AnvilCompilationException(
"${nodeClass.fqName} @Assisted parameter must be named buildContext",
element = contextAssistedParam.parameter,
)
}
val pluginsAssistedParam = assistedParameters[1]
if (pluginsAssistedParam.name != "plugins") {
throw AnvilCompilationException(
"${nodeClass.fqName} @Assisted parameter must be named plugins",
element = pluginsAssistedParam.parameter,
)
}
val nodeClassName = nodeClass.asClassName()
val buildContextClassName = contextAssistedParam.type().asTypeName()
val pluginsClassName = pluginsAssistedParam.type().asTypeName()
val content = FileSpec.buildFile(generatedPackage, assistedFactoryClassName) {
addType(
TypeSpec.interfaceBuilder(assistedFactoryClassName)
.addSuperinterface(assistedNodeFactoryFqName.asClassName(module).parameterizedBy(nodeClassName))
.addAnnotation(AssistedFactory::class)
.addFunction(
FunSpec.builder("create")
.addModifiers(KModifier.OVERRIDE, KModifier.ABSTRACT)
.addParameter("buildContext", buildContextClassName)
.addParameter("plugins", pluginsClassName)
.returns(nodeClassName)
.build(),
)
.build(),
)
}
return createGeneratedFile(codeGenDir, generatedPackage, assistedFactoryClassName, content)
}
companion object {
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2022-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.anvilcodegen
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getConstructors
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.validate
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo
import dagger.Binds
import dagger.Module
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.multibindings.IntoMap
import io.element.android.anvilannotations.ContributesNode
import org.jetbrains.kotlin.name.FqName
class ContributesNodeProcessor(
private val logger: KSPLogger,
private val codeGenerator: CodeGenerator,
private val config: Config,
) : SymbolProcessor {
data class Config(
val enableLogging: Boolean = false,
)
override fun process(resolver: Resolver): List<KSAnnotated> {
val annotatedSymbols = resolver.getSymbolsWithAnnotation(ContributesNode::class.qualifiedName!!)
.filterIsInstance<KSClassDeclaration>()
val (validSymbols, invalidSymbols) = annotatedSymbols.partition { it.validate() }
if (validSymbols.isEmpty()) return invalidSymbols
for (ksClass in validSymbols) {
if (config.enableLogging) {
logger.warn("Processing ${ksClass.qualifiedName?.asString()}")
}
generateModule(ksClass)
generateFactory(ksClass)
}
return invalidSymbols
}
private fun generateModule(ksClass: KSClassDeclaration) {
val annotation = ksClass.annotations.find { it.shortName.asString() == "ContributesNode" }!!
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
val modulePackage = ksClass.packageName.asString()
val moduleClassName = "${ksClass.simpleName.asString()}_Module"
val content = FileSpec.builder(
packageName = modulePackage,
fileName = moduleClassName,
)
.addType(
TypeSpec.classBuilder(moduleClassName)
.addModifiers(KModifier.ABSTRACT)
.addAnnotation(Module::class)
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.toTypeName()).build())
.addFunction(
FunSpec.builder("bind${ksClass.simpleName.asString()}Factory")
.addModifiers(KModifier.ABSTRACT)
.addParameter("factory", ClassName(modulePackage, "${ksClass.simpleName.asString()}_AssistedFactory"))
.returns(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(STAR))
.addAnnotation(Binds::class)
.addAnnotation(IntoMap::class)
.addAnnotation(
AnnotationSpec.Companion.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember(
"%T::class",
ClassName.bestGuess(ksClass.qualifiedName!!.asString())
).build()
)
.build(),
)
.build(),
)
.build()
content.writeTo(
codeGenerator = codeGenerator,
dependencies = Dependencies(
aggregating = true,
ksClass.containingFile!!
),
)
}
@OptIn(KspExperimental::class)
private fun generateFactory(ksClass: KSClassDeclaration) {
val generatedPackage = ksClass.packageName.asString()
val assistedFactoryClassName = "${ksClass.simpleName.asString()}_AssistedFactory"
val constructor = ksClass.getConstructors().singleOrNull { it.isAnnotationPresent(AssistedInject::class) }
val assistedParameters = constructor?.parameters?.filter { it.isAnnotationPresent(Assisted::class) }.orEmpty()
if (constructor == null || assistedParameters.size != 2) {
error(
"${ksClass.qualifiedName} must have an @AssistedInject constructor with 2 @Assisted parameters",
)
}
val contextAssistedParam = assistedParameters[0]
if (contextAssistedParam.name?.asString() != "buildContext") {
error(
"${ksClass.qualifiedName} @Assisted parameter must be named buildContext",
)
}
val pluginsAssistedParam = assistedParameters[1]
if (pluginsAssistedParam.name?.asString() != "plugins") {
error(
"${ksClass.qualifiedName} @Assisted parameter must be named plugins",
)
}
val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
val buildContextClassName = contextAssistedParam.type.toTypeName()
val pluginsClassName = pluginsAssistedParam.type.toTypeName()
val content = FileSpec.builder(generatedPackage, assistedFactoryClassName)
.addType(
TypeSpec.interfaceBuilder(assistedFactoryClassName)
.addSuperinterface(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(nodeClassName))
.addAnnotation(AssistedFactory::class)
.addFunction(
FunSpec.builder("create")
.addModifiers(KModifier.OVERRIDE, KModifier.ABSTRACT)
.addParameter("buildContext", buildContextClassName)
.addParameter("plugins", pluginsClassName)
.returns(nodeClassName)
.build(),
)
.build(),
)
.build()
content.writeTo(
codeGenerator = codeGenerator,
dependencies = Dependencies(
aggregating = true,
ksClass.containingFile!!
),
)
}
companion object {
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2022-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.anvilcodegen
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
class ContributesNodeProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
val enableLogging = environment.options["enableLogging"]?.toBoolean() ?: false
return ContributesNodeProcessor(
logger = environment.logger,
codeGenerator = environment.codeGenerator,
config = ContributesNodeProcessor.Config(enableLogging = enableLogging),
)
}
}

View File

@@ -0,0 +1 @@
io.element.android.anvilcodegen.ContributesNodeProcessorProvider

View File

@@ -11,6 +11,7 @@ import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.android.build.gradle.tasks.GenerateBuildConfig
import extension.AssetCopyTask
import extension.ComponentMergingStrategy
import extension.GitBranchNameValueSource
import extension.GitRevisionValueSource
import extension.allEnterpriseImpl
@@ -235,22 +236,24 @@ knit {
setupAnvil(
generateDaggerCode = true,
generateDaggerFactoriesUsingAnvil = false,
componentMergingStrategy = ComponentMergingStrategy.KSP,
)
dependencies {
allLibrariesImpl()
allServicesImpl()
if (isEnterpriseBuild) {
allEnterpriseImpl(rootDir, logger)
allEnterpriseImpl(project)
implementation(projects.appicon.enterprise)
} else {
implementation(projects.appicon.element)
}
allFeaturesImpl(rootDir, logger)
allFeaturesImpl(project)
implementation(projects.features.migration.api)
implementation(projects.appnav)
implementation(projects.appconfig)
implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.compose)
if (ModulesConfig.pushProvidersConfig.includeFirebase) {
"gplayImplementation"(projects.libraries.pushproviders.firebase)
@@ -275,6 +278,8 @@ dependencies {
implementation(libs.serialization.json)
implementation(libs.matrix.emojibase.bindings)
// Needed for UtdTracker
implementation(libs.matrix.sdk)
testImplementation(libs.test.junit)
testImplementation(libs.test.robolectric)

View File

@@ -10,7 +10,6 @@ package io.element.android.x.di
import android.content.Context
import com.squareup.anvil.annotations.MergeComponent
import dagger.BindsInstance
import dagger.Component
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
@@ -19,7 +18,7 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(AppScope::class)
@MergeComponent(AppScope::class)
interface AppComponent : NodeFactoriesBindings {
@Component.Factory
@MergeComponent.Factory
interface Factory {
fun create(
@ApplicationContext @BindsInstance

View File

@@ -10,7 +10,6 @@ package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SessionScope
@@ -20,7 +19,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
@SingleIn(RoomScope::class)
@MergeSubcomponent(RoomScope::class)
interface RoomComponent : NodeFactoriesBindings {
@Subcomponent.Builder
@MergeSubcomponent.Builder
interface Builder {
@BindsInstance
fun room(room: MatrixRoom): Builder

View File

@@ -10,7 +10,6 @@ package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope
@@ -20,7 +19,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
@SingleIn(SessionScope::class)
@MergeSubcomponent(SessionScope::class)
interface SessionComponent : NodeFactoriesBindings {
@Subcomponent.Builder
@MergeSubcomponent.Builder
interface Builder {
@BindsInstance
fun client(matrixClient: MatrixClient): Builder

View File

@@ -22,7 +22,7 @@ android {
setupAnvil()
dependencies {
allFeaturesApi(rootDir, logger)
allFeaturesApi(project)
implementation(projects.libraries.core)
implementation(projects.libraries.androidutils)

View File

@@ -71,8 +71,9 @@ allprojects {
// To have XML report for Danger
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
}
val generatedPath = "${layout.buildDirectory.asFile.get()}/generated/"
filter {
exclude { element -> element.file.path.contains("${layout.buildDirectory.asFile.get()}/generated/") }
exclude { element -> element.file.path.contains(generatedPath) }
}
}
// Dependency check

View File

@@ -1,3 +1,4 @@
import extension.ComponentMergingStrategy
import extension.setupAnvil
/*
@@ -22,7 +23,7 @@ android {
}
}
setupAnvil()
setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
dependencies {
implementation(projects.libraries.core)

View File

@@ -9,7 +9,6 @@ package io.element.android.features.createroom.impl.di
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn
@@ -17,7 +16,7 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(CreateRoomScope::class)
@MergeSubcomponent(CreateRoomScope::class)
interface CreateRoomComponent : NodeFactoriesBindings {
@Subcomponent.Builder
@MergeSubcomponent.Builder
interface Builder {
fun build(): CreateRoomComponent
}

View File

@@ -7,8 +7,10 @@
package io.element.android.features.lockscreen.impl.unlock
import androidx.biometric.BiometricPrompt
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
@@ -23,6 +25,9 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
aPinUnlockState(showBiometricUnlock = false),
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
aPinUnlockState(signOutAction = AsyncAction.Loading),
aPinUnlockState(biometricUnlockResult = BiometricUnlock.AuthenticationResult.Failure(
BiometricUnlockError(BiometricPrompt.ERROR_LOCKOUT, "Biometric auth disabled")
)),
)
}

View File

@@ -1,3 +1,4 @@
import extension.ComponentMergingStrategy
import extension.setupAnvil
/*
@@ -23,7 +24,7 @@ android {
}
}
setupAnvil()
setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
dependencies {
implementation(projects.appconfig)

View File

@@ -9,7 +9,6 @@ package io.element.android.features.login.impl.di
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
@@ -17,7 +16,7 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(QrCodeLoginScope::class)
@MergeSubcomponent(QrCodeLoginScope::class)
interface QrCodeLoginComponent : NodeFactoriesBindings {
@Subcomponent.Builder
@MergeSubcomponent.Builder
interface Builder {
fun build(): QrCodeLoginComponent
}

View File

@@ -16,15 +16,15 @@ import io.element.android.features.login.impl.qrcode.QrCodeLoginManager
import io.element.android.libraries.architecture.AssistedNodeFactory
import io.element.android.libraries.architecture.createNode
internal class FakeQrCodeLoginComponent(private val qrCodeLoginManager: QrCodeLoginManager) :
QrCodeLoginComponent {
internal class FakeMergedQrCodeLoginComponent(private val qrCodeLoginManager: QrCodeLoginManager) :
MergedQrCodeLoginComponent {
// Ignore this error, it does override a method once code generation is done
override fun qrCodeLoginManager(): QrCodeLoginManager = qrCodeLoginManager
class Builder(private val qrCodeLoginManager: QrCodeLoginManager = FakeQrCodeLoginManager()) :
QrCodeLoginComponent.Builder {
override fun build(): QrCodeLoginComponent {
return FakeQrCodeLoginComponent(qrCodeLoginManager)
return FakeMergedQrCodeLoginComponent(qrCodeLoginManager)
}
}

View File

@@ -13,7 +13,7 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.di.FakeQrCodeLoginComponent
import io.element.android.features.login.impl.di.FakeMergedQrCodeLoginComponent
import io.element.android.features.login.impl.screens.qrcode.confirmation.QrCodeConfirmationStep
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
@@ -200,7 +200,7 @@ class QrCodeLoginFlowNodeTest {
return QrCodeLoginFlowNode(
buildContext = buildContext,
plugins = emptyList(),
qrCodeLoginComponentBuilder = FakeQrCodeLoginComponent.Builder(qrCodeLoginManager),
qrCodeLoginComponentBuilder = FakeMergedQrCodeLoginComponent.Builder(qrCodeLoginManager),
defaultLoginUserStory = defaultLoginUserStory,
coroutineDispatchers = coroutineDispatchers,
)

View File

@@ -1,13 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.messages.impl.timeline.session
data class SessionState(
val isSessionVerified: Boolean,
val isKeyBackupEnabled: Boolean,
)

View File

@@ -28,6 +28,7 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
@@ -100,7 +101,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinne
onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
) {
setContent {
setSafeContent {
PinnedMessagesListView(
state = state,
onBackClick = onBackClick,

View File

@@ -29,6 +29,7 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.setSafeContent
import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule
import org.junit.Test
@@ -151,7 +152,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
forceJumpToBottomVisibility: Boolean = false,
) {
setContent {
setSafeContent {
TimelineView(
state = state,
typingNotificationState = typingNotificationState,

View File

@@ -1,18 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.roomlist.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class InvitesStateProvider : PreviewParameterProvider<InvitesState> {
override val values: Sequence<InvitesState>
get() = sequenceOf(
InvitesState.SeenInvites,
InvitesState.NewInvites,
)
}

View File

@@ -51,12 +51,6 @@ data class RoomListState(
}
}
enum class InvitesState {
NoInvites,
SeenInvites,
NewInvites,
}
enum class SecurityBannerState {
None,
SetUpRecovery,

View File

@@ -47,6 +47,7 @@ class ResetIdentityFlowManagerTest {
var result: AsyncData.Success<IdentityResetHandle>? = null
flowManager.getResetHandle().test {
assertThat(awaitItem().isLoading()).isTrue()
@Suppress("UNCHECKED_CAST")
result = awaitItem() as? AsyncData.Success<IdentityResetHandle>
assertThat(result).isNotNull()
}

View File

@@ -25,12 +25,13 @@ kotlin.code.style=official
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.parallel=true
# Check here for the reasons https://github.com/square/anvil/issues/693
# useClasspathSnapshot=false is not enough in most cases.
kotlin.incremental=false
# Caching
org.gradle.caching=true
org.gradle.configuration-cache=true
kotlin.incremental=true
# Dummy values for signing secrets / nightly
signing.element.nightly.storePassword=Secret
@@ -46,3 +47,9 @@ android.experimental.enableTestFixtures=true
# Create BuildConfig files as bytecode to avoid Java compilation phase
android.enableBuildConfigAsBytecode=true
# Add the KSP code generation annotations to the list of contributing annotations for Anvil
com.squareup.anvil.kspContributingAnnotations=io.element.android.anvilannotations.ContributesNode
# Only apply KSP to main sources
ksp.allow.all.target.configuration=false

View File

@@ -5,6 +5,7 @@
# Project
android_gradle_plugin = "8.6.1"
kotlin = "1.9.25"
kotlinpoetKsp = "1.17.0"
ksp = "1.9.25-1.0.20"
firebaseAppDistribution = "5.0.0"
@@ -49,7 +50,7 @@ telephoto = "0.13.0"
# DI
dagger = "2.52"
anvil = "2.4.9"
anvil = "0.3.1"
# Auto service
autoservice = "1.1.1"
@@ -63,14 +64,17 @@ kover = "0.8.3"
android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" }
# https://developer.android.com/studio/write/java8-support#library-desugaring-versions
android_desugar = "com.android.tools:desugar_jdk_libs:2.1.2"
anvil_gradle_plugin = { module = "com.squareup.anvil:gradle-plugin", version.ref = "anvil" }
anvil_gradle_plugin = { module = "dev.zacsweers.anvil:gradle-plugin", version.ref = "anvil" }
kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoetKsp" }
kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
gms_google_services = "com.google.gms:google-services:4.4.2"
# https://firebase.google.com/docs/android/setup#available-libraries
google_firebase_bom = "com.google.firebase:firebase-bom:33.4.0"
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
# AndroidX
androidx_core = { module = "androidx.core:core", version.ref = "core" }
@@ -198,8 +202,8 @@ sigpwned_emoji4j = "com.sigpwned:emoji4j-core:15.1.2"
inject = "javax.inject:javax.inject:1"
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
dagger_compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
anvil_compiler_api = { module = "com.squareup.anvil:compiler-api", version.ref = "anvil" }
anvil_compiler_utils = { module = "com.squareup.anvil:compiler-utils", version.ref = "anvil" }
anvil_compiler_api = { module = "dev.zacsweers.anvil:compiler-api", version.ref = "anvil" }
anvil_compiler_utils = { module = "dev.zacsweers.anvil:compiler-utils", version.ref = "anvil" }
# Auto services
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
@@ -222,7 +226,7 @@ kotlin_jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
anvil = { id = "com.squareup.anvil", version.ref = "anvil" }
anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" }
detekt = "io.gitlab.arturbosch.detekt:1.23.7"
ktlint = "org.jlleitschuh.gradle.ktlint:12.1.1"
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"

View File

@@ -8,7 +8,7 @@
package io.element.android.libraries.designsystem.component.async
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.core.rememberTransition
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.rememberCoroutineScope
@@ -237,7 +237,7 @@ class AsyncIndicatorTest {
val coroutineScope = rememberCoroutineScope()
val transition = state.currentItem.value?.let {
// If there is an item, update its transition state to simulate an animation
updateTransition(state.currentAnimationState, label = "")
rememberTransition(state.currentAnimationState, label = "")
}
if (state.currentAnimationState.hasEntered() && state.currentItem.value?.durationMs != null) {
SideEffect {

View File

@@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2023, 2024 New Vector Ltd.
*
@@ -7,16 +9,17 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
}
setupAnvil()
android {
namespace = "io.element.android.libraries.mediapickers.impl"
dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.di)
implementation(libs.inject)
api(projects.libraries.mediapickers.api)
}
}
dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.di)
implementation(libs.inject)
api(projects.libraries.mediapickers.api)
}

View File

@@ -1,4 +1,6 @@
import extension.setupAnvil
import org.gradle.internal.extensions.stdlib.capitalized
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
/*
* Copyright 2023, 2024 New Vector Ltd.
@@ -51,3 +53,15 @@ sqldelight {
}
}
}
// Workaround for KSP not picking up the generated files from SqlDelight
androidComponents {
onVariants(selector().all()) { variant ->
afterEvaluate {
val variantName = variant.name.capitalized()
tasks.getByName<KotlinCompile>("ksp${variantName}Kotlin") {
setSource(tasks.getByName("generate${variantName}SessionDatabaseInterface").outputs)
}
}
}
}

View File

@@ -23,4 +23,5 @@ dependencies {
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
implementation(libs.autonomousapps.dependencyanalysis.plugin)
implementation(libs.anvil.gradle.plugin)
implementation(libs.ksp.gradle.plugin)
}

View File

@@ -16,22 +16,40 @@ import org.gradle.plugin.use.PluginDependency
/**
* Setup Anvil plugin with the given configuration.
* @param generateDaggerCode whether to enable general Dagger code generation using Kapt
* @param generateDaggerFactoriesUsingAnvil whether to generate Dagger factories using Anvil instead of Kapt
* @param generateDaggerCode whether to enable general Dagger code generation using Kapt. `false` by default.
* @param generateDaggerFactoriesUsingAnvil whether to generate Dagger factories using Anvil instead of Kapt. `true` by default.
* @param componentMergingStrategy how to perform component merging. This is `ComponentMergingStrategy.NONE` by default, which will prevent component merging
* from running.
*/
fun Project.setupAnvil(
generateDaggerCode: Boolean = false,
generateDaggerFactoriesUsingAnvil: Boolean = true,
componentMergingStrategy: ComponentMergingStrategy = ComponentMergingStrategy.NONE,
) {
val libs = the<LibrariesForLibs>()
// Apply plugins and dependencies
// Add dagger dependency, needed for generated code
dependencies.implementation(libs.dagger)
// Apply Anvil plugin and configure it
applyPluginIfNeeded(libs.plugins.anvil)
project.pluginManager.withPlugin(libs.plugins.anvil.get().pluginId) {
// Setup extension
extensions.configure(AnvilExtension::class.java) {
this.generateDaggerFactories.set(generateDaggerFactoriesUsingAnvil)
this.disableComponentMerging.set(componentMergingStrategy == ComponentMergingStrategy.NONE)
useKsp(
contributesAndFactoryGeneration = true,
componentMerging = componentMergingStrategy == ComponentMergingStrategy.KSP,
)
}
}
if (generateDaggerCode) {
applyPluginIfNeeded(libs.plugins.kapt)
// Needed at the top level since dagger code should be generated at a single point for performance
dependencies.implementation(libs.dagger)
dependencies.add("kapt", libs.dagger.compiler)
// Needed at the top level since dagger code should be generated at a single point for performance reasons
dependencies.add("ksp", libs.dagger.compiler)
}
// These dependencies are only needed for compose library or application modules
@@ -40,14 +58,7 @@ fun Project.setupAnvil(
// Annotations to generate DI code for Appyx nodes
dependencies.implementation(project.project(":anvilannotations"))
// Code generator for the annotations above
dependencies.add("anvil", project.project(":anvilcodegen"))
}
project.pluginManager.withPlugin(libs.plugins.anvil.get().pluginId) {
// Setup extension
extensions.configure(AnvilExtension::class.java) {
this.generateDaggerFactories.set(generateDaggerFactoriesUsingAnvil)
}
dependencies.add("ksp", project.project(":anvilcodegen"))
}
}
@@ -57,3 +68,9 @@ private fun Project.applyPluginIfNeeded(plugin: Provider<PluginDependency>) {
pluginManager.apply(pluginId)
}
}
enum class ComponentMergingStrategy {
NONE,
KAPT,
KSP
}

View File

@@ -7,17 +7,16 @@
package extension
import config.AnalyticsConfig
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.api.logging.Logger
import org.gradle.kotlin.dsl.DependencyHandlerScope
import org.gradle.kotlin.dsl.closureOf
import org.gradle.kotlin.dsl.project
import java.io.File
private fun DependencyHandlerScope.implementation(dependency: Any) = dependencies.add("implementation", dependency)
internal fun DependencyHandler.implementation(dependency: Any) = add("implementation", dependency)
@@ -26,7 +25,7 @@ internal fun DependencyHandler.implementation(dependency: Any) = add("implementa
private fun DependencyHandlerScope.implementation(
dependency: Any,
config: Action<ExternalModuleDependency>
) = dependencies.add("implementation", dependency, closureOf<ExternalModuleDependency> { config.execute(this) })
) = dependencies.add("implementation", dependency, closureOf<ExternalModuleDependency> { config.execute(this) })
private fun DependencyHandlerScope.androidTestImplementation(dependency: Any) = dependencies.add("androidTestImplementation", dependency)
@@ -58,26 +57,6 @@ fun DependencyHandlerScope.composeDependencies(libs: LibrariesForLibs) {
implementation(libs.kotlinx.collections.immutable)
}
private fun DependencyHandlerScope.addImplementationProjects(
directory: File,
path: String,
nameFilter: String,
logger: Logger,
) {
directory.listFiles().orEmpty().also { it.sort() }.forEach { file ->
if (file.isDirectory) {
val newPath = "$path:${file.name}"
val buildFile = File(file, "build.gradle.kts")
if (buildFile.exists() && file.name == nameFilter) {
implementation(project(newPath))
logger.lifecycle("Added implementation(project($newPath))")
} else {
addImplementationProjects(file, newPath, nameFilter, logger)
}
}
}
}
fun DependencyHandlerScope.allLibrariesImpl() {
implementation(project(":libraries:androidutils"))
implementation(project(":libraries:deeplink"))
@@ -128,22 +107,21 @@ fun DependencyHandlerScope.allServicesImpl() {
}
}
}
implementation(project(":services:apperror:impl"))
implementation(project(":services:appnavstate:impl"))
implementation(project(":services:toolbox:impl"))
}
fun DependencyHandlerScope.allEnterpriseImpl(rootDir: File, logger: Logger) {
val enterpriseDir = File(rootDir, "enterprise")
addImplementationProjects(enterpriseDir, ":enterprise", "impl", logger)
}
fun DependencyHandlerScope.allEnterpriseImpl(project: Project) = addAll(project, "enterprise", "impl")
fun DependencyHandlerScope.allFeaturesApi(rootDir: File, logger: Logger) {
val featuresDir = File(rootDir, "features")
addImplementationProjects(featuresDir, ":features", "api", logger)
}
fun DependencyHandlerScope.allFeaturesImpl(project: Project) = addAll(project, "features", "impl")
fun DependencyHandlerScope.allFeaturesImpl(rootDir: File, logger: Logger) {
val featuresDir = File(rootDir, "features")
addImplementationProjects(featuresDir, ":features", "impl", logger)
fun DependencyHandlerScope.allFeaturesApi(project: Project) = addAll(project, "features", "api")
private fun DependencyHandlerScope.addAll(project: Project, prefix: String, suffix: String) {
val subProjects = project.rootProject.subprojects.filter { it.path.startsWith(":$prefix") && it.path.endsWith(":$suffix") }
for (p in subProjects) {
add("implementation", p)
}
}

View File

@@ -14,7 +14,6 @@ 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.configurationcache.extensions.capitalized
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.assign
@@ -142,24 +141,18 @@ fun Project.setupKover() {
}
}
filters {
includes {
classes(
"*Presenter",
)
}
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",
)
}
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) {
@@ -175,33 +168,31 @@ fun Project.setupKover() {
}
}
filters {
includes {
classes(
"^*State$",
)
}
excludes {
classes(
"io.element.android.appnav.root.RootNavState*",
"io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*",
"io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*",
"io.element.android.libraries.matrix.api.room.RoomMembershipState*",
"io.element.android.libraries.matrix.api.room.MatrixRoomMembersState*",
"io.element.android.libraries.push.impl.notifications.NotificationState*",
"io.element.android.features.messages.impl.media.local.pdf.PdfViewerState",
"io.element.android.features.messages.impl.media.local.LocalMediaViewState",
"io.element.android.features.location.impl.map.MapState*",
"io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*",
"io.element.android.libraries.designsystem.swipe.SwipeableActionsState*",
"io.element.android.features.messages.impl.timeline.components.ExpandableState*",
"io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*",
"io.element.android.libraries.maplibre.compose.CameraPositionState*",
"io.element.android.libraries.maplibre.compose.SaveableCameraPositionState",
"io.element.android.libraries.maplibre.compose.SymbolState*",
"io.element.android.features.ftue.api.state.*",
"io.element.android.features.ftue.impl.welcome.state.*",
)
}
excludes.classes(
"*State$*", // Exclude inner classes
"io.element.android.appnav.root.RootNavState*",
"io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*",
"io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*",
"io.element.android.libraries.matrix.api.room.RoomMembershipState*",
"io.element.android.libraries.matrix.api.room.MatrixRoomMembersState*",
"io.element.android.libraries.push.impl.notifications.NotificationState*",
"io.element.android.features.messages.impl.media.local.pdf.PdfViewerState",
"io.element.android.features.messages.impl.media.local.LocalMediaViewState",
"io.element.android.features.location.impl.map.MapState*",
"io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*",
"io.element.android.libraries.designsystem.swipe.SwipeableActionsState*",
"io.element.android.features.messages.impl.timeline.components.ExpandableState*",
"io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*",
"io.element.android.libraries.maplibre.compose.CameraPositionState*",
"io.element.android.libraries.maplibre.compose.SaveableCameraPositionState",
"io.element.android.libraries.maplibre.compose.SymbolState*",
"io.element.android.features.ftue.api.state.*",
"io.element.android.features.ftue.impl.welcome.state.*",
"io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState",
"io.element.android.libraries.mediaviewer.api.local.pdf.PdfViewerState",
"io.element.android.libraries.textcomposer.model.TextEditorState",
)
includes.classes("*State")
}
}
variant(KoverVariant.Views.variantName) {
@@ -218,11 +209,8 @@ fun Project.setupKover() {
}
}
filters {
includes {
classes(
"*ViewKt",
)
}
excludes.classes("*ViewKt$*") // Exclude inner classes
includes.classes("*ViewKt")
}
}
}
@@ -236,16 +224,31 @@ fun Project.applyKoverPluginToAllSubProjects() = rootProject.subprojects {
currentProject {
for (variant in koverVariants) {
createVariant(variant) {
defaultVariants()
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() {
addWithDependencies("gplayDebug", "debug", optional = true)
fun KoverVariantCreateConfig.defaultVariants(project: Project) {
if (project.name == "app") {
addWithDependencies("gplayDebug")
} else {
addWithDependencies("debug", "jvm", optional = true)
}
}
fun Project.koverSubprojects() = project.rootProject.subprojects

View File

@@ -29,7 +29,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class, boundType = AnalyticsService::class, priority = ContributesBinding.Priority.HIGHEST)
@ContributesBinding(AppScope::class, boundType = AnalyticsService::class, rank = ContributesBinding.RANK_HIGHEST)
class DefaultAnalyticsService @Inject constructor(
private val analyticsProviders: Set<@JvmSuppressWildcards AnalyticsProvider>,
private val analyticsStore: AnalyticsStore,

View File

@@ -28,7 +28,6 @@ dependencies {
// - Add every single module as a dependency of this one.
// - Move the Konsist tests to the `app` module, but the `app` module does not need to know about Konsist.
tasks.withType<Test>().configureEach {
outputs.upToDateWhen {
gradle.startParameter.taskNames.any { it.contains("check", ignoreCase = true).not() }
}
val isNotCheckTask = gradle.startParameter.taskNames.any { it.contains("check", ignoreCase = true).not() }
outputs.upToDateWhen { isNotCheckTask }
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.tests.testutils
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import org.junit.Assert.assertFalse
import org.junit.rules.TestRule
import kotlin.coroutines.CoroutineContext
object RobolectricDispatcherCleaner {
// HACK: Workaround for https://github.com/robolectric/robolectric/issues/7055#issuecomment-1551119229
fun clearAndroidUiDispatcher(pkg: String = "androidx.compose.ui.platform") {
val clazz = javaClass.classLoader!!.loadClass("$pkg.AndroidUiDispatcher")
val combinedContextClass = javaClass.classLoader!!.loadClass("kotlin.coroutines.CombinedContext")
val companionClazz = clazz.getDeclaredField("Companion").get(clazz)
val combinedContext = companionClazz.javaClass.getDeclaredMethod("getMain")
.invoke(companionClazz) as CoroutineContext
val androidUiDispatcher = combinedContextClass.getDeclaredField("element")
.apply { isAccessible = true }
.get(combinedContext)
.let { clazz.cast(it) }
var scheduledFrameDispatch = clazz.getDeclaredField("scheduledFrameDispatch")
.apply { isAccessible = true }
.getBoolean(androidUiDispatcher)
var scheduledTrampolineDispatch = clazz.getDeclaredField("scheduledTrampolineDispatch")
.apply { isAccessible = true }
.getBoolean(androidUiDispatcher)
val dispatchCallback = clazz.getDeclaredField("dispatchCallback")
.apply { isAccessible = true }
.get(androidUiDispatcher) as Runnable
if (scheduledFrameDispatch || scheduledTrampolineDispatch) {
dispatchCallback.run()
scheduledFrameDispatch = clazz.getDeclaredField("scheduledFrameDispatch")
.apply { isAccessible = true }
.getBoolean(androidUiDispatcher)
scheduledTrampolineDispatch = clazz.getDeclaredField("scheduledTrampolineDispatch")
.apply { isAccessible = true }
.getBoolean(androidUiDispatcher)
}
assertFalse(scheduledFrameDispatch)
assertFalse(scheduledTrampolineDispatch)
}
}
fun <R : TestRule, A : ComponentActivity> AndroidComposeTestRule<R, A>.setSafeContent(content: @Composable () -> Unit) {
RobolectricDispatcherCleaner.clearAndroidUiDispatcher()
setContent(content)
}

View File

@@ -52,7 +52,7 @@ dependencies {
implementation(projects.appnav)
allLibrariesImpl()
allServicesImpl()
allFeaturesImpl(rootDir, logger)
allFeaturesImpl(project)
implementation(projects.appicon.element)
implementation(projects.appicon.enterprise)

View File

@@ -122,7 +122,7 @@ const previewAnnotations = [
const filesWithPreviews = editedFiles.filter(file => file.endsWith(".kt")).filter(file => {
const content = fs.readFileSync(file);
return previewAnnotations.some((ann) => content.includes(ann));
return previewAnnotations.some((ann) => content.includes("import " + ann));
})
const composablePreviewProviderContents = fs.readFileSync('tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt');