diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f136fdea5..fab988acb4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,7 +32,7 @@ jobs: - uses: actions/setup-java@v3 with: distribution: 'adopt' - java-version: '11' + java-version: '17' - uses: gradle/gradle-build-action@v2 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} diff --git a/ELEMENT_CHANGES.md b/ELEMENT_CHANGES.md index 5a90fd3f01..e3e7f46744 100644 --- a/ELEMENT_CHANGES.md +++ b/ELEMENT_CHANGES.md @@ -1,3 +1,17 @@ +Changes in Element v1.6.18 (2024-06-25) +======================================= + +Bugfixes 🐛 +---------- + - Fix redacted events not grouped correctly when hidden events are inserted between. ([#8840](https://github.com/element-hq/element-android/issues/8840)) + - Element-Android session doesn't encrypt for a dehydrated device ([#8842](https://github.com/element-hq/element-android/issues/8842)) + - Intercept only links from `element.io` well known hosts. The previous behaviour broke OIDC login in Element X. ([#8894](https://github.com/element-hq/element-android/issues/8894)) + +Other changes +------------- + - Posthog | report platform code for EA ([#8839](https://github.com/element-hq/element-android/issues/8839)) + + Changes in Element v1.6.16 (2024-05-29) ======================================= diff --git a/TCHAP_CHANGES.md b/TCHAP_CHANGES.md index 7c3aeef4d9..dbecda2039 100644 --- a/TCHAP_CHANGES.md +++ b/TCHAP_CHANGES.md @@ -1,3 +1,15 @@ +Changes in Tchap 2.12.0 (2024-07-22) +==================================== + +Features ✹ +---------- + - Forcer la gĂ©nĂ©ration du code de rĂ©cupĂ©ration et la sauvegarde automatique. ([#988](https://github.com/tchapgouv/tchap-android/issues/988)) + +Improvements 🙌 +-------------- + - Rebase against Element-Android v1.6.18 ([#1074](https://github.com/tchapgouv/tchap-android/issues/1074)) + - Mise Ă  jour du style quand un utilisateur est mentionnĂ©. ([#1075](https://github.com/tchapgouv/tchap-android/issues/1075)) + Changes in Tchap 2.11.11 (2024-06-24) ===================================== diff --git a/dependencies.gradle b/dependencies.gradle index 3ebf633a17..05d680eaf3 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -102,7 +102,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:2.35.0" + 'wysiwyg' : "io.element.android:wysiwyg:2.37.3" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", diff --git a/fastlane/metadata/android/en-US/changelogs/40106180.txt b/fastlane/metadata/android/en-US/changelogs/40106180.txt new file mode 100644 index 0000000000..bc5a0f731a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40106180.txt @@ -0,0 +1,2 @@ +Main changes in this version: Bugfixes. +Full changelog: https://github.com/element-hq/element-android/releases diff --git a/library/external/realmfieldnameshelper/bin/main/META-INF/gradle/incremental.annotation.processors b/library/external/realmfieldnameshelper/bin/main/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 0000000000..57897c8297 --- /dev/null +++ b/library/external/realmfieldnameshelper/bin/main/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +dk.ilios.realmfieldnames.RealmFieldNamesProcessor,aggregating \ No newline at end of file diff --git a/library/external/realmfieldnameshelper/bin/main/META-INF/services/javax.annotation.processing.Processor b/library/external/realmfieldnameshelper/bin/main/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000000..58fadd699c --- /dev/null +++ b/library/external/realmfieldnameshelper/bin/main/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +dk.ilios.realmfieldnames.RealmFieldNamesProcessor \ No newline at end of file diff --git a/library/external/realmfieldnameshelper/bin/main/dk/ilios/realmfieldnames/ClassData.kt b/library/external/realmfieldnameshelper/bin/main/dk/ilios/realmfieldnames/ClassData.kt new file mode 100644 index 0000000000..d683a2adef --- /dev/null +++ b/library/external/realmfieldnameshelper/bin/main/dk/ilios/realmfieldnames/ClassData.kt @@ -0,0 +1,24 @@ +package dk.ilios.realmfieldnames + +import java.util.TreeMap + +/** + * Class responsible for keeping track of the metadata for each Realm model class. + */ +class ClassData(val packageName: String?, val simpleClassName: String, val libraryClass: Boolean = false) { + + val fields = TreeMap() // + + fun addField(field: String, linkedType: String?) { + fields.put(field, linkedType) + } + + val qualifiedClassName: String + get() { + if (packageName != null && !packageName.isEmpty()) { + return packageName + "." + simpleClassName + } else { + return simpleClassName + } + } +} diff --git a/library/external/realmfieldnameshelper/bin/main/dk/ilios/realmfieldnames/FieldNameFormatter.kt b/library/external/realmfieldnameshelper/bin/main/dk/ilios/realmfieldnames/FieldNameFormatter.kt new file mode 100644 index 0000000000..95f0024721 --- /dev/null +++ b/library/external/realmfieldnameshelper/bin/main/dk/ilios/realmfieldnames/FieldNameFormatter.kt @@ -0,0 +1,79 @@ +package dk.ilios.realmfieldnames + +import java.util.Locale + +/** + * Class for encapsulating the rules for converting between the field name in the Realm model class + * and the matching name in the "<class>Fields" class. + */ +class FieldNameFormatter { + + @JvmOverloads + fun format(fieldName: String?, locale: Locale = Locale.US): String { + if (fieldName == null || fieldName == "") { + return "" + } + + // Normalize word separator chars + val normalizedFieldName: String = fieldName.replace('-', '_') + + // Iterate field name using the following rules + // lowerCase m followed by upperCase anything is considered hungarian notation + // lowercase char followed by uppercase char is considered camel case + // Two uppercase chars following each other is considered non-standard camelcase + // _ and - are treated as word separators + val result = StringBuilder(normalizedFieldName.length) + + if (normalizedFieldName.codePointCount(0, normalizedFieldName.length) == 1) { + result.append(normalizedFieldName) + } else { + var previousCodepoint: Int? + var currentCodepoint: Int? = null + val length = normalizedFieldName.length + var offset = 0 + while (offset < length) { + previousCodepoint = currentCodepoint + currentCodepoint = normalizedFieldName.codePointAt(offset) + + if (previousCodepoint != null) { + if (Character.isUpperCase(currentCodepoint) && + !Character.isUpperCase(previousCodepoint) && + previousCodepoint === 'm'.code as Int? && + result.length == 1 + ) { + // Hungarian notation starting with: mX + result.delete(0, 1) + result.appendCodePoint(currentCodepoint) + } else if (Character.isUpperCase(currentCodepoint) && Character.isUpperCase(previousCodepoint)) { + // InvalidCamelCase: XXYx (should have been xxYx) + if (offset + Character.charCount(currentCodepoint) < normalizedFieldName.length) { + val nextCodePoint = normalizedFieldName.codePointAt(offset + Character.charCount(currentCodepoint)) + if (Character.isLowerCase(nextCodePoint)) { + result.append("_") + } + } + result.appendCodePoint(currentCodepoint) + } else if (currentCodepoint === '-'.code as Int? || currentCodepoint === '_'.code as Int?) { + // Word-separator: x-x or x_x + result.append("_") + } else if (Character.isUpperCase(currentCodepoint) && !Character.isUpperCase(previousCodepoint) && Character.isLetterOrDigit( + previousCodepoint + )) { + // camelCase: xX + result.append("_") + result.appendCodePoint(currentCodepoint) + } else { + // Unknown type + result.appendCodePoint(currentCodepoint) + } + } else { + // Only triggered for first code point + result.appendCodePoint(currentCodepoint) + } + offset += Character.charCount(currentCodepoint) + } + } + + return result.toString().uppercase(locale) + } +} diff --git a/library/external/realmfieldnameshelper/bin/main/dk/ilios/realmfieldnames/FileGenerator.kt b/library/external/realmfieldnameshelper/bin/main/dk/ilios/realmfieldnames/FileGenerator.kt new file mode 100644 index 0000000000..2ddba1ccbd --- /dev/null +++ b/library/external/realmfieldnameshelper/bin/main/dk/ilios/realmfieldnames/FileGenerator.kt @@ -0,0 +1,77 @@ +package dk.ilios.realmfieldnames + +import com.squareup.javapoet.FieldSpec +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.TypeSpec +import java.io.IOException +import javax.annotation.processing.Filer +import javax.lang.model.element.Modifier + +/** + * Class responsible for creating the final output files. + */ +class FileGenerator(private val filer: Filer) { + private val formatter: FieldNameFormatter + + init { + this.formatter = FieldNameFormatter() + } + + /** + * Generates all the "<class>Fields" fields with field name references. + * @param fileData Files to create. + * * + * @return `true` if the files where generated, `false` if not. + */ + fun generate(fileData: Set): Boolean { + return fileData + .filter { !it.libraryClass } + .all { generateFile(it, fileData) } + } + + private fun generateFile(classData: ClassData, classPool: Set): Boolean { + val fileBuilder = TypeSpec.classBuilder(classData.simpleClassName + "Fields") + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addJavadoc("This class enumerate all queryable fields in {@link \$L.\$L}\n", + classData.packageName, classData.simpleClassName) + + // Add a static field reference to each queryable field in the Realm model class + classData.fields.forEach { fieldName, value -> + if (value != null) { + // Add linked field names (only up to depth 1) + for (data in classPool) { + if (data.qualifiedClassName == value) { + val linkedTypeSpec = TypeSpec.classBuilder(formatter.format(fieldName)) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) + val linkedClassFields = data.fields + addField(linkedTypeSpec, "$", fieldName) + for (linkedFieldName in linkedClassFields.keys) { + addField(linkedTypeSpec, linkedFieldName, fieldName + "." + linkedFieldName) + } + fileBuilder.addType(linkedTypeSpec.build()) + } + } + } else { + // Add normal field name + addField(fileBuilder, fieldName, fieldName) + } + } + + val javaFile = JavaFile.builder(classData.packageName, fileBuilder.build()).build() + try { + javaFile.writeTo(filer) + return true + } catch (e: IOException) { + // e.printStackTrace() + return false + } + } + + private fun addField(fileBuilder: TypeSpec.Builder, fieldName: String, fieldNameValue: String) { + val field = FieldSpec.builder(String::class.java, formatter.format(fieldName)) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .initializer("\$S", fieldNameValue) + .build() + fileBuilder.addField(field) + } +} diff --git a/library/external/realmfieldnameshelper/bin/main/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt b/library/external/realmfieldnameshelper/bin/main/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt new file mode 100644 index 0000000000..29d044c46c --- /dev/null +++ b/library/external/realmfieldnameshelper/bin/main/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt @@ -0,0 +1,197 @@ +package dk.ilios.realmfieldnames + +import javax.annotation.processing.AbstractProcessor +import javax.annotation.processing.Messager +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.RoundEnvironment +import javax.annotation.processing.SupportedAnnotationTypes +import javax.lang.model.SourceVersion +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.Modifier +import javax.lang.model.element.PackageElement +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.TypeMirror +import javax.lang.model.util.Elements +import javax.lang.model.util.Types +import javax.tools.Diagnostic + +/** + * The Realm Field Names Generator is a processor that looks at all available Realm model classes + * and create an companion class with easy, type-safe access to all field names. + */ + +@SupportedAnnotationTypes("io.realm.annotations.RealmClass") +class RealmFieldNamesProcessor : AbstractProcessor() { + + private val classes = HashSet() + private lateinit var typeUtils: Types + private lateinit var messager: Messager + private lateinit var elementUtils: Elements + private var ignoreAnnotation: TypeMirror? = null + private var realmClassAnnotation: TypeElement? = null + private var realmModelInterface: TypeMirror? = null + private var realmListClass: DeclaredType? = null + private var realmResultsClass: DeclaredType? = null + private var fileGenerator: FileGenerator? = null + private var done = false + + @Synchronized + override fun init(processingEnv: ProcessingEnvironment) { + super.init(processingEnv) + typeUtils = processingEnv.typeUtils!! + messager = processingEnv.messager!! + elementUtils = processingEnv.elementUtils!! + + // If the Realm class isn't found something is wrong the project setup. + // Most likely Realm isn't on the class path, so just disable the + // annotation processor + val isRealmAvailable = elementUtils.getTypeElement("io.realm.Realm") != null + if (!isRealmAvailable) { + done = true + } else { + ignoreAnnotation = elementUtils.getTypeElement("io.realm.annotations.Ignore")?.asType() + realmClassAnnotation = elementUtils.getTypeElement("io.realm.annotations.RealmClass") + realmModelInterface = elementUtils.getTypeElement("io.realm.RealmModel")?.asType() + realmListClass = typeUtils.getDeclaredType( + elementUtils.getTypeElement("io.realm.RealmList"), + typeUtils.getWildcardType(null, null) + ) + realmResultsClass = typeUtils.getDeclaredType( + elementUtils.getTypeElement("io.realm.RealmResults"), + typeUtils.getWildcardType(null, null) + ) + fileGenerator = FileGenerator(processingEnv.filer) + } + } + + override fun getSupportedSourceVersion(): SourceVersion { + return SourceVersion.latestSupported() + } + + override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { + if (done) { + return CONSUME_ANNOTATIONS + } + + // Create all proxy classes + roundEnv.getElementsAnnotatedWith(realmClassAnnotation).forEach { classElement -> + if (typeUtils.isAssignable(classElement.asType(), realmModelInterface)) { + val classData = processClass(classElement as TypeElement) + classes.add(classData) + } + } + + // If a model class references a library class, the library class will not be part of this + // annotation processor round. For all those references we need to pull field information + // from the classpath instead. + val libraryClasses = HashMap() + classes.forEach { + it.fields.forEach { _, value -> + // Analyze the library class file the first time it is encountered. + if (value != null) { + if (classes.all { it.qualifiedClassName != value } && !libraryClasses.containsKey(value)) { + libraryClasses.put(value, processLibraryClass(value)) + } + } + } + } + classes.addAll(libraryClasses.values) + + done = fileGenerator!!.generate(classes) + return CONSUME_ANNOTATIONS + } + + private fun processClass(classElement: TypeElement): ClassData { + val packageName = getPackageName(classElement) + val className = classElement.simpleName.toString() + val data = ClassData(packageName, className) + + // Find all appropriate fields + classElement.enclosedElements.forEach { + val elementKind = it.kind + if (elementKind == ElementKind.FIELD) { + val variableElement = it as VariableElement + + val modifiers = variableElement.modifiers + if (modifiers.contains(Modifier.STATIC)) { + return@forEach // completely ignore any static fields + } + + // Don't add any fields marked with @Ignore + val ignoreField = variableElement.annotationMirrors + .map { it.annotationType.toString() } + .contains("io.realm.annotations.Ignore") + + if (!ignoreField) { + data.addField(it.getSimpleName().toString(), getLinkedFieldType(it)) + } + } + } + + return data + } + + private fun processLibraryClass(qualifiedClassName: String): ClassData { + val libraryClass = Class.forName(qualifiedClassName) // Library classes should be on the classpath + val packageName = libraryClass.`package`.name + val className = libraryClass.simpleName + val data = ClassData(packageName, className, libraryClass = true) + + libraryClass.declaredFields.forEach { field -> + if (java.lang.reflect.Modifier.isStatic(field.modifiers)) { + return@forEach // completely ignore any static fields + } + + // Add field if it is not being ignored. + if (field.annotations.all { it.toString() != "io.realm.annotations.Ignore" }) { + data.addField(field.name, field.type.name) + } + } + + return data + } + + /** + * Returns the qualified name of the linked Realm class field or `null` if it is not a linked + * class. + */ + private fun getLinkedFieldType(field: Element): String? { + if (typeUtils.isAssignable(field.asType(), realmModelInterface)) { + // Object link + val typeElement = elementUtils.getTypeElement(field.asType().toString()) + return typeElement.qualifiedName.toString() + } else if (typeUtils.isAssignable(field.asType(), realmListClass) || typeUtils.isAssignable(field.asType(), realmResultsClass)) { + // List link or LinkingObjects + val fieldType = field.asType() + val typeArguments = (fieldType as DeclaredType).typeArguments + if (typeArguments.size == 0) { + return null + } + return typeArguments[0].toString() + } else { + return null + } + } + + private fun getPackageName(classElement: TypeElement): String? { + val enclosingElement = classElement.enclosingElement + + if (enclosingElement.kind != ElementKind.PACKAGE) { + messager.printMessage( + Diagnostic.Kind.ERROR, + "Could not determine the package name. Enclosing element was: " + enclosingElement.kind + ) + return null + } + + val packageElement = enclosingElement as PackageElement + return packageElement.qualifiedName.toString() + } + + companion object { + private const val CONSUME_ANNOTATIONS = false + } +} diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml index 340611eda6..7c88b18119 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -1292,7 +1292,7 @@ Nom d’utilisateur Outils de dĂ©veloppement DonnĂ©es du compte - Utiliser le Code de RĂ©cupĂ©ration + VĂ©rifier avec le Code de RĂ©cupĂ©ration Si vous n’avez pas accĂšs Ă  un appareil existant Impossible de trouver les secrets dans le stockage Supprimer
 @@ -1521,7 +1521,7 @@ EN SAVOIR PLUS COMPRIS L’apparence et l’organisation de votre application Ă©voluent.\nDe nouveaux changements seront introduits progressivement pour faciliter et enrichir votre expĂ©rience. - Bienvenue dans la nouvelle version de Tchap ! + Bienvenue dans la nouvelle version de ${app_name} ! Attente de l’historique du chiffrement Impossible d’accĂ©der Ă  ce message car l’envoyeur n’a intentionnellement pas envoyĂ© les clĂ©s Vous ne pouvez pas accĂ©der Ă  ce message car l’envoyeur n’a pas confiance en votre appareil @@ -1542,9 +1542,9 @@ DĂ©finir le rĂŽle Vous redĂ©marrerez sans aucun historique, message, appareil ou utilisateurs connus Si vous rĂ©initialisez tout - Faites uniquement ceci si vous n\'avez aucun autre appareil pouvant vĂ©rifier celui-ci. + Uniquement si vous avez perdu votre Code et n\'avez aucun autre appareil connectĂ© Ă  ${app_name}. RĂ©initialiser tout - Vous avez perdu votre Code de RĂ©cupĂ©ration ? GĂ©nĂ©rez-en un nouveau. + GĂ©nĂ©rer un nouveau Code de RĂ©cupĂ©ration Impossible d’enregistrer le fichier multimĂ©dia Ce compte a Ă©tĂ© dĂ©sactivĂ©. Si vous annulez maintenant, vous pourrez perdre les messages et donnĂ©es chiffrĂ©s si vous perdez accĂšs Ă  vos identifiants. @@ -2940,12 +2940,12 @@ Message Message de %s ChiffrĂ© par un appareil supprimĂ© - Veuillez ne continuer que si vous ĂȘtes certain d’avoir perdu tous vos autres appareils et votre clĂ© de sĂ©curitĂ©. - La rĂ©initialisation de vos clĂ©s de vĂ©rification ne peut pas ĂȘtre annulĂ©. AprĂšs la rĂ©initialisation, vous n’aurez plus accĂšs Ă  vos anciens messages chiffrĂ©s, et tous les amis que vous aviez prĂ©cĂ©demment vĂ©rifiĂ©s verront des avertissement de sĂ©curitĂ© jusqu\'Ă  ce vous les vĂ©rifiiez Ă  nouveau. + Veuillez ne continuer que si vous ĂȘtes certain d’avoir perdu tous vos autres appareils et votre Code de RĂ©cupĂ©ration. + La rĂ©initialisation ne peut pas ĂȘtre annulĂ©e. Vous n’aurez plus accĂšs Ă  vos anciens messages chiffrĂ©s. La demande de vĂ©rification n’a pas Ă©tĂ© trouvĂ©e. Elle a peut-ĂȘtre Ă©tĂ© annulĂ©e, ou prise en charge dans une autre session. - Une demande de vĂ©rification a Ă©tĂ© envoyĂ©e. Ouvrez l’une de vos autres sessions pour accepter et commencer la vĂ©rification. + Une demande de vĂ©rification a Ă©tĂ© envoyĂ©e. Ouvrez ${app_name} sur l’un de vos autres appareils pour accepter et commencer la vĂ©rification. Reprendre - VĂ©rifiez votre identitĂ© pour accĂ©der aux messages chiffrĂ©s et prouver votre identitĂ© aux autres. + VĂ©rifiez cet appareil pour accĂ©der Ă  vos messages. VĂ©rifier avec un autre appareil VĂ©rification depuis la clĂ© ou phrase de sĂ©curité  Politique d’utilisation acceptable diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 584dc098cc..f1aa6a575a 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -62,7 +62,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.6.16\"" + buildConfigField "String", "SDK_VERSION", "\"1.6.18\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt index 0560cfec95..7fe8a75dc9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt @@ -26,10 +26,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.util.TextContent -import org.matrix.android.sdk.common.TestRoomDisplayNameFallbackProvider -import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver import org.matrix.android.sdk.internal.session.room.send.pills.MentionLinkSpecComparator import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils @@ -56,12 +53,6 @@ class MarkdownParserTest : InstrumentedTest { HtmlRenderer.builder().softbreak("
").build(), TextPillsUtils( MentionLinkSpecComparator(), - DisplayNameResolver( - MatrixConfiguration( - applicationFlavor = "TestFlavor", - roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider() - ) - ), TestPermalinkService() ) ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt index 6b297be352..210d4f9552 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -151,5 +151,12 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) { fun getSdkVersion(): String { return BuildConfig.SDK_VERSION + " (" + BuildConfig.GIT_SDK_REVISION + ")" } + + fun getCryptoVersion(longFormat: Boolean): String { + val version = org.matrix.rustcomponents.sdk.crypto.version() + val gitHash = org.matrix.rustcomponents.sdk.crypto.versionInfo().gitSha + val vodozemac = org.matrix.rustcomponents.sdk.crypto.vodozemacVersion() + return if (longFormat) "Rust SDK $version ($gitHash), Vodozemac $vodozemac" else version + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 3ed6dd1450..fa1208059a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.api.session.crypto -import android.content.Context import androidx.annotation.Size import androidx.lifecycle.LiveData import androidx.paging.PagedList @@ -61,8 +60,6 @@ interface CryptoService { suspend fun deleteDevices(@Size(min = 1) deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) - fun getCryptoVersion(context: Context, longFormat: Boolean): String - fun isCryptoEnabled(): Boolean fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt index 5ba74f705b..a6e4efd875 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.crypto -import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.map import androidx.paging.PagedList @@ -184,13 +183,6 @@ internal class RustCryptoService @Inject constructor( deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor) } - override fun getCryptoVersion(context: Context, longFormat: Boolean): String { - val version = org.matrix.rustcomponents.sdk.crypto.version() - val gitHash = org.matrix.rustcomponents.sdk.crypto.versionInfo().gitSha - val vodozemac = org.matrix.rustcomponents.sdk.crypto.vodozemacVersion() - return if (longFormat) "Rust SDK $version ($gitHash), Vodozemac $vodozemac" else version - } - override suspend fun getMyCryptoDevice(): CryptoDeviceInfo = withContext(coroutineDispatchers.io) { olmMachine.ownDevice() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt index de9b3f24ff..119372ad32 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt @@ -18,23 +18,10 @@ package org.matrix.android.sdk.internal.crypto.model import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys -import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeysWithUnsigned import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo internal object CryptoInfoMapper { - fun map(deviceKeysWithUnsigned: DeviceKeysWithUnsigned): CryptoDeviceInfo { - return CryptoDeviceInfo( - deviceId = deviceKeysWithUnsigned.deviceId, - userId = deviceKeysWithUnsigned.userId, - algorithms = deviceKeysWithUnsigned.algorithms, - keys = deviceKeysWithUnsigned.keys, - signatures = deviceKeysWithUnsigned.signatures, - unsigned = deviceKeysWithUnsigned.unsigned, - trustLevel = null - ) - } - fun map(cryptoDeviceInfo: CryptoDeviceInfo): DeviceKeys { return DeviceKeys( deviceId = cryptoDeviceInfo.deviceId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt deleted file mode 100644 index 32f577c99b..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo - -@JsonClass(generateAdapter = true) -internal data class DeviceKeysWithUnsigned( - /** - * Required. The ID of the user the device belongs to. Must match the user ID used when logging in. - */ - @Json(name = "user_id") - val userId: String, - - /** - * Required. The ID of the device these keys belong to. Must match the device ID used when logging in. - */ - @Json(name = "device_id") - val deviceId: String, - - /** - * Required. The encryption algorithms supported by this device. - */ - @Json(name = "algorithms") - val algorithms: List?, - - /** - * Required. Public identity keys. The names of the properties should be in the format :. - * The keys themselves should be encoded as specified by the key algorithm. - */ - @Json(name = "keys") - val keys: Map?, - - /** - * Required. Signatures for the device key object. A map from user ID, to a map from : to the signature. - * The signature is calculated using the process described at https://matrix.org/docs/spec/appendices.html#signing-json. - */ - @Json(name = "signatures") - val signatures: Map>?, - - /** - * Additional data added to the device key information by intermediate servers, and not covered by the signatures. - */ - @Json(name = "unsigned") - val unsigned: UnsignedDeviceInfo? = null -) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryResponse.kt index a099419c3c..ce10af8c67 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysQueryResponse.kt @@ -34,7 +34,7 @@ internal data class KeysQueryResponse( * For each device, the information returned will be the same as uploaded via /keys/upload, with the addition of an unsigned property. */ @Json(name = "device_keys") - val deviceKeys: Map>? = null, + val deviceKeys: Map>>? = null, /** * If any remote homeservers could not be reached, they are recorded here. The names of the diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt index 70af859ddb..8fd943afcc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.matrix.android.sdk.internal.crypto.api.CryptoApi -import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeysWithUnsigned import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo @@ -52,7 +51,7 @@ internal class DefaultDownloadKeysForUsers @Inject constructor( return if (bestChunkSize.shouldChunk()) { // Store server results in these mutable maps - val deviceKeys = mutableMapOf>() + val deviceKeys = mutableMapOf>>() val failures = mutableMapOf>() val masterKeys = mutableMapOf() val selfSigningKeys = mutableMapOf() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt index 38bddae951..4ac0b4d674 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt @@ -19,7 +19,6 @@ import android.text.SpannableString import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan import org.matrix.android.sdk.api.util.MatrixItem -import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver import java.util.Collections import javax.inject.Inject @@ -29,7 +28,6 @@ import javax.inject.Inject */ internal class TextPillsUtils @Inject constructor( private val mentionLinkSpecComparator: MentionLinkSpecComparator, - private val displayNameResolver: DisplayNameResolver, private val permalinkService: PermalinkService ) { @@ -70,7 +68,7 @@ internal class TextPillsUtils @Inject constructor( // append text before pill append(text, currIndex, start) // append the pill - append(String.format(template, urlSpan.matrixItem.id, displayNameResolver.getBestName(urlSpan.matrixItem))) + append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.id)) currIndex = end } // append text after the last pill diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/KeysQueryResponseTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/KeysQueryResponseTest.kt new file mode 100644 index 0000000000..d4bf2b9e70 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/KeysQueryResponseTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.amshove.kluent.internal.assertEquals +import org.junit.Test +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse +import org.matrix.android.sdk.internal.di.MoshiProvider + +class KeysQueryResponseTest { + + private val moshi = MoshiProvider.providesMoshi() + private val keysQueryResponseAdapter = moshi.adapter(KeysQueryResponse::class.java) + + private fun aKwysQueryResponseWithDehydrated(): KeysQueryResponse { + val rawResponseWithDehydratedDevice = """ + { + "device_keys": { + "@dehydration2:localhost": { + "TDHZGMDVNO": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "device_id": "TDHZGMDVNO", + "keys": { + "curve25519:TDHZGMDVNO": "ClMOrHlQJqaqr4oESYyPURwD4BSQxMlEZZk/AnYxVSk", + "ed25519:TDHZGMDVNO": "5iZ4zfk0URyIH8YOIWnXmJo41Vn34IixGYphkMdDzik" + }, + "signatures": { + "@dehydration2:localhost": { + "ed25519:TDHZGMDVNO": "O6VP+ELiCVAJGHaRdReKga0LGMQahjRnp4znZH7iJO6maZV8aSXnpugSoVsSPRvQ4GBkjX+KXAXU+ODZ0J8MDg", + "ed25519:YZ0EmlbDX+t/m/MB5EWkQLw8cEDg7hX4Zy9699h3hd8": "lG3idYliFGOAe4F/7tENIQ6qI0d41VQKY34BHyVvvWKbv63zDDO5kBTwBeXfUSEeRqyxET3SXLXfB1D8E8LUDg" + } + }, + "user_id": "@dehydration2:localhost", + "unsigned": { + "device_display_name": "localhost:8080: Chrome on macOS" + } + }, + "Y2gISVBZ024gKKAe6Xos44cDbNlO/49YjaOyiqFwjyQ": { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" + ], + "dehydrated": true, + "device_id": "Y2gISVBZ024gKKAe6Xos44cDbNlO/49YjaOyiqFwjyQ", + "keys": { + "curve25519:Y2gISVBZ024gKKAe6Xos44cDbNlO/49YjaOyiqFwjyQ": "Y2gISVBZ024gKKAe6Xos44cDbNlO/49YjaOyiqFwjyQ", + "ed25519:Y2gISVBZ024gKKAe6Xos44cDbNlO/49YjaOyiqFwjyQ": "sVY5Xq13sIdhC4We/p5CH69++GsIWRNUhHijtucBirs" + }, + "signatures": { + "@dehydration2:localhost": { + "ed25519:Y2gISVBZ024gKKAe6Xos44cDbNlO/49YjaOyiqFwjyQ": "e2aVrdnD/kor2T0Ok/4SC32MW4WB5JXFSd2wnXV8apxFJBfbdZErANiUbo1Zz/HAasaXM5NBfkr/9gVTdph9BQ", + "ed25519:YZ0EmlbDX+t/m/MB5EWkQLw8cEDg7hX4Zy9699h3hd8": "rVzeE1LbB12XOlckxjRLjt3eq2jVlek6OJ4p08+8g8CMoiJDcw1OVzbJuG/8u6ryarxQF6Yqr4Xu2TqCPBmHDw" + } + }, + "user_id": "@dehydration2:localhost", + "unsigned": { + "device_display_name": "Dehydrated device" + } + } + } + } + } + """.trimIndent() + + return keysQueryResponseAdapter.fromJson(rawResponseWithDehydratedDevice)!! + } + + @Test + fun `Should parse correctly devices with new dehydrated field`() { + val aKeysQueryResponse = aKwysQueryResponseWithDehydrated() + + val pojoToJson = keysQueryResponseAdapter.toJson(aKeysQueryResponse) + + val rawAdapter = moshi.adapter(Map::class.java) + + val rawJson = rawAdapter.fromJson(pojoToJson)!! + + val deviceKeys = (rawJson["device_keys"] as Map<*, *>)["@dehydration2:localhost"] as Map<*, *> + + assertEquals(deviceKeys.keys.size, 2) + + val dehydratedDevice = deviceKeys["Y2gISVBZ024gKKAe6Xos44cDbNlO/49YjaOyiqFwjyQ"] as Map<*, *> + + assertEquals(dehydratedDevice["dehydrated"] as? Boolean, true) + } +} diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh index f31899551e..92ac7c1a7b 100755 --- a/tools/release/releaseScript.sh +++ b/tools/release/releaseScript.sh @@ -66,7 +66,7 @@ if [ ${envError} == 1 ]; then exit 1 fi -buildToolsVersion="30.0.2" +buildToolsVersion="35.0.0" buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}" if [[ ! -d ${buildToolsPath} ]]; then diff --git a/towncrier.toml b/towncrier.toml index c68d5f538b..c7cfa32428 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,5 +1,5 @@ [tool.towncrier] - version = "2.11.11" + version = "2.12.0" directory = "changelog.d" filename = "TCHAP_CHANGES.md" name = "Changes in Tchap" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 3ee91a5fbd..04593929e2 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -33,11 +33,11 @@ knit { // Note: 2 digits max for each value ext.versionMajor = 2 -ext.versionMinor = 11 +ext.versionMinor = 12 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 11 +ext.versionPatch = 0 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index 54c7de9b7a..f982223665 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -50,6 +50,8 @@ class DebugVectorFeatures( override fun tchapIsLabsVisible(domain: String) = vectorFeatures.tchapIsLabsVisible(domain) + override fun tchapIsSecureBackupRequired() = vectorFeatures.tchapIsSecureBackupRequired() + override fun onboardingVariant(): OnboardingVariant { return readPreferences().getEnum() ?: vectorFeatures.onboardingVariant() } diff --git a/vector-app/src/main/java/im/vector/app/VectorApplication.kt b/vector-app/src/main/java/im/vector/app/VectorApplication.kt index bd28938808..c0c5ddf33c 100644 --- a/vector-app/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector-app/src/main/java/im/vector/app/VectorApplication.kt @@ -54,6 +54,7 @@ import im.vector.app.core.resources.BuildMeta import im.vector.app.features.VectorFeatures import im.vector.app.features.analytics.DecryptionFailureTracker import im.vector.app.features.analytics.VectorAnalytics +import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.invite.InvitesAcceptor @@ -131,6 +132,13 @@ class VectorApplication : appContext = this flipperProxy.init(matrix) vectorAnalytics.init() + vectorAnalytics.updateSuperProperties( + SuperProperties( + appPlatform = SuperProperties.AppPlatform.EA, + cryptoSDK = SuperProperties.CryptoSDK.Rust, + cryptoSDKVersion = Matrix.getCryptoVersion(longFormat = false) + ) + ) invitesAcceptor.initialize() autoRageShaker.initialize() decryptionFailureTracker.start() diff --git a/vector-config/src/btchap/res/values/config-features.xml b/vector-config/src/btchap/res/values/config-features.xml index e30415e59d..683c904075 100755 --- a/vector-config/src/btchap/res/values/config-features.xml +++ b/vector-config/src/btchap/res/values/config-features.xml @@ -3,6 +3,7 @@ true true false + true diff --git a/vector-config/src/devTchap/res/values/config-features.xml b/vector-config/src/devTchap/res/values/config-features.xml index e30415e59d..683c904075 100755 --- a/vector-config/src/devTchap/res/values/config-features.xml +++ b/vector-config/src/devTchap/res/values/config-features.xml @@ -3,6 +3,7 @@ true true false + true diff --git a/vector-config/src/tchap/res/values/config-features.xml b/vector-config/src/tchap/res/values/config-features.xml index e30415e59d..683c904075 100755 --- a/vector-config/src/tchap/res/values/config-features.xml +++ b/vector-config/src/tchap/res/values/config-features.xml @@ -3,6 +3,7 @@ true true false + true diff --git a/vector/build.gradle b/vector/build.gradle index c25eb8db19..d02a70c7bb 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -188,7 +188,7 @@ dependencies { api 'com.facebook.stetho:stetho:1.6.0' // Analytics - api 'com.github.matrix-org:matrix-analytics-events:0.15.0' + api 'com.github.matrix-org:matrix-analytics-events:0.23.0' api libs.google.phonenumber diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 0d6521126c..96deb8d8f4 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -188,7 +188,13 @@ - + + + + + + + diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index 5523c84994..f3d775b39f 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -22,7 +22,6 @@ import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.session.ConfigureAndStartSessionUseCase -import im.vector.app.features.analytics.DecryptionFailureTracker import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.crypto.keysrequest.KeyRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler @@ -57,7 +56,6 @@ class ActiveSessionHolder @Inject constructor( private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, private val applicationCoroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, - private val decryptionFailureTracker: DecryptionFailureTracker, ) { private var activeSessionReference: AtomicReference = AtomicReference() diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index 70e9593172..2315005aec 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -32,6 +32,7 @@ interface VectorFeatures { fun tchapIsKeyBackupEnabled(): Boolean fun tchapIsThreadEnabled(): Boolean fun tchapIsLabsVisible(domain: String): Boolean + fun tchapIsSecureBackupRequired(): Boolean fun onboardingVariant(): OnboardingVariant fun isOnboardingAlreadyHaveAccountSplashEnabled(): Boolean fun isOnboardingSplashCarouselEnabled(): Boolean @@ -71,6 +72,7 @@ class DefaultVectorFeatures @Inject constructor( override fun tchapIsThreadEnabled() = booleanProvider.getBoolean(R.bool.tchap_is_thread_enabled) override fun tchapIsLabsVisible(domain: String) = booleanProvider.getBoolean(R.bool.settings_root_labs_visible) || domain == appNameProvider.getAppName() + override fun tchapIsSecureBackupRequired() = booleanProvider.getBoolean(R.bool.tchap_is_secure_backup_required) override fun onboardingVariant() = Config.ONBOARDING_VARIANT override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true override fun isOnboardingSplashCarouselEnabled() = false // TCHAP no carousel diff --git a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt index 871782e473..d233900d2c 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt @@ -18,6 +18,7 @@ package im.vector.app.features.analytics import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsScreen +import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.analytics.plan.UserProperties interface AnalyticsTracker { @@ -35,4 +36,10 @@ interface AnalyticsTracker { * Update user specific properties. */ fun updateUserProperties(userProperties: UserProperties) + + /** + * Update the super properties. + * Super properties are added to any tracked event automatically. + */ + fun updateSuperProperties(updatedProperties: SuperProperties) } diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index 8520a40ca2..a7df8c54a8 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -23,6 +23,7 @@ import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import im.vector.app.features.analytics.log.analyticsTag +import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.analytics.plan.UserProperties import im.vector.app.features.analytics.store.AnalyticsStore import kotlinx.coroutines.CoroutineScope @@ -63,6 +64,8 @@ class DefaultVectorAnalytics @Inject constructor( // Cache for the properties to send private var pendingUserProperties: UserProperties? = null + private var superProperties: SuperProperties? = null + override fun init() { observeUserConsent() observeAnalyticsId() @@ -168,20 +171,14 @@ class DefaultVectorAnalytics @Inject constructor( override fun capture(event: VectorAnalyticsEvent) { Timber.tag(analyticsTag.value).d("capture($event)") - posthog - ?.takeIf { userConsent == true } - ?.capture( - event.getName(), - analyticsId, - event.getProperties()?.toPostHogProperties() + posthog?.takeIf { userConsent == true }?.capture( + event.getName(), analyticsId, event.getProperties()?.toPostHogProperties().orEmpty().withSuperProperties() ) } override fun screen(screen: VectorAnalyticsScreen) { Timber.tag(analyticsTag.value).d("screen($screen)") - posthog - ?.takeIf { userConsent == true } - ?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties()) + posthog?.takeIf { userConsent == true }?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties().orEmpty().withSuperProperties()) } override fun updateUserProperties(userProperties: UserProperties) { @@ -195,9 +192,7 @@ class DefaultVectorAnalytics @Inject constructor( private fun doUpdateUserProperties(userProperties: UserProperties) { // we need a distinct id to set user properties val distinctId = analyticsId ?: return - posthog - ?.takeIf { userConsent == true } - ?.identify(distinctId, userProperties.getProperties()) + posthog?.takeIf { userConsent == true }?.identify(distinctId, userProperties.getProperties()) } private fun Map?.toPostHogProperties(): Map? { @@ -226,9 +221,32 @@ class DefaultVectorAnalytics @Inject constructor( return nonNulls } + /** + * Adds super properties to the actual property set. + * If a property of the same name is already on the reported event it will not be overwritten. + */ + private fun Map.withSuperProperties(): Map? { + val withSuperProperties = this.toMutableMap() + val superProperties = this@DefaultVectorAnalytics.superProperties?.getProperties() + superProperties?.forEach { + if (!withSuperProperties.containsKey(it.key)) { + withSuperProperties[it.key] = it.value + } + } + return withSuperProperties.takeIf { it.isEmpty().not() } + } + override fun trackError(throwable: Throwable) { sentryAnalytics .takeIf { userConsent == true } ?.trackError(throwable) } + + override fun updateSuperProperties(updatedProperties: SuperProperties) { + this.superProperties = SuperProperties( + cryptoSDK = updatedProperties.cryptoSDK ?: this.superProperties?.cryptoSDK, + appPlatform = updatedProperties.appPlatform ?: this.superProperties?.appPlatform, + cryptoSDKVersion = updatedProperties.cryptoSDKVersion ?: superProperties?.cryptoSDKVersion + ) + } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt index 4ab11a218c..206a93aa67 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -32,12 +32,16 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider +import im.vector.app.features.VectorFeatures +import im.vector.app.features.raw.wellknown.getElementWellknown +import im.vector.app.features.raw.wellknown.isSecureBackupRequired import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.securestorage.IntegrityResult import org.matrix.android.sdk.api.session.securestorage.KeyInfo @@ -88,7 +92,9 @@ data class SharedSecureStorageViewState( class SharedSecureStorageViewModel @AssistedInject constructor( @Assisted private val initialState: SharedSecureStorageViewState, private val stringProvider: StringProvider, + private val vectorFeatures: VectorFeatures, private val session: Session, + private val rawService: RawService, private val matrix: Matrix, ) : VectorViewModel(initialState) { @@ -102,16 +108,25 @@ class SharedSecureStorageViewModel @AssistedInject constructor( setState { copy(userId = session.myUserId) } - if (initialState.requestType is RequestType.ReadSecrets) { - val integrityResult = - session.sharedSecretStorageService().checkShouldBeAbleToAccessSecrets(initialState.requestType.secretsName, initialState.keyId) - if (integrityResult !is IntegrityResult.Success) { - _viewEvents.post( - SharedSecureStorageViewEvent.Error( - stringProvider.getString(R.string.enter_secret_storage_invalid), - true - ) - ) + + // TCHAP force to configure secure backup even if well-known is null + // Do not check integrity if the session is not verified + viewModelScope.launch(Dispatchers.IO) { + val elementWellKnown = rawService.getElementWellknown(session.sessionParams) + val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() ?: vectorFeatures.tchapIsSecureBackupRequired() + val isThisSessionVerified = session.cryptoService().crossSigningService().isCrossSigningVerified() + + if ((isThisSessionVerified || !isSecureBackupRequired) && initialState.requestType is RequestType.ReadSecrets) { + val integrityResult = + session.sharedSecretStorageService().checkShouldBeAbleToAccessSecrets(initialState.requestType.secretsName, initialState.keyId) + if (integrityResult !is IntegrityResult.Success) { + _viewEvents.post( + SharedSecureStorageViewEvent.Error( + stringProvider.getString(R.string.enter_secret_storage_invalid), + true + ) + ) + } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt index 5ec1c9858c..08e929b192 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt @@ -44,18 +44,17 @@ class BootstrapSetupRecoveryKeyFragment : // TCHAP we directly send user to Security Key // Actions when a key backup exist - // views.bootstrapSetupSecureSubmit.views.bottomSheetActionClickableZone.debouncedClicks { - // sharedViewModel.handle(BootstrapActions.StartKeyBackupMigration) - // } - - // Actions when there is no key backup - // views.bootstrapSetupSecureUseSecurityKey.views.bottomSheetActionClickableZone.debouncedClicks { - // sharedViewModel.handle(BootstrapActions.Start(userWantsToEnterPassphrase = false)) - // } - // views.bootstrapSetupSecureUseSecurityPassphrase.views.bottomSheetActionClickableZone.debouncedClicks { - // sharedViewModel.handle(BootstrapActions.Start(userWantsToEnterPassphrase = true)) - // } - +// views.bootstrapSetupSecureSubmit.views.bottomSheetActionClickableZone.debouncedClicks { +// sharedViewModel.handle(BootstrapActions.StartKeyBackupMigration) +// } +// +// // Actions when there is no key backup +// views.bootstrapSetupSecureUseSecurityKey.views.bottomSheetActionClickableZone.debouncedClicks { +// sharedViewModel.handle(BootstrapActions.Start(userWantsToEnterPassphrase = false)) +// } +// views.bootstrapSetupSecureUseSecurityPassphrase.views.bottomSheetActionClickableZone.debouncedClicks { +// sharedViewModel.handle(BootstrapActions.Start(userWantsToEnterPassphrase = true)) +// } sharedViewModel.handle(BootstrapActions.Start(userWantsToEnterPassphrase = false)) } @@ -95,7 +94,7 @@ class BootstrapSetupRecoveryKeyFragment : private fun renderBackupMethodActions(method: SecureBackupMethod) = with(views) { bootstrapSetupSecureUseSecurityKey.isVisible = method.isKeyAvailable // TCHAP Hide Security Passphrase -// views.bootstrapSetupSecureUseSecurityPassphrase.isVisible = method.isPassphraseAvailable -// views.bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = method.isPassphraseAvailable +// bootstrapSetupSecureUseSecurityPassphrase.isVisible = method.isPassphraseAvailable +// bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = method.isPassphraseAvailable } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index c9c2c5ce9a..d13da10ee7 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt @@ -32,6 +32,7 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider +import im.vector.app.features.VectorFeatures import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.raw.wellknown.SecureBackupMethod import im.vector.app.features.raw.wellknown.getElementWellknown @@ -45,7 +46,6 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.raw.RawService @@ -63,6 +63,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( @Assisted initialState: BootstrapViewState, private val stringProvider: StringProvider, private val errorFormatter: ErrorFormatter, + private val vectorFeatures: VectorFeatures, private val session: Session, private val rawService: RawService, private val bootstrapTask: BootstrapCrossSigningTask, @@ -92,8 +93,9 @@ class BootstrapSharedViewModel @AssistedInject constructor( val wellKnown = rawService.getElementWellknown(session.sessionParams) setState { copy( - isSecureBackupRequired = wellKnown?.isSecureBackupRequired().orFalse(), - secureBackupMethod = wellKnown?.secureBackupMethod() ?: SecureBackupMethod.KEY_OR_PASSPHRASE, + // TCHAP force to configure secure backup key even if well-known is null + isSecureBackupRequired = wellKnown?.isSecureBackupRequired() ?: vectorFeatures.tchapIsSecureBackupRequired(), + secureBackupMethod = wellKnown?.secureBackupMethod() ?: SecureBackupMethod.KEY, ) } } @@ -212,7 +214,10 @@ class BootstrapSharedViewModel @AssistedInject constructor( } is BootstrapActions.DoInitialize -> { if (state.passphrase == state.passphraseRepeat) { - startInitializeFlow(state) + // TCHAP do not ask user password multiple times + if (state.step !is BootstrapStep.AccountReAuth) { + startInitializeFlow(state) + } } else { setState { copy( @@ -222,7 +227,10 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } is BootstrapActions.DoInitializeGeneratedKey -> { - startInitializeFlow(state) + // TCHAP do not ask user password multiple times + if (state.step !is BootstrapStep.AccountReAuth) { + startInitializeFlow(state) + } } BootstrapActions.RecoveryKeySaved -> { _viewEvents.post(BootstrapViewEvents.RecoveryKeySaved) @@ -603,4 +611,4 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } -private val BootstrapViewState.canLeave: Boolean get() = !isSecureBackupRequired || isRecoverySetup +private val BootstrapViewState.canLeave: Boolean get() = !isSecureBackupRequired || isRecoverySetup // TCHAP add a reminder instead ? diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt index 9871d21601..8548aa3d04 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt @@ -296,7 +296,8 @@ class SelfVerificationController @Inject constructor( id("passphrase") title(host.stringProvider.getString(R.string.verification_cannot_access_other_session)) titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) - subTitle(host.stringProvider.getString(R.string.verification_use_passphrase)) + // TCHAP use recovery key with no restriction +// subTitle(host.stringProvider.getString(R.string.verification_use_passphrase)) iconRes(R.drawable.ic_arrow_right) iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) listener { host.selfVerificationListener?.onClickRecoverFromPassphrase() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt index 029fa2ca15..99d3165fcb 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt @@ -37,8 +37,6 @@ import im.vector.app.features.crypto.verification.VerificationAction import im.vector.app.features.crypto.verification.VerificationBottomSheetViewEvents import im.vector.app.features.crypto.verification.user.VerificationTransactionData import im.vector.app.features.crypto.verification.user.toDataClass -import im.vector.app.features.raw.wellknown.getElementWellknown -import im.vector.app.features.raw.wellknown.isSecureBackupRequired import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.filter @@ -46,7 +44,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.Matrix -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session @@ -140,9 +137,9 @@ class SelfVerificationViewModel @AssistedInject constructor( // This is async, but at this point should be in cache // so it's ok to not wait until result viewModelScope.launch(Dispatchers.IO) { - val wellKnown = rawService.getElementWellknown(session.sessionParams) + // TCHAP force verification when recovery is setup setState { - copy(isVerificationRequired = wellKnown?.isSecureBackupRequired().orFalse()) + copy(isVerificationRequired = session.sharedSecretStorageService().isRecoverySetup()) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index bc2bbad6bc..dc88747c25 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -47,6 +47,7 @@ import im.vector.app.features.raw.wellknown.isSecureBackupRequired import im.vector.app.features.raw.wellknown.withElementWellKnown import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.voicebroadcast.recording.usecase.StopOngoingVoiceBroadcastUseCase import im.vector.lib.core.utils.compat.getParcelableExtraCompat import kotlinx.coroutines.Dispatchers @@ -97,6 +98,7 @@ class HomeActivityViewModel @AssistedInject constructor( private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, private val ensureSessionSyncingUseCase: EnsureSessionSyncingUseCase, + private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, private val coroutineDispatchers: CoroutineDispatchers, ) : VectorViewModel(initialState) { @@ -391,7 +393,8 @@ class HomeActivityViewModel @AssistedInject constructor( private fun sessionHasBeenUnverified(elementWellKnown: ElementWellKnown?) { val session = activeSessionHolder.getSafeActiveSession() ?: return - val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() ?: false + val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() + ?: vectorFeatures.tchapIsSecureBackupRequired() // TCHAP force to configure secure backup even if well-known is null if (isSecureBackupRequired) { // If 4S is forced, force verification // for stability cancel all pending verifications? @@ -425,7 +428,8 @@ class HomeActivityViewModel @AssistedInject constructor( } val elementWellKnown = rawService.getElementWellknown(session.sessionParams) - val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() ?: false + val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() + ?: vectorFeatures.tchapIsSecureBackupRequired() // TCHAP force to configure secure backup even if well-known is null // In case of account creation, it is already done before if (initialState.authenticationDescription is AuthenticationDescription.Register) { @@ -464,15 +468,24 @@ class HomeActivityViewModel @AssistedInject constructor( // Is there already cross signing keys here? val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys() if (mxCrossSigningInfo != null) { - if (isSecureBackupRequired && !session.sharedSecretStorageService().isRecoverySetup()) { + // TCHAP Setup 4S and cross-signing if needed + if (isSecureBackupRequired && !session.sharedSecretStorageService().isRecoverySetup() && mxCrossSigningInfo.isTrusted()) { // If 4S is forced, start the full interactive setup flow _viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow) } else { // Cross-signing is already set up for this user, is it trusted? if (!mxCrossSigningInfo.isTrusted()) { if (isSecureBackupRequired) { - // If 4S is forced, force verification - _viewEvents.post(HomeActivityViewEvents.ForceVerification(true)) + // TCHAP Setup 4S and cross-signing if needed + viewModelScope.launch { + val currentSessionCanBeVerified = checkIfCurrentSessionCanBeVerifiedUseCase.execute() + if (currentSessionCanBeVerified) { + // If 4S is forced, force verification + _viewEvents.post(HomeActivityViewEvents.ForceVerification(true)) + } else { + _viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow) + } + } } else { // we wan't to check if there is a way to actually verify this session, // that means that there is another session to verify against, or @@ -592,11 +605,11 @@ class HomeActivityViewModel @AssistedInject constructor( private suspend fun CrossSigningService.awaitCrossSigninInitialization( block: Continuation.(response: RegistrationFlowResponse, errCode: String?) -> Unit ) { - initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.block(flowResponse, errCode) - } + initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.block(flowResponse, errCode) } - ) + } + ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt index daf401efc3..c6b63722ef 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt @@ -246,10 +246,10 @@ class AutoCompleter @AssistedInject constructor( val linkText = when (matrixItem) { is MatrixItem.RoomAliasItem, is MatrixItem.RoomItem, - is MatrixItem.SpaceItem -> + is MatrixItem.SpaceItem, + is MatrixItem.UserItem -> matrixItem.id is MatrixItem.EveryoneInRoomItem, - is MatrixItem.UserItem, is MatrixItem.EventItem -> matrixItem.getBestName() } @@ -285,6 +285,7 @@ class AutoCompleter @AssistedInject constructor( // Add the span val span = PillImageSpan( + session, // TCHAP set pill background color when the user is mentioned glideRequests, avatarRenderer, editText.context, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index 68b8304fcb..f57201d062 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -432,7 +432,8 @@ class MessageComposerFragment : VectorBaseFragment(), A getRoom = timelineViewModel::getRoom, getMember = timelineViewModel::getMember, ) { matrixItem: MatrixItem -> - PillImageSpan(glideRequests, avatarRenderer, requireContext(), matrixItem) + // TCHAP set pill background color when the user is mentioned + PillImageSpan(session, glideRequests, avatarRenderer, requireContext(), matrixItem) } } @@ -808,18 +809,19 @@ class MessageComposerFragment : VectorBaseFragment(), A composer.editText.setSelection(Command.EMOTE.command.length + 1) } else { val roomMember = timelineViewModel.getMember(userId) - val displayName = sanitizeDisplayName(roomMember?.displayName ?: userId) if ((composer as? RichTextComposerLayout)?.isTextFormattingEnabled == true) { // Rich text editor is enabled so we need to use its APIs permalinkService.createPermalink(userId)?.let { url -> - (composer as RichTextComposerLayout).insertMention(url, displayName) + (composer as RichTextComposerLayout).insertMention(url, userId) composer.editText.append(" ") } } else { + val displayName = sanitizeDisplayName(roomMember?.displayName ?: userId) val pill = buildSpannedString { append(displayName) setSpan( PillImageSpan( + session, // TCHAP set pill background color when the user is mentioned glideRequests, avatarRenderer, requireContext(), diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 2149857ec2..b9457b9dc1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -84,7 +84,7 @@ class MergedHeaderItemFactory @Inject constructor( buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) isStartOfSameTypeEventsSummary(event, nextEvent, addDaySeparator) -> buildSameTypeEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) - isStartOfRedactedEventsSummary(event, items, currentPosition, addDaySeparator) -> + isStartOfRedactedEventsSummary(event, items, currentPosition, partialState, addDaySeparator) -> buildRedactedEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) else -> null } @@ -122,19 +122,25 @@ class MergedHeaderItemFactory @Inject constructor( * @param event the main timeline event * @param items all known items, sorted from newer event to oldest event * @param currentPosition the current position + * @param partialState partial state data * @param addDaySeparator true to add a day separator */ private fun isStartOfRedactedEventsSummary( event: TimelineEvent, items: List, currentPosition: Int, + partialState: TimelineEventController.PartialState, addDaySeparator: Boolean, ): Boolean { - val nextNonRedactionEvent = items - .subList(fromIndex = currentPosition + 1, toIndex = items.size) - .find { it.root.getClearType() != EventType.REDACTION } - return event.root.isRedacted() && - (!nextNonRedactionEvent?.root?.isRedacted().orFalse() || addDaySeparator) + val nextDisplayableEvent = items.subList(currentPosition + 1, items.size).firstOrNull { + timelineEventVisibilityHelper.shouldShowEvent( + timelineEvent = it, + highlightedEventId = partialState.highlightedEventId, + isFromThreadTimeline = partialState.isFromThreadTimeline(), + rootThreadEventId = partialState.rootThreadEventId + ) + } + return event.root.isRedacted() && (nextDisplayableEvent?.root?.isRedacted() == false || addDaySeparator) } private fun buildSameTypeEventsMergedSummary( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 703a5cb911..09c22aa771 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -151,16 +151,20 @@ class TimelineEventVisibilityHelper @Inject constructor( rootThreadEventId: String?, isFromThreadTimeline: Boolean ): List { - val prevSub = timelineEvents - .subList(0, index + 1) - // Ensure to not take the REDACTION events into account - .filter { it.root.getClearType() != EventType.REDACTION } - return prevSub + val prevDisplayableEvents = timelineEvents.subList(0, index + 1) + .filter { + shouldShowEvent( + timelineEvent = it, + highlightedEventId = eventIdToHighlight, + isFromThreadTimeline = isFromThreadTimeline, + rootThreadEventId = rootThreadEventId) + } + return prevDisplayableEvents .reversed() .let { nextEventsUntil(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline, object : PredicateToStopSearch { override fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean { - return oldEvent.isRedacted() && !newEvent.isRedacted() + return !newEvent.isRedacted() } }) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt index 79e8690c91..e3769281ca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt @@ -116,7 +116,8 @@ class EventTextRenderer @AssistedInject constructor( } private fun createPillImageSpan(matrixItem: MatrixItem) = - PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem) + // TCHAP set pill background color when the user is mentioned + PillImageSpan(sessionHolder.getActiveSession(), GlideApp.with(context), avatarRenderer, context, matrixItem) private fun addPillSpan( renderedText: Spannable, diff --git a/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt index a1130bd251..95592e564f 100644 --- a/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt @@ -39,6 +39,7 @@ import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.themes.ThemeUtils import org.matrix.android.sdk.api.extensions.orTrue +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan import org.matrix.android.sdk.api.util.MatrixItem import java.lang.ref.WeakReference @@ -49,6 +50,7 @@ import java.lang.ref.WeakReference * Implements MatrixItemSpan so that it could be automatically transformed in matrix links and displayed as pills. */ class PillImageSpan( + private val session: Session, private val glideRequests: GlideRequests, private val avatarRenderer: AvatarRenderer, private val context: Context, @@ -156,10 +158,17 @@ class PillImageSpan( setChipMinHeightResource(R.dimen.pill_min_height) setChipIconSizeResource(R.dimen.pill_avatar_size) chipIcon = icon - if (matrixItem is MatrixItem.EveryoneInRoomItem) { - chipBackgroundColor = ColorStateList.valueOf(ThemeUtils.getColor(context, R.attr.colorError)) - // setTextColor API does not exist right now for ChipDrawable, use textAppearance - setTextAppearanceResource(R.style.TextAppearance_Vector_Body_OnError) + // TCHAP set pill background color when the user is mentioned + when { + matrixItem is MatrixItem.UserItem && matrixItem.id == session.myUserId -> { + chipBackgroundColor = ColorStateList.valueOf(ThemeUtils.getColor(context, R.attr.colorSecondary)) + setTextColor(ThemeUtils.getColor(context, R.attr.colorOnSecondary)) + } + matrixItem is MatrixItem.EveryoneInRoomItem -> { + chipBackgroundColor = ColorStateList.valueOf(ThemeUtils.getColor(context, R.attr.colorError)) + // setTextColor API does not exist right now for ChipDrawable, use textAppearance + setTextAppearanceResource(R.style.TextAppearance_Vector_Body_OnError) + } } setBounds(0, 0, intrinsicWidth, intrinsicHeight) } diff --git a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt index 92d5b16998..5b8e4c85a8 100644 --- a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt @@ -104,7 +104,8 @@ class PillsPostProcessor @AssistedInject constructor( } private fun createPillImageSpan(matrixItem: MatrixItem) = - PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem) + // TCHAP set pill background color when the user is mentioned + PillImageSpan(sessionHolder.getActiveSession(), GlideApp.with(context), avatarRenderer, context, matrixItem) private fun LinkSpan.createPillSpan(): PillImageSpan? { val supportedHosts = context.resources.getStringArray(R.array.permalink_supported_hosts) diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt index 6d3b970d73..3885d3eadb 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt @@ -280,7 +280,7 @@ class BugReporter @Inject constructor( activeSessionHolder.getSafeActiveSession()?.let { session -> userId = session.myUserId deviceId = session.sessionParams.deviceId - olmVersion = session.cryptoService().getCryptoVersion(context, true) + olmVersion = Matrix.getCryptoVersion(true) bugReportURL = session.sessionParams.homeServerUrl.removeSuffix("/") + BUG_REPORT_URL_SUFFIX email = session.profileService().getThreePids().filterIsInstance().firstOrNull()?.email ?: "undefined" // TCHAP Add Email } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt index 7777602166..359cc041f8 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt @@ -96,7 +96,7 @@ class VectorSettingsHelpAboutFragment : // olm version findPreference(VectorPreferences.SETTINGS_CRYPTO_VERSION_PREFERENCE_KEY)!! - .summary = session.cryptoService().getCryptoVersion(requireContext(), true) + .summary = Matrix.getCryptoVersion(true) } companion object { diff --git a/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt index ea8beaa86c..72ff56bc27 100644 --- a/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt +++ b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt @@ -16,6 +16,7 @@ package im.vector.app.features.analytics.impl +import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.test.fakes.FakeAnalyticsStore import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory import im.vector.app.test.fakes.FakePostHog @@ -51,7 +52,7 @@ class DefaultVectorAnalyticsTest { analyticsStore = fakeAnalyticsStore.instance, globalScope = CoroutineScope(Dispatchers.Unconfined), analyticsConfig = anAnalyticsConfig(isEnabled = true), - lateInitUserPropertiesFactory = fakeLateInitUserPropertiesFactory.instance + lateInitUserPropertiesFactory = fakeLateInitUserPropertiesFactory.instance, ) @Before @@ -174,6 +175,117 @@ class DefaultVectorAnalyticsTest { fakeSentryAnalytics.verifyNoErrorTracking() } + @Test + fun `Super properties should be added to all captured events`() = runTest { + fakeAnalyticsStore.givenUserContent(consent = true) + + val updatedProperties = SuperProperties( + appPlatform = SuperProperties.AppPlatform.EA, + cryptoSDKVersion = "0.0", + cryptoSDK = SuperProperties.CryptoSDK.Rust + ) + + defaultVectorAnalytics.updateSuperProperties(updatedProperties) + + val fakeEvent = aVectorAnalyticsEvent("THE_NAME", mutableMapOf("foo" to "bar")) + defaultVectorAnalytics.capture(fakeEvent) + + fakePostHog.verifyEventTracked( + "THE_NAME", + fakeEvent.getProperties().clearNulls()?.toMutableMap()?.apply { + updatedProperties.getProperties()?.let { putAll(it) } + } + ) + + // Check with a screen event + val fakeScreen = aVectorAnalyticsScreen("Screen", mutableMapOf("foo" to "bar")) + defaultVectorAnalytics.screen(fakeScreen) + + fakePostHog.verifyScreenTracked( + "Screen", + fakeScreen.getProperties().clearNulls()?.toMutableMap()?.apply { + updatedProperties.getProperties()?.let { putAll(it) } + } + ) + } + + @Test + fun `Super properties can be updated`() = runTest { + fakeAnalyticsStore.givenUserContent(consent = true) + + val superProperties = SuperProperties( + appPlatform = SuperProperties.AppPlatform.EA, + cryptoSDKVersion = "0.0", + cryptoSDK = SuperProperties.CryptoSDK.Rust + ) + + defaultVectorAnalytics.updateSuperProperties(superProperties) + + val fakeEvent = aVectorAnalyticsEvent("THE_NAME", mutableMapOf("foo" to "bar")) + defaultVectorAnalytics.capture(fakeEvent) + + fakePostHog.verifyEventTracked( + "THE_NAME", + fakeEvent.getProperties().clearNulls()?.toMutableMap()?.apply { + superProperties.getProperties()?.let { putAll(it) } + } + ) + + val superPropertiesUpdate = superProperties.copy(cryptoSDKVersion = "1.0") + defaultVectorAnalytics.updateSuperProperties(superPropertiesUpdate) + + defaultVectorAnalytics.capture(fakeEvent) + + fakePostHog.verifyEventTracked( + "THE_NAME", + fakeEvent.getProperties().clearNulls()?.toMutableMap()?.apply { + superPropertiesUpdate.getProperties()?.let { putAll(it) } + } + ) + } + + @Test + fun `Super properties should not override event property`() = runTest { + fakeAnalyticsStore.givenUserContent(consent = true) + + val superProperties = SuperProperties( + cryptoSDKVersion = "0.0", + ) + + defaultVectorAnalytics.updateSuperProperties(superProperties) + + val fakeEvent = aVectorAnalyticsEvent("THE_NAME", mutableMapOf("cryptoSDKVersion" to "XXX")) + defaultVectorAnalytics.capture(fakeEvent) + + fakePostHog.verifyEventTracked( + "THE_NAME", + mapOf( + "cryptoSDKVersion" to "XXX" + ) + ) + } + + @Test + fun `Super properties should be added to event with no properties`() = runTest { + fakeAnalyticsStore.givenUserContent(consent = true) + + val superProperties = SuperProperties( + cryptoSDKVersion = "0.0", + ) + + defaultVectorAnalytics.updateSuperProperties(superProperties) + + val fakeEvent = aVectorAnalyticsEvent("THE_NAME", null) + defaultVectorAnalytics.capture(fakeEvent) + + fakePostHog.verifyEventTracked( + "THE_NAME", + mapOf( + "cryptoSDKVersion" to "0.0" + ) + ) + } + private fun Map?.clearNulls(): Map? { if (this == null) return null