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 # Enrich gradle.properties for CI/CD
env: 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
jobs: jobs:

View file

@ -9,7 +9,7 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
jobs: jobs:

View file

@ -8,7 +8,7 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
jobs: jobs:

View file

@ -7,7 +7,7 @@ on:
- cron: "0 4 * * *" - cron: "0 4 * * *"
env: 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
jobs: jobs:

View file

@ -8,7 +8,7 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
jobs: jobs:

View file

@ -7,7 +7,7 @@ on:
- cron: "0 4 * * *" - cron: "0 4 * * *"
env: 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
jobs: jobs:

View file

@ -9,7 +9,7 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
jobs: jobs:

View file

@ -7,7 +7,7 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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: jobs:
record: record:

View file

@ -7,7 +7,7 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true
jobs: jobs:

View file

@ -9,7 +9,7 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace --warn -Dsonar.gradle.skipCompile=true
GROUP: ${{ format('sonar-{0}', github.ref) }} GROUP: ${{ format('sonar-{0}', github.ref) }}

View file

@ -9,7 +9,7 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: 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 CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true
jobs: jobs:

View file

@ -7,7 +7,6 @@
plugins { plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kapt)
} }
dependencies { dependencies {
@ -16,6 +15,6 @@ dependencies {
implementation(libs.anvil.compiler.utils) implementation(libs.anvil.compiler.utils)
implementation(libs.kotlinpoet) implementation(libs.kotlinpoet)
implementation(libs.dagger) implementation(libs.dagger)
compileOnly(libs.google.autoservice.annotations) implementation(libs.ksp.plugin)
kapt(libs.google.autoservice) 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.internal.tasks.factory.dependsOn
import com.android.build.gradle.tasks.GenerateBuildConfig import com.android.build.gradle.tasks.GenerateBuildConfig
import extension.AssetCopyTask import extension.AssetCopyTask
import extension.ComponentMergingStrategy
import extension.GitBranchNameValueSource import extension.GitBranchNameValueSource
import extension.GitRevisionValueSource import extension.GitRevisionValueSource
import extension.allEnterpriseImpl import extension.allEnterpriseImpl
@ -235,22 +236,24 @@ knit {
setupAnvil( setupAnvil(
generateDaggerCode = true, generateDaggerCode = true,
generateDaggerFactoriesUsingAnvil = false, generateDaggerFactoriesUsingAnvil = false,
componentMergingStrategy = ComponentMergingStrategy.KSP,
) )
dependencies { dependencies {
allLibrariesImpl() allLibrariesImpl()
allServicesImpl() allServicesImpl()
if (isEnterpriseBuild) { if (isEnterpriseBuild) {
allEnterpriseImpl(rootDir, logger) allEnterpriseImpl(project)
implementation(projects.appicon.enterprise) implementation(projects.appicon.enterprise)
} else { } else {
implementation(projects.appicon.element) implementation(projects.appicon.element)
} }
allFeaturesImpl(rootDir, logger) allFeaturesImpl(project)
implementation(projects.features.migration.api) implementation(projects.features.migration.api)
implementation(projects.appnav) implementation(projects.appnav)
implementation(projects.appconfig) implementation(projects.appconfig)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.compose)
if (ModulesConfig.pushProvidersConfig.includeFirebase) { if (ModulesConfig.pushProvidersConfig.includeFirebase) {
"gplayImplementation"(projects.libraries.pushproviders.firebase) "gplayImplementation"(projects.libraries.pushproviders.firebase)
@ -275,6 +278,8 @@ dependencies {
implementation(libs.serialization.json) implementation(libs.serialization.json)
implementation(libs.matrix.emojibase.bindings) implementation(libs.matrix.emojibase.bindings)
// Needed for UtdTracker
implementation(libs.matrix.sdk)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.test.robolectric) testImplementation(libs.test.robolectric)

View file

@ -10,7 +10,6 @@ package io.element.android.x.di
import android.content.Context import android.content.Context
import com.squareup.anvil.annotations.MergeComponent import com.squareup.anvil.annotations.MergeComponent
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Component
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
@ -19,7 +18,7 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
@MergeComponent(AppScope::class) @MergeComponent(AppScope::class)
interface AppComponent : NodeFactoriesBindings { interface AppComponent : NodeFactoriesBindings {
@Component.Factory @MergeComponent.Factory
interface Factory { interface Factory {
fun create( fun create(
@ApplicationContext @BindsInstance @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.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ -20,7 +19,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
@SingleIn(RoomScope::class) @SingleIn(RoomScope::class)
@MergeSubcomponent(RoomScope::class) @MergeSubcomponent(RoomScope::class)
interface RoomComponent : NodeFactoriesBindings { interface RoomComponent : NodeFactoriesBindings {
@Subcomponent.Builder @MergeSubcomponent.Builder
interface Builder { interface Builder {
@BindsInstance @BindsInstance
fun room(room: MatrixRoom): Builder 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.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ -20,7 +19,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
@SingleIn(SessionScope::class) @SingleIn(SessionScope::class)
@MergeSubcomponent(SessionScope::class) @MergeSubcomponent(SessionScope::class)
interface SessionComponent : NodeFactoriesBindings { interface SessionComponent : NodeFactoriesBindings {
@Subcomponent.Builder @MergeSubcomponent.Builder
interface Builder { interface Builder {
@BindsInstance @BindsInstance
fun client(matrixClient: MatrixClient): Builder fun client(matrixClient: MatrixClient): Builder

View file

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

View file

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

@ -1 +1 @@
Subproject commit ceb65e32d95052c028a37654a4a0410639f69053 Subproject commit b4f0427e3595049d39846aabcdc06e818f2e96ea

View file

@ -1,3 +1,4 @@
import extension.ComponentMergingStrategy
import extension.setupAnvil import extension.setupAnvil
/* /*
@ -22,7 +23,7 @@ android {
} }
} }
setupAnvil() setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
dependencies { dependencies {
implementation(projects.libraries.core) 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.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
@ -17,7 +16,7 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(CreateRoomScope::class) @SingleIn(CreateRoomScope::class)
@MergeSubcomponent(CreateRoomScope::class) @MergeSubcomponent(CreateRoomScope::class)
interface CreateRoomComponent : NodeFactoriesBindings { interface CreateRoomComponent : NodeFactoriesBindings {
@Subcomponent.Builder @MergeSubcomponent.Builder
interface Builder { interface Builder {
fun build(): CreateRoomComponent fun build(): CreateRoomComponent
} }

View file

@ -7,8 +7,10 @@
package io.element.android.features.lockscreen.impl.unlock package io.element.android.features.lockscreen.impl.unlock
import androidx.biometric.BiometricPrompt
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock 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.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -23,6 +25,9 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
aPinUnlockState(showBiometricUnlock = false), aPinUnlockState(showBiometricUnlock = false),
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
aPinUnlockState(signOutAction = AsyncAction.Loading), 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 import extension.setupAnvil
/* /*
@ -23,7 +24,7 @@ android {
} }
} }
setupAnvil() setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
dependencies { dependencies {
implementation(projects.appconfig) 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.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.Subcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
@ -17,7 +16,7 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(QrCodeLoginScope::class) @SingleIn(QrCodeLoginScope::class)
@MergeSubcomponent(QrCodeLoginScope::class) @MergeSubcomponent(QrCodeLoginScope::class)
interface QrCodeLoginComponent : NodeFactoriesBindings { interface QrCodeLoginComponent : NodeFactoriesBindings {
@Subcomponent.Builder @MergeSubcomponent.Builder
interface Builder { interface Builder {
fun build(): QrCodeLoginComponent 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.AssistedNodeFactory
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
internal class FakeQrCodeLoginComponent(private val qrCodeLoginManager: QrCodeLoginManager) : internal class FakeMergedQrCodeLoginComponent(private val qrCodeLoginManager: QrCodeLoginManager) :
QrCodeLoginComponent { MergedQrCodeLoginComponent {
// Ignore this error, it does override a method once code generation is done // Ignore this error, it does override a method once code generation is done
override fun qrCodeLoginManager(): QrCodeLoginManager = qrCodeLoginManager override fun qrCodeLoginManager(): QrCodeLoginManager = qrCodeLoginManager
class Builder(private val qrCodeLoginManager: QrCodeLoginManager = FakeQrCodeLoginManager()) : class Builder(private val qrCodeLoginManager: QrCodeLoginManager = FakeQrCodeLoginManager()) :
QrCodeLoginComponent.Builder { QrCodeLoginComponent.Builder {
override fun build(): QrCodeLoginComponent { 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.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.DefaultLoginUserStory 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.features.login.impl.screens.qrcode.confirmation.QrCodeConfirmationStep
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
@ -200,7 +200,7 @@ class QrCodeLoginFlowNodeTest {
return QrCodeLoginFlowNode( return QrCodeLoginFlowNode(
buildContext = buildContext, buildContext = buildContext,
plugins = emptyList(), plugins = emptyList(),
qrCodeLoginComponentBuilder = FakeQrCodeLoginComponent.Builder(qrCodeLoginManager), qrCodeLoginComponentBuilder = FakeMergedQrCodeLoginComponent.Builder(qrCodeLoginManager),
defaultLoginUserStory = defaultLoginUserStory, defaultLoginUserStory = defaultLoginUserStory,
coroutineDispatchers = coroutineDispatchers, 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.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule import org.junit.rules.TestRule
@ -100,7 +101,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinne
onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(), onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
) { ) {
setContent { setSafeContent {
PinnedMessagesListView( PinnedMessagesListView(
state = state, state = state,
onBackClick = onBackClick, 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.EnsureNeverCalledWithTwoParams
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.setSafeContent
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -151,7 +152,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
onJoinCallClick: () -> Unit = EnsureNeverCalled(), onJoinCallClick: () -> Unit = EnsureNeverCalled(),
forceJumpToBottomVisibility: Boolean = false, forceJumpToBottomVisibility: Boolean = false,
) { ) {
setContent { setSafeContent {
TimelineView( TimelineView(
state = state, state = state,
typingNotificationState = typingNotificationState, 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 { enum class SecurityBannerState {
None, None,
SetUpRecovery, SetUpRecovery,

View file

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

View file

@ -25,12 +25,13 @@ kotlin.code.style=official
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
org.gradle.caching=true
org.gradle.configureondemand=true org.gradle.configureondemand=true
org.gradle.parallel=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. # Caching
kotlin.incremental=false org.gradle.caching=true
org.gradle.configuration-cache=true
kotlin.incremental=true
# Dummy values for signing secrets / nightly # Dummy values for signing secrets / nightly
signing.element.nightly.storePassword=Secret signing.element.nightly.storePassword=Secret
@ -46,3 +47,9 @@ android.experimental.enableTestFixtures=true
# Create BuildConfig files as bytecode to avoid Java compilation phase # Create BuildConfig files as bytecode to avoid Java compilation phase
android.enableBuildConfigAsBytecode=true 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 # Project
android_gradle_plugin = "8.6.1" android_gradle_plugin = "8.6.1"
kotlin = "1.9.25" kotlin = "1.9.25"
kotlinpoetKsp = "1.17.0"
ksp = "1.9.25-1.0.20" ksp = "1.9.25-1.0.20"
firebaseAppDistribution = "5.0.0" firebaseAppDistribution = "5.0.0"
@ -49,7 +50,7 @@ telephoto = "0.13.0"
# DI # DI
dagger = "2.52" dagger = "2.52"
anvil = "2.4.9" anvil = "0.3.1"
# Auto service # Auto service
autoservice = "1.1.1" 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" } 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 # https://developer.android.com/studio/write/java8-support#library-desugaring-versions
android_desugar = "com.android.tools:desugar_jdk_libs:2.1.2" 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" } 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" } 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" gms_google_services = "com.google.gms:google-services:4.4.2"
# https://firebase.google.com/docs/android/setup#available-libraries # https://firebase.google.com/docs/android/setup#available-libraries
google_firebase_bom = "com.google.firebase:firebase-bom:33.4.0" google_firebase_bom = "com.google.firebase:firebase-bom:33.4.0"
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" } 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" } 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
androidx_core = { module = "androidx.core:core", version.ref = "core" } 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" inject = "javax.inject:javax.inject:1"
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
dagger_compiler = { module = "com.google.dagger:dagger-compiler", 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_api = { module = "dev.zacsweers.anvil:compiler-api", version.ref = "anvil" }
anvil_compiler_utils = { module = "com.squareup.anvil:compiler-utils", version.ref = "anvil" } anvil_compiler_utils = { module = "dev.zacsweers.anvil:compiler-utils", version.ref = "anvil" }
# Auto services # Auto services
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } 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" } kotlin_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 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" detekt = "io.gitlab.arturbosch.detekt:1.23.7"
ktlint = "org.jlleitschuh.gradle.ktlint:12.1.1" ktlint = "org.jlleitschuh.gradle.ktlint:12.1.1"
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12" dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"

View file

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

View file

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

View file

@ -1,4 +1,6 @@
import extension.setupAnvil import extension.setupAnvil
import org.gradle.internal.extensions.stdlib.capitalized
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
/* /*
* Copyright 2023, 2024 New Vector Ltd. * 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(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
implementation(libs.autonomousapps.dependencyanalysis.plugin) implementation(libs.autonomousapps.dependencyanalysis.plugin)
implementation(libs.anvil.gradle.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. * Setup Anvil plugin with the given configuration.
* @param generateDaggerCode whether to enable general Dagger code generation using 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 * @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( fun Project.setupAnvil(
generateDaggerCode: Boolean = false, generateDaggerCode: Boolean = false,
generateDaggerFactoriesUsingAnvil: Boolean = true, generateDaggerFactoriesUsingAnvil: Boolean = true,
componentMergingStrategy: ComponentMergingStrategy = ComponentMergingStrategy.NONE,
) { ) {
val libs = the<LibrariesForLibs>() 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) 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) { if (generateDaggerCode) {
applyPluginIfNeeded(libs.plugins.kapt) // Needed at the top level since dagger code should be generated at a single point for performance reasons
// Needed at the top level since dagger code should be generated at a single point for performance dependencies.add("ksp", libs.dagger.compiler)
dependencies.implementation(libs.dagger)
dependencies.add("kapt", libs.dagger.compiler)
} }
// These dependencies are only needed for compose library or application modules // 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 // Annotations to generate DI code for Appyx nodes
dependencies.implementation(project.project(":anvilannotations")) dependencies.implementation(project.project(":anvilannotations"))
// Code generator for the annotations above // Code generator for the annotations above
dependencies.add("anvil", project.project(":anvilcodegen")) dependencies.add("ksp", project.project(":anvilcodegen"))
}
project.pluginManager.withPlugin(libs.plugins.anvil.get().pluginId) {
// Setup extension
extensions.configure(AnvilExtension::class.java) {
this.generateDaggerFactories.set(generateDaggerFactoriesUsingAnvil)
}
} }
} }
@ -57,3 +68,9 @@ private fun Project.applyPluginIfNeeded(plugin: Provider<PluginDependency>) {
pluginManager.apply(pluginId) pluginManager.apply(pluginId)
} }
} }
enum class ComponentMergingStrategy {
NONE,
KAPT,
KSP
}

View file

@ -7,17 +7,16 @@
package extension package extension
import config.AnalyticsConfig
import ModulesConfig import ModulesConfig
import config.AnalyticsConfig
import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.api.Action import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.artifacts.ExternalModuleDependency import org.gradle.api.artifacts.ExternalModuleDependency
import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.logging.Logger
import org.gradle.kotlin.dsl.DependencyHandlerScope import org.gradle.kotlin.dsl.DependencyHandlerScope
import org.gradle.kotlin.dsl.closureOf import org.gradle.kotlin.dsl.closureOf
import org.gradle.kotlin.dsl.project import org.gradle.kotlin.dsl.project
import java.io.File
private fun DependencyHandlerScope.implementation(dependency: Any) = dependencies.add("implementation", dependency) private fun DependencyHandlerScope.implementation(dependency: Any) = dependencies.add("implementation", dependency)
internal fun DependencyHandler.implementation(dependency: Any) = 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( private fun DependencyHandlerScope.implementation(
dependency: Any, dependency: Any,
config: Action<ExternalModuleDependency> 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) private fun DependencyHandlerScope.androidTestImplementation(dependency: Any) = dependencies.add("androidTestImplementation", dependency)
@ -58,26 +57,6 @@ fun DependencyHandlerScope.composeDependencies(libs: LibrariesForLibs) {
implementation(libs.kotlinx.collections.immutable) 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() { fun DependencyHandlerScope.allLibrariesImpl() {
implementation(project(":libraries:androidutils")) implementation(project(":libraries:androidutils"))
implementation(project(":libraries:deeplink")) implementation(project(":libraries:deeplink"))
@ -128,22 +107,21 @@ fun DependencyHandlerScope.allServicesImpl() {
} }
} }
} }
implementation(project(":services:apperror:impl")) implementation(project(":services:apperror:impl"))
implementation(project(":services:appnavstate:impl")) implementation(project(":services:appnavstate:impl"))
implementation(project(":services:toolbox:impl")) implementation(project(":services:toolbox:impl"))
} }
fun DependencyHandlerScope.allEnterpriseImpl(rootDir: File, logger: Logger) { fun DependencyHandlerScope.allEnterpriseImpl(project: Project) = addAll(project, "enterprise", "impl")
val enterpriseDir = File(rootDir, "enterprise")
addImplementationProjects(enterpriseDir, ":enterprise", "impl", logger)
}
fun DependencyHandlerScope.allFeaturesApi(rootDir: File, logger: Logger) { fun DependencyHandlerScope.allFeaturesImpl(project: Project) = addAll(project, "features", "impl")
val featuresDir = File(rootDir, "features")
addImplementationProjects(featuresDir, ":features", "api", logger)
}
fun DependencyHandlerScope.allFeaturesImpl(rootDir: File, logger: Logger) { fun DependencyHandlerScope.allFeaturesApi(project: Project) = addAll(project, "features", "api")
val featuresDir = File(rootDir, "features")
addImplementationProjects(featuresDir, ":features", "impl", logger) 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 kotlinx.kover.gradle.plugin.dsl.KoverVariantCreateConfig
import org.gradle.api.Action import org.gradle.api.Action
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.configurationcache.extensions.capitalized
import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.assign
@ -142,24 +141,18 @@ fun Project.setupKover() {
} }
} }
filters { filters {
includes { excludes.classes(
classes( "*Fake*Presenter*",
"*Presenter", "io.element.android.appnav.loggedin.LoggedInPresenter$*",
) // Some options can't be tested at the moment
} "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*",
excludes { // Need an Activity to use rememberMultiplePermissionsState
classes( "io.element.android.features.location.impl.common.permissions.DefaultPermissionsPresenter",
"*Fake*Presenter*", "*Presenter\$present\$*",
"io.element.android.appnav.loggedin.LoggedInPresenter$*", // Too small to be > 85% tested
// Some options can't be tested at the moment "io.element.android.libraries.fullscreenintent.impl.DefaultFullScreenIntentPermissionsPresenter",
"io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*", )
// Need an Activity to use rememberMultiplePermissionsState includes.inheritedFrom("io.element.android.libraries.architecture.Presenter")
"io.element.android.features.location.impl.common.permissions.DefaultPermissionsPresenter",
"*Presenter\$present\$*",
// Too small to be > 85% tested
"io.element.android.libraries.fullscreenintent.impl.DefaultFullScreenIntentPermissionsPresenter",
)
}
} }
} }
variant(KoverVariant.States.variantName) { variant(KoverVariant.States.variantName) {
@ -175,33 +168,31 @@ fun Project.setupKover() {
} }
} }
filters { filters {
includes { excludes.classes(
classes( "*State$*", // Exclude inner classes
"^*State$", "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$*",
excludes { "io.element.android.libraries.matrix.api.room.RoomMembershipState*",
classes( "io.element.android.libraries.matrix.api.room.MatrixRoomMembersState*",
"io.element.android.appnav.root.RootNavState*", "io.element.android.libraries.push.impl.notifications.NotificationState*",
"io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*", "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState",
"io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*", "io.element.android.features.messages.impl.media.local.LocalMediaViewState",
"io.element.android.libraries.matrix.api.room.RoomMembershipState*", "io.element.android.features.location.impl.map.MapState*",
"io.element.android.libraries.matrix.api.room.MatrixRoomMembersState*", "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*",
"io.element.android.libraries.push.impl.notifications.NotificationState*", "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*",
"io.element.android.features.messages.impl.media.local.pdf.PdfViewerState", "io.element.android.features.messages.impl.timeline.components.ExpandableState*",
"io.element.android.features.messages.impl.media.local.LocalMediaViewState", "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*",
"io.element.android.features.location.impl.map.MapState*", "io.element.android.libraries.maplibre.compose.CameraPositionState*",
"io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*", "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState",
"io.element.android.libraries.designsystem.swipe.SwipeableActionsState*", "io.element.android.libraries.maplibre.compose.SymbolState*",
"io.element.android.features.messages.impl.timeline.components.ExpandableState*", "io.element.android.features.ftue.api.state.*",
"io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*", "io.element.android.features.ftue.impl.welcome.state.*",
"io.element.android.libraries.maplibre.compose.CameraPositionState*", "io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState",
"io.element.android.libraries.maplibre.compose.SaveableCameraPositionState", "io.element.android.libraries.mediaviewer.api.local.pdf.PdfViewerState",
"io.element.android.libraries.maplibre.compose.SymbolState*", "io.element.android.libraries.textcomposer.model.TextEditorState",
"io.element.android.features.ftue.api.state.*", )
"io.element.android.features.ftue.impl.welcome.state.*", includes.classes("*State")
)
}
} }
} }
variant(KoverVariant.Views.variantName) { variant(KoverVariant.Views.variantName) {
@ -218,11 +209,8 @@ fun Project.setupKover() {
} }
} }
filters { filters {
includes { excludes.classes("*ViewKt$*") // Exclude inner classes
classes( includes.classes("*ViewKt")
"*ViewKt",
)
}
} }
} }
} }
@ -236,16 +224,31 @@ fun Project.applyKoverPluginToAllSubProjects() = rootProject.subprojects {
currentProject { currentProject {
for (variant in koverVariants) { for (variant in koverVariants) {
createVariant(variant) { 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() { fun KoverVariantCreateConfig.defaultVariants(project: Project) {
addWithDependencies("gplayDebug", "debug", optional = true) if (project.name == "app") {
addWithDependencies("gplayDebug")
} else {
addWithDependencies("debug", "jvm", optional = true)
}
} }
fun Project.koverSubprojects() = project.rootProject.subprojects fun Project.koverSubprojects() = project.rootProject.subprojects

View file

@ -29,7 +29,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
@SingleIn(AppScope::class) @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( class DefaultAnalyticsService @Inject constructor(
private val analyticsProviders: Set<@JvmSuppressWildcards AnalyticsProvider>, private val analyticsProviders: Set<@JvmSuppressWildcards AnalyticsProvider>,
private val analyticsStore: AnalyticsStore, private val analyticsStore: AnalyticsStore,

View file

@ -28,7 +28,6 @@ dependencies {
// - Add every single module as a dependency of this one. // - 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. // - Move the Konsist tests to the `app` module, but the `app` module does not need to know about Konsist.
tasks.withType<Test>().configureEach { tasks.withType<Test>().configureEach {
outputs.upToDateWhen { val isNotCheckTask = gradle.startParameter.taskNames.any { it.contains("check", ignoreCase = true).not() }
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) implementation(projects.appnav)
allLibrariesImpl() allLibrariesImpl()
allServicesImpl() allServicesImpl()
allFeaturesImpl(rootDir, logger) allFeaturesImpl(project)
implementation(projects.appicon.element) implementation(projects.appicon.element)
implementation(projects.appicon.enterprise) implementation(projects.appicon.enterprise)

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a12b9a2dfafb1f8cdb329a41c422ebe913b6a8da2f48a217955f43b9f46ac4d
size 23505

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:053b62a43c2ebec26c9d32f9cd002e01f90665be7530d281aad302377585dfae
size 21420

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:306561dca24390baca77657839f96b63f93114e5bce523764903cf9030185320
size 30358

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c0f023eda3a5fd9ff9584e93a6685010de35fb9464025b8c82634be801ada533
size 27476

View file

@ -122,7 +122,7 @@ const previewAnnotations = [
const filesWithPreviews = editedFiles.filter(file => file.endsWith(".kt")).filter(file => { const filesWithPreviews = editedFiles.filter(file => file.endsWith(".kt")).filter(file => {
const content = fs.readFileSync(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'); const composablePreviewProviderContents = fs.readFileSync('tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt');