Merge branch 'release/1.6.18' into main

This commit is contained in:
Benoit Marty 2024-06-25 15:13:30 +02:00
commit 026318304f
30 changed files with 328 additions and 141 deletions

View file

@ -32,7 +32,7 @@ jobs:
- uses: actions/setup-java@v3 - uses: actions/setup-java@v3
with: with:
distribution: 'adopt' distribution: 'adopt'
java-version: '11' java-version: '17'
- uses: gradle/gradle-build-action@v2 - uses: gradle/gradle-build-action@v2
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}

View file

@ -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) Changes in Element v1.6.16 (2024-05-29)
======================================= =======================================

View file

@ -101,7 +101,7 @@ ext.libs = [
], ],
element : [ element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0", '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 : [ squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi", 'moshi' : "com.squareup.moshi:moshi:$moshi",

View file

@ -0,0 +1,2 @@
Main changes in this version: Bugfixes.
Full changelog: https://github.com/element-hq/element-android/releases

View file

@ -62,7 +62,7 @@ android {
// that the app's state is completely cleared between tests. // that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true' 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", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""

View file

@ -26,10 +26,7 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest 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.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.MentionLinkSpecComparator
import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
@ -56,12 +53,6 @@ class MarkdownParserTest : InstrumentedTest {
HtmlRenderer.builder().softbreak("<br />").build(), HtmlRenderer.builder().softbreak("<br />").build(),
TextPillsUtils( TextPillsUtils(
MentionLinkSpecComparator(), MentionLinkSpecComparator(),
DisplayNameResolver(
MatrixConfiguration(
applicationFlavor = "TestFlavor",
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider()
)
),
TestPermalinkService() TestPermalinkService()
) )
) )

View file

@ -147,5 +147,12 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) {
fun getSdkVersion(): String { fun getSdkVersion(): String {
return BuildConfig.SDK_VERSION + " (" + BuildConfig.GIT_SDK_REVISION + ")" 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
}
} }
} }

View file

@ -16,7 +16,6 @@
package org.matrix.android.sdk.api.session.crypto package org.matrix.android.sdk.api.session.crypto
import android.content.Context
import androidx.annotation.Size import androidx.annotation.Size
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList import androidx.paging.PagedList
@ -61,8 +60,6 @@ interface CryptoService {
suspend fun deleteDevices(@Size(min = 1) deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) suspend fun deleteDevices(@Size(min = 1) deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor)
fun getCryptoVersion(context: Context, longFormat: Boolean): String
fun isCryptoEnabled(): Boolean fun isCryptoEnabled(): Boolean
fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean

View file

@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.paging.PagedList import androidx.paging.PagedList
@ -184,13 +183,6 @@ internal class RustCryptoService @Inject constructor(
deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor) 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) { override suspend fun getMyCryptoDevice(): CryptoDeviceInfo = withContext(coroutineDispatchers.io) {
olmMachine.ownDevice() olmMachine.ownDevice()
} }

View file

@ -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.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo 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.DeviceKeys
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeysWithUnsigned
import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo
internal object CryptoInfoMapper { 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 { fun map(cryptoDeviceInfo: CryptoDeviceInfo): DeviceKeys {
return DeviceKeys( return DeviceKeys(
deviceId = cryptoDeviceInfo.deviceId, deviceId = cryptoDeviceInfo.deviceId,

View file

@ -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<String>?,
/**
* Required. Public identity keys. The names of the properties should be in the format <algorithm>:<device_id>.
* The keys themselves should be encoded as specified by the key algorithm.
*/
@Json(name = "keys")
val keys: Map<String, String>?,
/**
* Required. Signatures for the device key object. A map from user ID, to a map from <algorithm>:<device_id> 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<String, Map<String, String>>?,
/**
* Additional data added to the device key information by intermediate servers, and not covered by the signatures.
*/
@Json(name = "unsigned")
val unsigned: UnsignedDeviceInfo? = null
)

View file

@ -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. * 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") @Json(name = "device_keys")
val deviceKeys: Map<String, Map<String, DeviceKeysWithUnsigned>>? = null, val deviceKeys: Map<String, Map<String, Map<String, Any>>>? = null,
/** /**
* If any remote homeservers could not be reached, they are recorded here. The names of the * If any remote homeservers could not be reached, they are recorded here. The names of the

View file

@ -22,7 +22,6 @@ import kotlinx.coroutines.joinAll
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.matrix.android.sdk.internal.crypto.api.CryptoApi 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.KeysQueryBody
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo
@ -52,7 +51,7 @@ internal class DefaultDownloadKeysForUsers @Inject constructor(
return if (bestChunkSize.shouldChunk()) { return if (bestChunkSize.shouldChunk()) {
// Store server results in these mutable maps // Store server results in these mutable maps
val deviceKeys = mutableMapOf<String, Map<String, DeviceKeysWithUnsigned>>() val deviceKeys = mutableMapOf<String, Map<String, Map<String, Any>>>()
val failures = mutableMapOf<String, Map<String, Any>>() val failures = mutableMapOf<String, Map<String, Any>>()
val masterKeys = mutableMapOf<String, RestKeyInfo?>() val masterKeys = mutableMapOf<String, RestKeyInfo?>()
val selfSigningKeys = mutableMapOf<String, RestKeyInfo?>() val selfSigningKeys = mutableMapOf<String, RestKeyInfo?>()

View file

@ -19,7 +19,6 @@ import android.text.SpannableString
import org.matrix.android.sdk.api.session.permalinks.PermalinkService 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.session.room.send.MatrixItemSpan
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
import java.util.Collections import java.util.Collections
import javax.inject.Inject import javax.inject.Inject
@ -29,7 +28,6 @@ import javax.inject.Inject
*/ */
internal class TextPillsUtils @Inject constructor( internal class TextPillsUtils @Inject constructor(
private val mentionLinkSpecComparator: MentionLinkSpecComparator, private val mentionLinkSpecComparator: MentionLinkSpecComparator,
private val displayNameResolver: DisplayNameResolver,
private val permalinkService: PermalinkService private val permalinkService: PermalinkService
) { ) {
@ -70,7 +68,7 @@ internal class TextPillsUtils @Inject constructor(
// append text before pill // append text before pill
append(text, currIndex, start) append(text, currIndex, start)
// append the pill // 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 currIndex = end
} }
// append text after the last pill // append text after the last pill

View file

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

View file

@ -66,7 +66,7 @@ if [ ${envError} == 1 ]; then
exit 1 exit 1
fi fi
buildToolsVersion="30.0.2" buildToolsVersion="35.0.0"
buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}" buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}"
if [[ ! -d ${buildToolsPath} ]]; then if [[ ! -d ${buildToolsPath} ]]; then

View file

@ -37,7 +37,7 @@ ext.versionMinor = 6
// Note: even values are reserved for regular release, odd values for hotfix release. // 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 // When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release. // is the value for the next regular release.
ext.versionPatch = 16 ext.versionPatch = 18
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'

View file

@ -53,6 +53,7 @@ import im.vector.app.core.pushers.FcmHelper
import im.vector.app.core.resources.BuildMeta import im.vector.app.core.resources.BuildMeta
import im.vector.app.features.analytics.DecryptionFailureTracker import im.vector.app.features.analytics.DecryptionFailureTracker
import im.vector.app.features.analytics.VectorAnalytics 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.call.webrtc.WebRtcCallManager
import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.invite.InvitesAcceptor import im.vector.app.features.invite.InvitesAcceptor
@ -130,6 +131,13 @@ class VectorApplication :
appContext = this appContext = this
flipperProxy.init(matrix) flipperProxy.init(matrix)
vectorAnalytics.init() vectorAnalytics.init()
vectorAnalytics.updateSuperProperties(
SuperProperties(
appPlatform = SuperProperties.AppPlatform.EA,
cryptoSDK = SuperProperties.CryptoSDK.Rust,
cryptoSDKVersion = Matrix.getCryptoVersion(longFormat = false)
)
)
invitesAcceptor.initialize() invitesAcceptor.initialize()
autoRageShaker.initialize() autoRageShaker.initialize()
decryptionFailureTracker.start() decryptionFailureTracker.start()

View file

@ -160,7 +160,7 @@ dependencies {
api 'com.facebook.stetho:stetho:1.6.0' api 'com.facebook.stetho:stetho:1.6.0'
// Analytics // 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 api libs.google.phonenumber

View file

@ -184,7 +184,13 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <data android:scheme="https" />
<data android:host="*.element.io" /> <!-- Note: we can't use "*.element.io" here because it'll intercept the "mas.element.io" domain too. -->
<!-- Matching asset file: https://app.element.io/.well-known/assetlinks.json -->
<data android:host="app.element.io" />
<!-- Matching asset file: https://develop.element.io/.well-known/assetlinks.json -->
<data android:host="develop.element.io" />
<!-- Matching asset file: https://staging.element.io/.well-known/assetlinks.json -->
<data android:host="staging.element.io" />
</intent-filter> </intent-filter>
</activity> </activity>

View file

@ -22,7 +22,6 @@ import im.vector.app.core.dispatchers.CoroutineDispatchers
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.core.session.ConfigureAndStartSessionUseCase 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.call.webrtc.WebRtcCallManager
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
@ -57,7 +56,6 @@ class ActiveSessionHolder @Inject constructor(
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
private val applicationCoroutineScope: CoroutineScope, private val applicationCoroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
private val decryptionFailureTracker: DecryptionFailureTracker,
) { ) {
private var activeSessionReference: AtomicReference<Session?> = AtomicReference() private var activeSessionReference: AtomicReference<Session?> = AtomicReference()

View file

@ -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.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
import im.vector.app.features.analytics.plan.SuperProperties
import im.vector.app.features.analytics.plan.UserProperties import im.vector.app.features.analytics.plan.UserProperties
interface AnalyticsTracker { interface AnalyticsTracker {
@ -35,4 +36,10 @@ interface AnalyticsTracker {
* Update user specific properties. * Update user specific properties.
*/ */
fun updateUserProperties(userProperties: UserProperties) fun updateUserProperties(userProperties: UserProperties)
/**
* Update the super properties.
* Super properties are added to any tracked event automatically.
*/
fun updateSuperProperties(updatedProperties: SuperProperties)
} }

View file

@ -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.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
import im.vector.app.features.analytics.log.analyticsTag 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.plan.UserProperties
import im.vector.app.features.analytics.store.AnalyticsStore import im.vector.app.features.analytics.store.AnalyticsStore
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -63,6 +64,8 @@ class DefaultVectorAnalytics @Inject constructor(
// Cache for the properties to send // Cache for the properties to send
private var pendingUserProperties: UserProperties? = null private var pendingUserProperties: UserProperties? = null
private var superProperties: SuperProperties? = null
override fun init() { override fun init() {
observeUserConsent() observeUserConsent()
observeAnalyticsId() observeAnalyticsId()
@ -168,20 +171,14 @@ class DefaultVectorAnalytics @Inject constructor(
override fun capture(event: VectorAnalyticsEvent) { override fun capture(event: VectorAnalyticsEvent) {
Timber.tag(analyticsTag.value).d("capture($event)") Timber.tag(analyticsTag.value).d("capture($event)")
posthog posthog?.takeIf { userConsent == true }?.capture(
?.takeIf { userConsent == true } event.getName(), analyticsId, event.getProperties()?.toPostHogProperties().orEmpty().withSuperProperties()
?.capture(
event.getName(),
analyticsId,
event.getProperties()?.toPostHogProperties()
) )
} }
override fun screen(screen: VectorAnalyticsScreen) { override fun screen(screen: VectorAnalyticsScreen) {
Timber.tag(analyticsTag.value).d("screen($screen)") Timber.tag(analyticsTag.value).d("screen($screen)")
posthog posthog?.takeIf { userConsent == true }?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties().orEmpty().withSuperProperties())
?.takeIf { userConsent == true }
?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties())
} }
override fun updateUserProperties(userProperties: UserProperties) { override fun updateUserProperties(userProperties: UserProperties) {
@ -195,9 +192,7 @@ class DefaultVectorAnalytics @Inject constructor(
private fun doUpdateUserProperties(userProperties: UserProperties) { private fun doUpdateUserProperties(userProperties: UserProperties) {
// we need a distinct id to set user properties // we need a distinct id to set user properties
val distinctId = analyticsId ?: return val distinctId = analyticsId ?: return
posthog posthog?.takeIf { userConsent == true }?.identify(distinctId, userProperties.getProperties())
?.takeIf { userConsent == true }
?.identify(distinctId, userProperties.getProperties())
} }
private fun Map<String, Any?>?.toPostHogProperties(): Map<String, Any>? { private fun Map<String, Any?>?.toPostHogProperties(): Map<String, Any>? {
@ -226,9 +221,32 @@ class DefaultVectorAnalytics @Inject constructor(
return nonNulls 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<String, Any>.withSuperProperties(): Map<String, Any>? {
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) { override fun trackError(throwable: Throwable) {
sentryAnalytics sentryAnalytics
.takeIf { userConsent == true } .takeIf { userConsent == true }
?.trackError(throwable) ?.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
)
}
} }

View file

@ -246,10 +246,10 @@ class AutoCompleter @AssistedInject constructor(
val linkText = when (matrixItem) { val linkText = when (matrixItem) {
is MatrixItem.RoomAliasItem, is MatrixItem.RoomAliasItem,
is MatrixItem.RoomItem, is MatrixItem.RoomItem,
is MatrixItem.SpaceItem -> is MatrixItem.SpaceItem,
is MatrixItem.UserItem ->
matrixItem.id matrixItem.id
is MatrixItem.EveryoneInRoomItem, is MatrixItem.EveryoneInRoomItem,
is MatrixItem.UserItem,
is MatrixItem.EventItem -> is MatrixItem.EventItem ->
matrixItem.getBestName() matrixItem.getBestName()
} }

View file

@ -796,14 +796,14 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
composer.editText.setSelection(Command.EMOTE.command.length + 1) composer.editText.setSelection(Command.EMOTE.command.length + 1)
} else { } else {
val roomMember = timelineViewModel.getMember(userId) val roomMember = timelineViewModel.getMember(userId)
val displayName = sanitizeDisplayName(roomMember?.displayName ?: userId)
if ((composer as? RichTextComposerLayout)?.isTextFormattingEnabled == true) { if ((composer as? RichTextComposerLayout)?.isTextFormattingEnabled == true) {
// Rich text editor is enabled so we need to use its APIs // Rich text editor is enabled so we need to use its APIs
permalinkService.createPermalink(userId)?.let { url -> permalinkService.createPermalink(userId)?.let { url ->
(composer as RichTextComposerLayout).insertMention(url, displayName) (composer as RichTextComposerLayout).insertMention(url, userId)
composer.editText.append(" ") composer.editText.append(" ")
} }
} else { } else {
val displayName = sanitizeDisplayName(roomMember?.displayName ?: userId)
val pill = buildSpannedString { val pill = buildSpannedString {
append(displayName) append(displayName)
setSpan( setSpan(

View file

@ -84,7 +84,7 @@ class MergedHeaderItemFactory @Inject constructor(
buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
isStartOfSameTypeEventsSummary(event, nextEvent, addDaySeparator) -> isStartOfSameTypeEventsSummary(event, nextEvent, addDaySeparator) ->
buildSameTypeEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) 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) buildRedactedEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
else -> null else -> null
} }
@ -122,19 +122,25 @@ class MergedHeaderItemFactory @Inject constructor(
* @param event the main timeline event * @param event the main timeline event
* @param items all known items, sorted from newer event to oldest event * @param items all known items, sorted from newer event to oldest event
* @param currentPosition the current position * @param currentPosition the current position
* @param partialState partial state data
* @param addDaySeparator true to add a day separator * @param addDaySeparator true to add a day separator
*/ */
private fun isStartOfRedactedEventsSummary( private fun isStartOfRedactedEventsSummary(
event: TimelineEvent, event: TimelineEvent,
items: List<TimelineEvent>, items: List<TimelineEvent>,
currentPosition: Int, currentPosition: Int,
partialState: TimelineEventController.PartialState,
addDaySeparator: Boolean, addDaySeparator: Boolean,
): Boolean { ): Boolean {
val nextNonRedactionEvent = items val nextDisplayableEvent = items.subList(currentPosition + 1, items.size).firstOrNull {
.subList(fromIndex = currentPosition + 1, toIndex = items.size) timelineEventVisibilityHelper.shouldShowEvent(
.find { it.root.getClearType() != EventType.REDACTION } timelineEvent = it,
return event.root.isRedacted() && highlightedEventId = partialState.highlightedEventId,
(!nextNonRedactionEvent?.root?.isRedacted().orFalse() || addDaySeparator) isFromThreadTimeline = partialState.isFromThreadTimeline(),
rootThreadEventId = partialState.rootThreadEventId
)
}
return event.root.isRedacted() && (nextDisplayableEvent?.root?.isRedacted() == false || addDaySeparator)
} }
private fun buildSameTypeEventsMergedSummary( private fun buildSameTypeEventsMergedSummary(

View file

@ -151,16 +151,20 @@ class TimelineEventVisibilityHelper @Inject constructor(
rootThreadEventId: String?, rootThreadEventId: String?,
isFromThreadTimeline: Boolean isFromThreadTimeline: Boolean
): List<TimelineEvent> { ): List<TimelineEvent> {
val prevSub = timelineEvents val prevDisplayableEvents = timelineEvents.subList(0, index + 1)
.subList(0, index + 1) .filter {
// Ensure to not take the REDACTION events into account shouldShowEvent(
.filter { it.root.getClearType() != EventType.REDACTION } timelineEvent = it,
return prevSub highlightedEventId = eventIdToHighlight,
isFromThreadTimeline = isFromThreadTimeline,
rootThreadEventId = rootThreadEventId)
}
return prevDisplayableEvents
.reversed() .reversed()
.let { .let {
nextEventsUntil(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline, object : PredicateToStopSearch { nextEventsUntil(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline, object : PredicateToStopSearch {
override fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean { override fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean {
return oldEvent.isRedacted() && !newEvent.isRedacted() return !newEvent.isRedacted()
} }
}) })
} }

View file

@ -265,7 +265,7 @@ class BugReporter @Inject constructor(
activeSessionHolder.getSafeActiveSession()?.let { session -> activeSessionHolder.getSafeActiveSession()?.let { session ->
userId = session.myUserId userId = session.myUserId
deviceId = session.sessionParams.deviceId deviceId = session.sessionParams.deviceId
olmVersion = session.cryptoService().getCryptoVersion(context, true) olmVersion = Matrix.getCryptoVersion(true)
} }
if (!mIsCancelled) { if (!mIsCancelled) {

View file

@ -96,7 +96,7 @@ class VectorSettingsHelpAboutFragment :
// olm version // olm version
findPreference<VectorPreference>(VectorPreferences.SETTINGS_CRYPTO_VERSION_PREFERENCE_KEY)!! findPreference<VectorPreference>(VectorPreferences.SETTINGS_CRYPTO_VERSION_PREFERENCE_KEY)!!
.summary = session.cryptoService().getCryptoVersion(requireContext(), true) .summary = Matrix.getCryptoVersion(true)
} }
companion object { companion object {

View file

@ -16,6 +16,7 @@
package im.vector.app.features.analytics.impl 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.FakeAnalyticsStore
import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory
import im.vector.app.test.fakes.FakePostHog import im.vector.app.test.fakes.FakePostHog
@ -51,7 +52,7 @@ class DefaultVectorAnalyticsTest {
analyticsStore = fakeAnalyticsStore.instance, analyticsStore = fakeAnalyticsStore.instance,
globalScope = CoroutineScope(Dispatchers.Unconfined), globalScope = CoroutineScope(Dispatchers.Unconfined),
analyticsConfig = anAnalyticsConfig(isEnabled = true), analyticsConfig = anAnalyticsConfig(isEnabled = true),
lateInitUserPropertiesFactory = fakeLateInitUserPropertiesFactory.instance lateInitUserPropertiesFactory = fakeLateInitUserPropertiesFactory.instance,
) )
@Before @Before
@ -174,6 +175,117 @@ class DefaultVectorAnalyticsTest {
fakeSentryAnalytics.verifyNoErrorTracking() 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<String, Any?>?.clearNulls(): Map<String, Any>? { private fun Map<String, Any?>?.clearNulls(): Map<String, Any>? {
if (this == null) return null if (this == null) return null