Browse Source

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>
pull/3580/head
Jorge Martin Espinosa 2 weeks ago committed by GitHub
parent
commit
79c17f714f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .github/workflows/build.yml
  2. 2
      .github/workflows/build_enterprise.yml
  3. 2
      .github/workflows/maestro.yml
  4. 2
      .github/workflows/nightly.yml
  5. 2
      .github/workflows/nightlyReports.yml
  6. 2
      .github/workflows/nightly_enterprise.yml
  7. 2
      .github/workflows/quality.yml
  8. 2
      .github/workflows/recordScreenshots.yml
  9. 2
      .github/workflows/release.yml
  10. 2
      .github/workflows/sonar.yml
  11. 2
      .github/workflows/tests.yml
  12. 5
      anvilcodegen/build.gradle.kts
  13. 144
      anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeCodeGenerator.kt
  14. 169
      anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt
  15. 23
      anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt
  16. 1
      anvilcodegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
  17. 9
      app/build.gradle.kts
  18. 3
      app/src/main/kotlin/io/element/android/x/di/AppComponent.kt
  19. 3
      app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt
  20. 3
      app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt
  21. 2
      appnav/build.gradle.kts
  22. 3
      build.gradle.kts
  23. 2
      enterprise
  24. 3
      features/createroom/impl/build.gradle.kts
  25. 3
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomComponent.kt
  26. 5
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt
  27. 3
      features/login/impl/build.gradle.kts
  28. 3
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt
  29. 6
      features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeMergedQrCodeLoginComponent.kt
  30. 4
      features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt
  31. 13
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionState.kt
  32. 3
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt
  33. 3
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
  34. 18
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/InvitesStateProvider.kt
  35. 6
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
  36. 1
      features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt
  37. 15
      gradle.properties
  38. 14
      gradle/libs.versions.toml
  39. 4
      libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTest.kt
  40. 9
      libraries/mediapickers/impl/build.gradle.kts
  41. 14
      libraries/session-storage/impl/build.gradle.kts
  42. 1
      plugins/build.gradle.kts
  43. 47
      plugins/src/main/kotlin/extension/AnvilExtensions.kt
  44. 46
      plugins/src/main/kotlin/extension/DependencyHandleScope.kt
  45. 53
      plugins/src/main/kotlin/extension/KoverExtension.kt
  46. 2
      services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt
  47. 5
      tests/konsist/build.gradle.kts
  48. 55
      tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt
  49. 2
      tests/uitests/build.gradle.kts
  50. 3
      tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en.png
  51. 3
      tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en.png
  52. 3
      tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png
  53. 3
      tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png
  54. 2
      tools/danger/dangerfile.js

2
.github/workflows/build.yml

@ -9,7 +9,7 @@ on: @@ -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:

2
.github/workflows/build_enterprise.yml

@ -9,7 +9,7 @@ on: @@ -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:

2
.github/workflows/maestro.yml

@ -8,7 +8,7 @@ on: @@ -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:

2
.github/workflows/nightly.yml

@ -7,7 +7,7 @@ on: @@ -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:

2
.github/workflows/nightlyReports.yml

@ -8,7 +8,7 @@ on: @@ -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:

2
.github/workflows/nightly_enterprise.yml

@ -7,7 +7,7 @@ on: @@ -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:

2
.github/workflows/quality.yml

@ -9,7 +9,7 @@ on: @@ -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:

2
.github/workflows/recordScreenshots.yml

@ -7,7 +7,7 @@ on: @@ -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:

2
.github/workflows/release.yml

@ -7,7 +7,7 @@ on: @@ -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:

2
.github/workflows/sonar.yml

@ -9,7 +9,7 @@ on: @@ -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) }}

2
.github/workflows/tests.yml

@ -9,7 +9,7 @@ on: @@ -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:

5
anvilcodegen/build.gradle.kts

@ -7,7 +7,6 @@ @@ -7,7 +7,6 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kapt)
}
dependencies {
@ -16,6 +15,6 @@ 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)
}

144
anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeCodeGenerator.kt

@ -1,144 +0,0 @@ @@ -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")
}
}

169
anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessor.kt

@ -0,0 +1,169 @@ @@ -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")
}
}

23
anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt

@ -0,0 +1,23 @@ @@ -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),
)
}
}

1
anvilcodegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider

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

9
app/build.gradle.kts

@ -11,6 +11,7 @@ import com.android.build.api.variant.FilterConfiguration.FilterType.ABI @@ -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 { @@ -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 { @@ -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)

3
app/src/main/kotlin/io/element/android/x/di/AppComponent.kt

@ -10,7 +10,6 @@ package io.element.android.x.di @@ -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 @@ -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

3
app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt

@ -10,7 +10,6 @@ package io.element.android.x.di @@ -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 @@ -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

3
app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt

@ -10,7 +10,6 @@ package io.element.android.x.di @@ -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 @@ -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

2
appnav/build.gradle.kts

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

3
build.gradle.kts

@ -71,8 +71,9 @@ allprojects { @@ -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

2
enterprise

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

3
features/createroom/impl/build.gradle.kts

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

3
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 @@ -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 @@ -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
}

5
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt

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

3
features/login/impl/build.gradle.kts

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

3
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 @@ -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 @@ -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
}

6
features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeQrCodeLoginComponent.kt → 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 @@ -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)
}
}

4
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 @@ -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 { @@ -200,7 +200,7 @@ class QrCodeLoginFlowNodeTest {
return QrCodeLoginFlowNode(
buildContext = buildContext,
plugins = emptyList(),
qrCodeLoginComponentBuilder = FakeQrCodeLoginComponent.Builder(qrCodeLoginManager),
qrCodeLoginComponentBuilder = FakeMergedQrCodeLoginComponent.Builder(qrCodeLoginManager),
defaultLoginUserStory = defaultLoginUserStory,
coroutineDispatchers = coroutineDispatchers,
)

13
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionState.kt

@ -1,13 +0,0 @@ @@ -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,
)

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

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

18
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/InvitesStateProvider.kt

@ -1,18 +0,0 @@ @@ -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,
)
}

6
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt

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

1
features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt

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

15
gradle.properties

@ -25,12 +25,13 @@ kotlin.code.style=official @@ -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 @@ -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

14
gradle/libs.versions.toml

@ -5,6 +5,7 @@ @@ -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" @@ -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" @@ -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" @@ -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" } @@ -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"

4
libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTest.kt

@ -8,7 +8,7 @@ @@ -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 { @@ -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 {

9
libraries/mediapickers/impl/build.gradle.kts

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
import extension.setupAnvil
/*
* Copyright 2023, 2024 New Vector Ltd.
*
@ -7,16 +9,17 @@ @@ -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 {
dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.di)
implementation(libs.inject)
api(projects.libraries.mediapickers.api)
}
}

14
libraries/session-storage/impl/build.gradle.kts

@ -1,4 +1,6 @@ @@ -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 { @@ -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)
}
}
}
}

1
plugins/build.gradle.kts

@ -23,4 +23,5 @@ dependencies { @@ -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)
}

47
plugins/src/main/kotlin/extension/AnvilExtensions.kt

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

46
plugins/src/main/kotlin/extension/DependencyHandleScope.kt

@ -7,17 +7,16 @@ @@ -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)
@ -58,26 +57,6 @@ fun DependencyHandlerScope.composeDependencies(libs: LibrariesForLibs) { @@ -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() { @@ -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)
}
}

53
plugins/src/main/kotlin/extension/KoverExtension.kt

@ -14,7 +14,6 @@ import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension @@ -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,13 +141,7 @@ fun Project.setupKover() { @@ -142,13 +141,7 @@ fun Project.setupKover() {
}
}
filters {
includes {
classes(
"*Presenter",
)
}
excludes {
classes(
excludes.classes(
"*Fake*Presenter*",
"io.element.android.appnav.loggedin.LoggedInPresenter$*",
// Some options can't be tested at the moment
@ -159,7 +152,7 @@ fun Project.setupKover() { @@ -159,7 +152,7 @@ fun Project.setupKover() {
// 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,13 +168,8 @@ fun Project.setupKover() { @@ -175,13 +168,8 @@ fun Project.setupKover() {
}
}
filters {
includes {
classes(
"^*State$",
)
}
excludes {
classes(
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$*",
@ -200,8 +188,11 @@ fun Project.setupKover() { @@ -200,8 +188,11 @@ fun Project.setupKover() {
"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() { @@ -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 { @@ -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

2
services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt

@ -29,7 +29,7 @@ import java.util.concurrent.atomic.AtomicBoolean @@ -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,

5
tests/konsist/build.gradle.kts

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

55
tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt

@ -0,0 +1,55 @@ @@ -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)
}

2
tests/uitests/build.gradle.kts

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

3
tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en.png

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

3
tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en.png

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

3
tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png

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

3
tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png

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

2
tools/danger/dangerfile.js

@ -122,7 +122,7 @@ const previewAnnotations = [ @@ -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');

Loading…
Cancel
Save