diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d28af77003..a9f751b556 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml index 43073a06c5..601db71ac7 100644 --- a/.github/workflows/build_enterprise.yml +++ b/.github/workflows/build_enterprise.yml @@ -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: diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index 6908deaa94..bbf1129eff 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -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: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index c2e9aa7877..67c53ccbae 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -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: diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 6aa6c151a7..bb1af3abc4 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -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: diff --git a/.github/workflows/nightly_enterprise.yml b/.github/workflows/nightly_enterprise.yml index 31077c7998..611986010e 100644 --- a/.github/workflows/nightly_enterprise.yml +++ b/.github/workflows/nightly_enterprise.yml @@ -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: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 5d0eaaabc8..37d86a51de 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -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: diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index 424622f8ca..1391aca49c 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -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: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 94aeb3dfd1..17ab2efd18 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 7519240ffb..1a9f2cf598 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -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) }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0cde28968f..45630e6140 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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: diff --git a/anvilcodegen/build.gradle.kts b/anvilcodegen/build.gradle.kts index 96bca85be4..30d244c9a9 100644 --- a/anvilcodegen/build.gradle.kts +++ b/anvilcodegen/build.gradle.kts @@ -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) } diff --git a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeCodeGenerator.kt b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeCodeGenerator.kt deleted file mode 100644 index ddf5d0d795..0000000000 --- a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeCodeGenerator.kt +++ /dev/null @@ -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): Collection { - 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") - } -} diff --git a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt new file mode 100644 index 0000000000..06bb60c55a --- /dev/null +++ b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt @@ -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 { + val annotatedSymbols = resolver.getSymbolsWithAnnotation(ContributesNode::class.qualifiedName!!) + .filterIsInstance() + + 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") + } +} diff --git a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt new file mode 100644 index 0000000000..478e30fbf0 --- /dev/null +++ b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt @@ -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), + ) + } +} diff --git a/anvilcodegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/anvilcodegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000000..d5c111b0b2 --- /dev/null +++ b/anvilcodegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +io.element.android.anvilcodegen.ContributesNodeProcessorProvider diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c16d30f618..b760bd63e1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt b/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt index ba7df0654b..42ff4c830a 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt @@ -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 diff --git a/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt b/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt index 924979d48f..7d5d3b2e5e 100644 --- a/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt +++ b/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt @@ -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 diff --git a/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt b/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt index df5dd9e454..2ff56211e3 100644 --- a/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt @@ -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 diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 6c35f8e3b9..890d06294a 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -22,7 +22,7 @@ android { setupAnvil() dependencies { - allFeaturesApi(rootDir, logger) + allFeaturesApi(project) implementation(projects.libraries.core) implementation(projects.libraries.androidutils) diff --git a/build.gradle.kts b/build.gradle.kts index d8cf2d8fd5..9c90da30ce 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 diff --git a/enterprise b/enterprise index ceb65e32d9..b4f0427e35 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit ceb65e32d95052c028a37654a4a0410639f69053 +Subproject commit b4f0427e3595049d39846aabcdc06e818f2e96ea diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 5c216894eb..bcd3799c92 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -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) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomComponent.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomComponent.kt index d35a77cdb5..be923154ab 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomComponent.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomComponent.kt @@ -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 } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index 5d98741514..fa6bca0ea8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -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 { aPinUnlockState(showBiometricUnlock = false), aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), aPinUnlockState(signOutAction = AsyncAction.Loading), + aPinUnlockState(biometricUnlockResult = BiometricUnlock.AuthenticationResult.Failure( + BiometricUnlockError(BiometricPrompt.ERROR_LOCKOUT, "Biometric auth disabled") + )), ) } diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 04901f4fea..8ea1ad99c5 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -1,3 +1,4 @@ +import extension.ComponentMergingStrategy import extension.setupAnvil /* @@ -23,7 +24,7 @@ android { } } -setupAnvil() +setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP) dependencies { implementation(projects.appconfig) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt index 1c27fbff22..5333e18a43 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt @@ -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 } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeQrCodeLoginComponent.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeMergedQrCodeLoginComponent.kt similarity index 88% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeQrCodeLoginComponent.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeMergedQrCodeLoginComponent.kt index f602400062..6aee8822d4 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeQrCodeLoginComponent.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeMergedQrCodeLoginComponent.kt @@ -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) } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt index aa0f371fa2..78e406e811 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt @@ -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, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionState.kt deleted file mode 100644 index 4ccd47747b..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionState.kt +++ /dev/null @@ -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, -) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt index a7c8e1c5aa..529a280994 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt @@ -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 AndroidComposeTestRule.setPinne onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(), ) { - setContent { + setSafeContent { PinnedMessagesListView( state = state, onBackClick = onBackClick, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 4292e6f226..4dc61b7b80 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -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 AndroidComposeTestRule.setTimel onJoinCallClick: () -> Unit = EnsureNeverCalled(), forceJumpToBottomVisibility: Boolean = false, ) { - setContent { + setSafeContent { TimelineView( state = state, typingNotificationState = typingNotificationState, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/InvitesStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/InvitesStateProvider.kt deleted file mode 100644 index e0e612b2f2..0000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/InvitesStateProvider.kt +++ /dev/null @@ -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 { - override val values: Sequence - get() = sequenceOf( - InvitesState.SeenInvites, - InvitesState.NewInvites, - ) -} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index ae9a721bfa..642c7d879b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -51,12 +51,6 @@ data class RoomListState( } } -enum class InvitesState { - NoInvites, - SeenInvites, - NewInvites, -} - enum class SecurityBannerState { None, SetUpRecovery, diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt index 7e54c385e3..3d8f54d7db 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt @@ -47,6 +47,7 @@ class ResetIdentityFlowManagerTest { var result: AsyncData.Success? = null flowManager.getResetHandle().test { assertThat(awaitItem().isLoading()).isTrue() + @Suppress("UNCHECKED_CAST") result = awaitItem() as? AsyncData.Success assertThat(result).isNotNull() } diff --git a/gradle.properties b/gradle.properties index e787539483..4240f02912 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 129b8aa46f..a01b4cd79b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" diff --git a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTest.kt b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTest.kt index c7aa400ae1..63826aa4e2 100644 --- a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTest.kt +++ b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTest.kt @@ -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 { diff --git a/libraries/mediapickers/impl/build.gradle.kts b/libraries/mediapickers/impl/build.gradle.kts index c5b4a2eca9..de1c018ad4 100644 --- a/libraries/mediapickers/impl/build.gradle.kts +++ b/libraries/mediapickers/impl/build.gradle.kts @@ -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) } diff --git a/libraries/session-storage/impl/build.gradle.kts b/libraries/session-storage/impl/build.gradle.kts index 3170b3dcaa..76d2fae177 100644 --- a/libraries/session-storage/impl/build.gradle.kts +++ b/libraries/session-storage/impl/build.gradle.kts @@ -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("ksp${variantName}Kotlin") { + setSource(tasks.getByName("generate${variantName}SessionDatabaseInterface").outputs) + } + } + } +} diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts index edfd1428da..33d7bb0bd6 100644 --- a/plugins/build.gradle.kts +++ b/plugins/build.gradle.kts @@ -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) } diff --git a/plugins/src/main/kotlin/extension/AnvilExtensions.kt b/plugins/src/main/kotlin/extension/AnvilExtensions.kt index 33f058d25e..c861ce0962 100644 --- a/plugins/src/main/kotlin/extension/AnvilExtensions.kt +++ b/plugins/src/main/kotlin/extension/AnvilExtensions.kt @@ -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() - // 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) { pluginManager.apply(pluginId) } } + +enum class ComponentMergingStrategy { + NONE, + KAPT, + KSP +} diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 554095323a..f54cdb81ca 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -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 -) = dependencies.add("implementation", dependency, closureOf { config.execute(this) }) +) = dependencies.add("implementation", dependency, closureOf { 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.allFeaturesApi(project: Project) = addAll(project, "features", "api") -fun DependencyHandlerScope.allFeaturesImpl(rootDir: File, logger: Logger) { - 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) + } } diff --git a/plugins/src/main/kotlin/extension/KoverExtension.kt b/plugins/src/main/kotlin/extension/KoverExtension.kt index ba84fe37c2..38a43c33dd 100644 --- a/plugins/src/main/kotlin/extension/KoverExtension.kt +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -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 diff --git a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt index 173b07410b..f211c5ecb8 100644 --- a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt +++ b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt @@ -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, diff --git a/tests/konsist/build.gradle.kts b/tests/konsist/build.gradle.kts index 41ae7b64ad..230ce37b92 100644 --- a/tests/konsist/build.gradle.kts +++ b/tests/konsist/build.gradle.kts @@ -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().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 } } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt new file mode 100644 index 0000000000..9d32536391 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt @@ -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 AndroidComposeTestRule.setSafeContent(content: @Composable () -> Unit) { + RobolectricDispatcherCleaner.clearAndroidUiDispatcher() + setContent(content) +} diff --git a/tests/uitests/build.gradle.kts b/tests/uitests/build.gradle.kts index 96573e2dd6..8da1f47a38 100644 --- a/tests/uitests/build.gradle.kts +++ b/tests/uitests/build.gradle.kts @@ -52,7 +52,7 @@ dependencies { implementation(projects.appnav) allLibrariesImpl() allServicesImpl() - allFeaturesImpl(rootDir, logger) + allFeaturesImpl(project) implementation(projects.appicon.element) implementation(projects.appicon.enterprise) diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en.png new file mode 100644 index 0000000000..634b69b361 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a12b9a2dfafb1f8cdb329a41c422ebe913b6a8da2f48a217955f43b9f46ac4d +size 23505 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en.png new file mode 100644 index 0000000000..974d33e340 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:053b62a43c2ebec26c9d32f9cd002e01f90665be7530d281aad302377585dfae +size 21420 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png new file mode 100644 index 0000000000..44c9d9f748 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:306561dca24390baca77657839f96b63f93114e5bce523764903cf9030185320 +size 30358 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png new file mode 100644 index 0000000000..565925137a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0f023eda3a5fd9ff9584e93a6685010de35fb9464025b8c82634be801ada533 +size 27476 diff --git a/tools/danger/dangerfile.js b/tools/danger/dangerfile.js index ae8c4cf002..377f943e50 100644 --- a/tools/danger/dangerfile.js +++ b/tools/danger/dangerfile.js @@ -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');