Merge branch 'release/1.6.14' into main
This commit is contained in:
commit
310cecf5cb
|
@ -3,6 +3,7 @@
|
||||||
/local.properties
|
/local.properties
|
||||||
# idea files: exclude everything except dictionnaries
|
# idea files: exclude everything except dictionnaries
|
||||||
.idea/caches
|
.idea/caches
|
||||||
|
.idea/copilot
|
||||||
.idea/libraries
|
.idea/libraries
|
||||||
.idea/inspectionProfiles
|
.idea/inspectionProfiles
|
||||||
.idea/sonarlint
|
.idea/sonarlint
|
||||||
|
|
18
CHANGES.md
18
CHANGES.md
|
@ -1,3 +1,17 @@
|
||||||
|
Changes in Element v1.6.14 (2024-04-02)
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Fix send button blinking once for each character you are typing in RTE. ([#send_button_blinking](https://github.com/element-hq/element-android/issues/send_button_blinking))
|
||||||
|
- Fix infinite loading on secure backup setup ("Re-Authentication needed" bottom sheet). ([#8786](https://github.com/element-hq/element-android/issues/8786))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Improve UTD reporting by adding additional fields to the report. ([#8780](https://github.com/element-hq/element-android/issues/8780))
|
||||||
|
- Add a report user action in the message bottom sheet and on the user profile page. ([#8796](https://github.com/element-hq/element-android/issues/8796))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element v1.6.12 (2024-02-16)
|
Changes in Element v1.6.12 (2024-02-16)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
|
@ -5,8 +19,8 @@ This update provides important security fixes, please update now.
|
||||||
|
|
||||||
Security fixes 🔐
|
Security fixes 🔐
|
||||||
-----------------
|
-----------------
|
||||||
- Add a check on incoming intent. ([#1506 internal](https://github.com/matrix-org/internal-config/issues/1506))
|
- Add a check on incoming intent. [CVE-2024-26131](https://www.cve.org/CVERecord?id=CVE-2024-26131) / [GHSA-j6pr-fpc8-q9vm](https://github.com/element-hq/element-android/security/advisories/GHSA-j6pr-fpc8-q9vm)
|
||||||
- Store temporary files created for Camera in the media folder. ([#1505 internal](https://github.com/matrix-org/internal-config/issues/1505))
|
- Store temporary files created for Camera in a dedicated media folder. [CVE-2024-26132](https://www.cve.org/CVERecord?id=CVE-2024-26132) / [GHSA-8wj9-cx7h-pvm4](https://github.com/element-hq/element-android/security/advisories/GHSA-8wj9-cx7h-pvm4)
|
||||||
|
|
||||||
Bugfixes 🐛
|
Bugfixes 🐛
|
||||||
----------
|
----------
|
||||||
|
|
|
@ -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.29.0"
|
'wysiwyg' : "io.element.android:wysiwyg:2.35.0"
|
||||||
],
|
],
|
||||||
squareup : [
|
squareup : [
|
||||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: Bugfixes and improvements.
|
||||||
|
Full changelog: https://github.com/element-hq/element-android/releases
|
|
@ -1953,8 +1953,11 @@
|
||||||
<string name="content_reported_as_spam_content">"This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can ignore them to hide their messages."</string>
|
<string name="content_reported_as_spam_content">"This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can ignore them to hide their messages."</string>
|
||||||
<string name="content_reported_as_inappropriate_title">"Reported as inappropriate"</string>
|
<string name="content_reported_as_inappropriate_title">"Reported as inappropriate"</string>
|
||||||
<string name="content_reported_as_inappropriate_content">"This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can ignore them to hide their messages."</string>
|
<string name="content_reported_as_inappropriate_content">"This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can ignore them to hide their messages."</string>
|
||||||
|
<string name="user_reported_as_inappropriate_title">"Reported user"</string>
|
||||||
|
<string name="user_reported_as_inappropriate_content">"The user has been reported.\n\nIf you don't want to see any more content from this user, you can ignore them to hide their messages."</string>
|
||||||
|
|
||||||
<string name="message_ignore_user">Ignore user</string>
|
<string name="message_ignore_user">Ignore user</string>
|
||||||
|
<string name="message_report_user">Report user</string>
|
||||||
|
|
||||||
<string name="room_list_quick_actions_notifications_all_noisy">"All messages (noisy)"</string>
|
<string name="room_list_quick_actions_notifications_all_noisy">"All messages (noisy)"</string>
|
||||||
<string name="room_list_quick_actions_notifications_all">"All messages"</string>
|
<string name="room_list_quick_actions_notifications_all">"All messages"</string>
|
||||||
|
|
|
@ -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.12\""
|
buildConfigField "String", "SDK_VERSION", "\"1.6.14\""
|
||||||
|
|
||||||
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()}\""
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.session
|
package org.matrix.android.sdk.api.session
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ interface LiveEventListener {
|
||||||
|
|
||||||
fun onEventDecrypted(event: Event, clearEvent: JsonDict)
|
fun onEventDecrypted(event: Event, clearEvent: JsonDict)
|
||||||
|
|
||||||
fun onEventDecryptionError(event: Event, throwable: Throwable)
|
fun onEventDecryptionError(event: Event, cryptoError: MXCryptoError)
|
||||||
|
|
||||||
fun onLiveToDeviceEvent(event: Event)
|
fun onLiveToDeviceEvent(event: Event)
|
||||||
|
|
||||||
|
|
|
@ -22,10 +22,12 @@ import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DecryptRoomEventUseCase @Inject constructor(private val olmMachine: OlmMachine) {
|
internal class DecryptRoomEventUseCase @Inject constructor(
|
||||||
|
private val cryptoService: RustCryptoService
|
||||||
|
) {
|
||||||
|
|
||||||
suspend operator fun invoke(event: Event): MXEventDecryptionResult {
|
suspend operator fun invoke(event: Event): MXEventDecryptionResult {
|
||||||
return olmMachine.decryptRoomEvent(event)
|
return cryptoService.decryptEvent(event, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun decryptAndSaveResult(event: Event) {
|
suspend fun decryptAndSaveResult(event: Event) {
|
||||||
|
|
|
@ -172,8 +172,6 @@ internal class Device @AssistedInject constructor(
|
||||||
* This will not fetch out fresh data from the Rust side.
|
* This will not fetch out fresh data from the Rust side.
|
||||||
**/
|
**/
|
||||||
internal fun toCryptoDeviceInfo(): CryptoDeviceInfo {
|
internal fun toCryptoDeviceInfo(): CryptoDeviceInfo {
|
||||||
// val keys = innerDevice.keys.map { (keyId, key) -> keyId to key }.toMap()
|
|
||||||
|
|
||||||
return CryptoDeviceInfo(
|
return CryptoDeviceInfo(
|
||||||
deviceId = innerDevice.deviceId,
|
deviceId = innerDevice.deviceId,
|
||||||
userId = innerDevice.userId,
|
userId = innerDevice.userId,
|
||||||
|
|
|
@ -189,18 +189,21 @@ internal class OlmMachine @Inject constructor(
|
||||||
is OwnUserIdentity -> ownIdentity.trustsOurOwnDevice()
|
is OwnUserIdentity -> ownIdentity.trustsOurOwnDevice()
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
val ownDevice = inner.getDevice(userId(), deviceId, 0u)!!
|
||||||
|
val creationTime = ownDevice.firstTimeSeenTs.toLong()
|
||||||
|
|
||||||
return CryptoDeviceInfo(
|
return CryptoDeviceInfo(
|
||||||
deviceId(),
|
deviceId(),
|
||||||
userId(),
|
userId(),
|
||||||
// TODO pass the algorithms here.
|
ownDevice.algorithms,
|
||||||
listOf(),
|
|
||||||
keys,
|
keys,
|
||||||
mapOf(),
|
mapOf(),
|
||||||
UnsignedDeviceInfo(),
|
UnsignedDeviceInfo(
|
||||||
|
deviceDisplayName = ownDevice.displayName
|
||||||
|
),
|
||||||
DeviceTrustLevel(crossSigningVerified, locallyVerified = true),
|
DeviceTrustLevel(crossSigningVerified, locallyVerified = true),
|
||||||
false,
|
false,
|
||||||
null
|
creationTime
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -882,6 +885,7 @@ internal class OlmMachine @Inject constructor(
|
||||||
inner.queryMissingSecretsFromOtherSessions()
|
inner.queryMissingSecretsFromOtherSessions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(CryptoStoreException::class)
|
@Throws(CryptoStoreException::class)
|
||||||
suspend fun enableBackupV1(key: String, version: String) {
|
suspend fun enableBackupV1(key: String, version: String) {
|
||||||
return withContext(coroutineDispatchers.computation) {
|
return withContext(coroutineDispatchers.computation) {
|
||||||
|
|
|
@ -497,8 +497,11 @@ internal class RustCryptoService @Inject constructor(
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
return try {
|
return try {
|
||||||
olmMachine.decryptRoomEvent(event)
|
olmMachine.decryptRoomEvent(event).also {
|
||||||
|
liveEventManager.get().dispatchLiveEventDecrypted(event, it)
|
||||||
|
}
|
||||||
} catch (mxCryptoError: MXCryptoError) {
|
} catch (mxCryptoError: MXCryptoError) {
|
||||||
|
liveEventManager.get().dispatchLiveEventDecryptionFailed(event, mxCryptoError)
|
||||||
if (mxCryptoError is MXCryptoError.Base && (
|
if (mxCryptoError is MXCryptoError.Base && (
|
||||||
mxCryptoError.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID ||
|
mxCryptoError.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID ||
|
||||||
mxCryptoError.errorType == MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX)) {
|
mxCryptoError.errorType == MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX)) {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.LiveEventListener
|
import org.matrix.android.sdk.api.session.LiveEventListener
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -75,7 +76,7 @@ internal class StreamEventsManager @Inject constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dispatchLiveEventDecryptionFailed(event: Event, error: Throwable) {
|
fun dispatchLiveEventDecryptionFailed(event: Event, error: MXCryptoError) {
|
||||||
Timber.v("## dispatchLiveEventDecryptionFailed ${event.eventId}")
|
Timber.v("## dispatchLiveEventDecryptionFailed ${event.eventId}")
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
listeners.forEach {
|
listeners.forEach {
|
||||||
|
|
|
@ -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 = 12
|
ext.versionPatch = 14
|
||||||
|
|
||||||
static def getGitTimestamp() {
|
static def getGitTimestamp() {
|
||||||
def cmd = 'git show -s --format=%ct'
|
def cmd = 'git show -s --format=%ct'
|
||||||
|
|
|
@ -51,6 +51,7 @@ import im.vector.app.core.debug.LeakDetector
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.pushers.FcmHelper
|
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.VectorAnalytics
|
import im.vector.app.features.analytics.VectorAnalytics
|
||||||
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
|
||||||
|
@ -100,6 +101,7 @@ class VectorApplication :
|
||||||
@Inject lateinit var callManager: WebRtcCallManager
|
@Inject lateinit var callManager: WebRtcCallManager
|
||||||
@Inject lateinit var invitesAcceptor: InvitesAcceptor
|
@Inject lateinit var invitesAcceptor: InvitesAcceptor
|
||||||
@Inject lateinit var autoRageShaker: AutoRageShaker
|
@Inject lateinit var autoRageShaker: AutoRageShaker
|
||||||
|
@Inject lateinit var decryptionFailureTracker: DecryptionFailureTracker
|
||||||
@Inject lateinit var vectorFileLogger: VectorFileLogger
|
@Inject lateinit var vectorFileLogger: VectorFileLogger
|
||||||
@Inject lateinit var vectorAnalytics: VectorAnalytics
|
@Inject lateinit var vectorAnalytics: VectorAnalytics
|
||||||
@Inject lateinit var flipperProxy: FlipperProxy
|
@Inject lateinit var flipperProxy: FlipperProxy
|
||||||
|
@ -130,6 +132,7 @@ class VectorApplication :
|
||||||
vectorAnalytics.init()
|
vectorAnalytics.init()
|
||||||
invitesAcceptor.initialize()
|
invitesAcceptor.initialize()
|
||||||
autoRageShaker.initialize()
|
autoRageShaker.initialize()
|
||||||
|
decryptionFailureTracker.start()
|
||||||
vectorUncaughtExceptionHandler.activate()
|
vectorUncaughtExceptionHandler.activate()
|
||||||
|
|
||||||
// Remove Log handler statically added by Jitsi
|
// Remove Log handler statically added by Jitsi
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.LiveEventListener
|
import org.matrix.android.sdk.api.session.LiveEventListener
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
|
@ -84,7 +85,7 @@ class UISIDetector(private val timeoutMillis: Long = 30_000L) : LiveEventListene
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEventDecryptionError(event: Event, throwable: Throwable) {
|
override fun onEventDecryptionError(event: Event, cryptoError: MXCryptoError) {
|
||||||
val eventId = event.eventId
|
val eventId = event.eventId
|
||||||
val roomId = event.roomId
|
val roomId = event.roomId
|
||||||
if (!enabled || eventId == null || roomId == null) return
|
if (!enabled || eventId == null || roomId == null) return
|
||||||
|
|
|
@ -23,7 +23,6 @@ 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.analytics.DecryptionFailureTracker
|
||||||
import im.vector.app.features.analytics.plan.Error
|
|
||||||
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
|
||||||
|
@ -75,11 +74,6 @@ class ActiveSessionHolder @Inject constructor(
|
||||||
session.callSignalingService().addCallListener(callManager)
|
session.callSignalingService().addCallListener(callManager)
|
||||||
imageManager.onSessionStarted(session)
|
imageManager.onSessionStarted(session)
|
||||||
guardServiceStarter.start()
|
guardServiceStarter.start()
|
||||||
decryptionFailureTracker.currentModule = if (session.cryptoService().name() == "rust-sdk") {
|
|
||||||
Error.CryptoModule.Rust
|
|
||||||
} else {
|
|
||||||
Error.CryptoModule.Native
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun clearActiveSession() {
|
suspend fun clearActiveSession() {
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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 im.vector.app.features.analytics
|
||||||
|
|
||||||
|
import im.vector.app.features.analytics.plan.Error
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
|
||||||
|
data class DecryptionFailure(
|
||||||
|
val timeStamp: Long,
|
||||||
|
val roomId: String,
|
||||||
|
val failedEventId: String,
|
||||||
|
val error: MXCryptoError,
|
||||||
|
val wasVisibleOnScreen: Boolean,
|
||||||
|
val ownIdentityTrustedAtTimeOfDecryptionFailure: Boolean,
|
||||||
|
// If this is set, it means that the event was decrypted but late. Will be -1 if
|
||||||
|
// the event was not decrypted after the maximum wait time.
|
||||||
|
val timeToDecryptMillis: Long? = null,
|
||||||
|
val isMatrixDotOrg: Boolean,
|
||||||
|
val isFederated: Boolean? = null,
|
||||||
|
val eventLocalAgeAtDecryptionFailure: Long? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
fun DecryptionFailure.toAnalyticsEvent(): Error {
|
||||||
|
val errorMsg = (error as? MXCryptoError.Base)?.technicalMessage ?: error.message
|
||||||
|
return Error(
|
||||||
|
context = "mxc_crypto_error_type|${errorMsg}",
|
||||||
|
domain = Error.Domain.E2EE,
|
||||||
|
name = this.toAnalyticsErrorName(),
|
||||||
|
// this is deprecated keep for backward compatibility
|
||||||
|
cryptoModule = Error.CryptoModule.Rust,
|
||||||
|
cryptoSDK = Error.CryptoSDK.Rust,
|
||||||
|
eventLocalAgeMillis = eventLocalAgeAtDecryptionFailure?.toInt(),
|
||||||
|
isFederated = isFederated,
|
||||||
|
isMatrixDotOrg = isMatrixDotOrg,
|
||||||
|
timeToDecryptMillis = timeToDecryptMillis?.toInt() ?: -1,
|
||||||
|
wasVisibleToUser = wasVisibleOnScreen,
|
||||||
|
userTrustsOwnIdentity = ownIdentityTrustedAtTimeOfDecryptionFailure,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DecryptionFailure.toAnalyticsErrorName(): Error.Name {
|
||||||
|
val error = this.error
|
||||||
|
val name = if (error is MXCryptoError.Base) {
|
||||||
|
when (error.errorType) {
|
||||||
|
MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID,
|
||||||
|
MXCryptoError.ErrorType.KEYS_WITHHELD -> Error.Name.OlmKeysNotSentError
|
||||||
|
MXCryptoError.ErrorType.OLM -> Error.Name.OlmUnspecifiedError
|
||||||
|
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX -> Error.Name.OlmIndexError
|
||||||
|
else -> Error.Name.UnknownError
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Error.Name.UnknownError
|
||||||
|
}
|
||||||
|
// check if it's an expected UTD!
|
||||||
|
val localAge = this.eventLocalAgeAtDecryptionFailure
|
||||||
|
val isHistorical = localAge != null && localAge < 0
|
||||||
|
if (isHistorical && !this.ownIdentityTrustedAtTimeOfDecryptionFailure) {
|
||||||
|
return Error.Name.HistoricalMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
|
@ -16,149 +16,282 @@
|
||||||
|
|
||||||
package im.vector.app.features.analytics
|
package im.vector.app.features.analytics
|
||||||
|
|
||||||
import im.vector.app.features.analytics.plan.Error
|
import im.vector.app.ActiveSessionDataSource
|
||||||
import im.vector.lib.core.utils.compat.removeIfCompat
|
|
||||||
import im.vector.lib.core.utils.flow.tickerFlow
|
|
||||||
import im.vector.lib.core.utils.timer.Clock
|
import im.vector.lib.core.utils.timer.Clock
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import org.matrix.android.sdk.api.MatrixPatterns
|
||||||
|
import org.matrix.android.sdk.api.session.LiveEventListener
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
private data class DecryptionFailure(
|
// If we can decrypt in less than 4s, we don't report
|
||||||
val timeStamp: Long,
|
|
||||||
val roomId: String,
|
|
||||||
val failedEventId: String,
|
|
||||||
val error: MXCryptoError.ErrorType
|
|
||||||
)
|
|
||||||
private typealias DetailedErrorName = Pair<String, Error.Name>
|
|
||||||
|
|
||||||
private const val GRACE_PERIOD_MILLIS = 4_000
|
private const val GRACE_PERIOD_MILLIS = 4_000
|
||||||
private const val CHECK_INTERVAL = 2_000L
|
|
||||||
|
// A tick to check when a decryption failure as exceeded the max time
|
||||||
|
private const val CHECK_INTERVAL = 10_000L
|
||||||
|
|
||||||
|
// If we can't decrypt after 60s, we report failures
|
||||||
|
private const val MAX_WAIT_MILLIS = 60_000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks decryption errors that are visible to the user.
|
* Tracks decryption errors.
|
||||||
* When an error is reported it is not directly tracked via analytics, there is a grace period
|
* When an error is reported it is not directly tracked via analytics, there is a grace period
|
||||||
* that gives the app a few seconds to get the key to decrypt.
|
* that gives the app a few seconds to get the key to decrypt.
|
||||||
|
*
|
||||||
|
* Decrypted under 4s => No report
|
||||||
|
* Decrypted before MAX_WAIT_MILLIS => Report with time to decrypt
|
||||||
|
* Not Decrypted after MAX_WAIT_MILLIS => Report with time = -1
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class DecryptionFailureTracker @Inject constructor(
|
class DecryptionFailureTracker @Inject constructor(
|
||||||
private val analyticsTracker: AnalyticsTracker,
|
private val analyticsTracker: AnalyticsTracker,
|
||||||
|
private val sessionDataSource: ActiveSessionDataSource,
|
||||||
private val clock: Clock
|
private val clock: Clock
|
||||||
) {
|
) : Session.Listener, LiveEventListener {
|
||||||
|
|
||||||
private val scope: CoroutineScope = CoroutineScope(SupervisorJob())
|
// The active session (set by the sessionDataSource)
|
||||||
private val failures = mutableListOf<DecryptionFailure>()
|
private var activeSession: Session? = null
|
||||||
|
|
||||||
|
// The coroutine scope to use for the tracker
|
||||||
|
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
|
// Map of eventId to tracked failure
|
||||||
|
// Only accessed on a `post` call, ensuring sequential access
|
||||||
|
private val trackedEventsMap = mutableMapOf<String, DecryptionFailure>()
|
||||||
|
|
||||||
|
// List of eventId that have been reported, to avoid double reporting
|
||||||
private val alreadyReported = mutableListOf<String>()
|
private val alreadyReported = mutableListOf<String>()
|
||||||
|
|
||||||
var currentModule: Error.CryptoModule? = null
|
// Mutex to ensure sequential access to internal state
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
init {
|
// Used to unsubscribe from the active session data source
|
||||||
start()
|
private lateinit var activeSessionSourceDisposable: Job
|
||||||
|
|
||||||
|
// The ticker job, to report permanent UTD (not decrypted after MAX_WAIT_MILLIS)
|
||||||
|
private var currentTicker: Job? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the tracker.
|
||||||
|
*
|
||||||
|
* @param scope The coroutine scope to use, exposed for tests. If null, it will use the default one
|
||||||
|
*/
|
||||||
|
fun start(scope: CoroutineScope? = null) {
|
||||||
|
if (scope != null) {
|
||||||
|
this.scope = scope
|
||||||
}
|
}
|
||||||
|
observeActiveSession()
|
||||||
fun start() {
|
|
||||||
tickerFlow(scope, CHECK_INTERVAL)
|
|
||||||
.onEach {
|
|
||||||
checkFailures()
|
|
||||||
}.launchIn(scope)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
scope.cancel()
|
Timber.v("Stop DecryptionFailureTracker")
|
||||||
|
activeSessionSourceDisposable.cancel(CancellationException("Closing DecryptionFailureTracker"))
|
||||||
|
|
||||||
|
activeSession?.removeListener(this)
|
||||||
|
activeSession?.eventStreamService()?.removeEventStreamListener(this)
|
||||||
|
activeSession = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun e2eEventDisplayedInTimeline(event: TimelineEvent) {
|
private fun post(block: suspend () -> Unit) {
|
||||||
scope.launch(Dispatchers.Default) {
|
scope.launch {
|
||||||
val mCryptoError = event.root.mCryptoError
|
mutex.withLock {
|
||||||
if (mCryptoError != null) {
|
block()
|
||||||
addDecryptionFailure(DecryptionFailure(clock.epochMillis(), event.roomId, event.eventId, mCryptoError))
|
|
||||||
} else {
|
|
||||||
removeFailureForEventId(event.eventId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private suspend fun rescheduleTicker() {
|
||||||
* Can be called when the timeline is disposed in order
|
currentTicker = scope.launch {
|
||||||
* to grace those events as they are not anymore displayed on screen.
|
Timber.v("Reschedule ticker")
|
||||||
* */
|
delay(CHECK_INTERVAL)
|
||||||
fun onTimeLineDisposed(roomId: String) {
|
post {
|
||||||
scope.launch(Dispatchers.Default) {
|
checkFailures()
|
||||||
synchronized(failures) {
|
currentTicker = null
|
||||||
failures.removeIfCompat { it.roomId == roomId }
|
if (trackedEventsMap.isNotEmpty()) {
|
||||||
|
// Reschedule
|
||||||
|
rescheduleTicker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun observeActiveSession() {
|
||||||
|
activeSessionSourceDisposable = sessionDataSource.stream()
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach {
|
||||||
|
Timber.v("Active session changed ${it.getOrNull()?.myUserId}")
|
||||||
|
it.orNull()?.let { session ->
|
||||||
|
post {
|
||||||
|
onSessionActive(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSessionActive(session: Session) {
|
||||||
|
Timber.v("onSessionActive ${session.myUserId} previous: ${activeSession?.myUserId}")
|
||||||
|
val sessionId = session.sessionId
|
||||||
|
if (sessionId == activeSession?.sessionId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.activeSession?.let { previousSession ->
|
||||||
|
previousSession.removeListener(this)
|
||||||
|
previousSession.eventStreamService().removeEventStreamListener(this)
|
||||||
|
// Do we want to clear the tracked events?
|
||||||
|
}
|
||||||
|
this.activeSession = session
|
||||||
|
session.addListener(this)
|
||||||
|
session.eventStreamService().addEventStreamListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionStopped(session: Session) {
|
||||||
|
post {
|
||||||
|
this.activeSession = null
|
||||||
|
session.addListener(this)
|
||||||
|
session.eventStreamService().addEventStreamListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LiveEventListener callbacks
|
||||||
|
|
||||||
|
override fun onEventDecrypted(event: Event, clearEvent: JsonDict) {
|
||||||
|
Timber.v("Event decrypted ${event.eventId}")
|
||||||
|
event.eventId?.let {
|
||||||
|
post {
|
||||||
|
handleEventDecrypted(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addDecryptionFailure(failure: DecryptionFailure) {
|
override fun onEventDecryptionError(event: Event, cryptoError: MXCryptoError) {
|
||||||
// de duplicate
|
Timber.v("Decryption error for event ${event.eventId} with error $cryptoError")
|
||||||
synchronized(failures) {
|
val session = activeSession ?: return
|
||||||
if (failures.none { it.failedEventId == failure.failedEventId }) {
|
// track the event
|
||||||
failures.add(failure)
|
post {
|
||||||
}
|
trackEvent(session, event, cryptoError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeFailureForEventId(eventId: String) {
|
override fun onLiveToDeviceEvent(event: Event) {}
|
||||||
synchronized(failures) {
|
override fun onLiveEvent(roomId: String, event: Event) {}
|
||||||
failures.removeIfCompat { it.failedEventId == eventId }
|
override fun onPaginatedEvent(roomId: String, event: Event) {}
|
||||||
|
|
||||||
|
private suspend fun trackEvent(session: Session, event: Event, error: MXCryptoError) {
|
||||||
|
Timber.v("Track event ${event.eventId}/${session.myUserId} time: ${clock.epochMillis()}")
|
||||||
|
val eventId = event.eventId
|
||||||
|
val roomId = event.roomId
|
||||||
|
if (eventId == null || roomId == null) return
|
||||||
|
if (trackedEventsMap.containsKey(eventId)) {
|
||||||
|
// already tracked
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
if (alreadyReported.contains(eventId)) {
|
||||||
|
// already reported
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val isOwnIdentityTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified()
|
||||||
|
val userHS = MatrixPatterns.extractServerNameFromId(session.myUserId)
|
||||||
|
val messageSenderHs = event.senderId?.let { MatrixPatterns.extractServerNameFromId(it) }
|
||||||
|
Timber.v("senderHs: $messageSenderHs, userHS: $userHS, isOwnIdentityTrusted: $isOwnIdentityTrusted")
|
||||||
|
|
||||||
|
val deviceCreationTs = session.cryptoService().getMyCryptoDevice().firstTimeSeenLocalTs
|
||||||
|
Timber.v("deviceCreationTs: $deviceCreationTs")
|
||||||
|
val eventRelativeAge = deviceCreationTs?.let { deviceTs ->
|
||||||
|
event.originServerTs?.let {
|
||||||
|
it - deviceTs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val failure = DecryptionFailure(
|
||||||
|
clock.epochMillis(),
|
||||||
|
roomId,
|
||||||
|
eventId,
|
||||||
|
error,
|
||||||
|
wasVisibleOnScreen = false,
|
||||||
|
ownIdentityTrustedAtTimeOfDecryptionFailure = isOwnIdentityTrusted,
|
||||||
|
isMatrixDotOrg = userHS == "matrix.org",
|
||||||
|
isFederated = messageSenderHs?.let { it != userHS },
|
||||||
|
eventLocalAgeAtDecryptionFailure = eventRelativeAge
|
||||||
|
)
|
||||||
|
Timber.v("Tracked failure: ${failure}")
|
||||||
|
trackedEventsMap[eventId] = failure
|
||||||
|
|
||||||
|
if (currentTicker == null) {
|
||||||
|
rescheduleTicker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEventDecrypted(eventId: String) {
|
||||||
|
Timber.v("Handle event decrypted $eventId time: ${clock.epochMillis()}")
|
||||||
|
// Only consider if it was tracked as a failure
|
||||||
|
val trackedFailure = trackedEventsMap[eventId] ?: return
|
||||||
|
|
||||||
|
// Grace event if decrypted under 4s
|
||||||
|
val now = clock.epochMillis()
|
||||||
|
val timeToDecrypt = now - trackedFailure.timeStamp
|
||||||
|
Timber.v("Handle event decrypted timeToDecrypt: $timeToDecrypt for event $eventId")
|
||||||
|
if (timeToDecrypt < GRACE_PERIOD_MILLIS) {
|
||||||
|
Timber.v("Grace event $eventId")
|
||||||
|
trackedEventsMap.remove(eventId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// We still want to report but with the time it took
|
||||||
|
if (trackedFailure.timeToDecryptMillis == null) {
|
||||||
|
val decryptionFailure = trackedFailure.copy(timeToDecryptMillis = timeToDecrypt)
|
||||||
|
trackedEventsMap[eventId] = decryptionFailure
|
||||||
|
reportFailure(decryptionFailure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun utdDisplayedInTimeline(event: TimelineEvent) {
|
||||||
|
post {
|
||||||
|
// should be tracked (unless already reported)
|
||||||
|
val eventId = event.root.eventId ?: return@post
|
||||||
|
val trackedEvent = trackedEventsMap[eventId] ?: return@post
|
||||||
|
|
||||||
|
trackedEventsMap[eventId] = trackedEvent.copy(wasVisibleOnScreen = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will mutate the trackedEventsMap, so don't call it while iterating on it.
|
||||||
|
private fun reportFailure(decryptionFailure: DecryptionFailure) {
|
||||||
|
Timber.v("Report failure for event ${decryptionFailure.failedEventId}")
|
||||||
|
val error = decryptionFailure.toAnalyticsEvent()
|
||||||
|
|
||||||
|
analyticsTracker.capture(error)
|
||||||
|
|
||||||
|
// now remove from tracked
|
||||||
|
trackedEventsMap.remove(decryptionFailure.failedEventId)
|
||||||
|
// mark as already reported
|
||||||
|
alreadyReported.add(decryptionFailure.failedEventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkFailures() {
|
private fun checkFailures() {
|
||||||
val now = clock.epochMillis()
|
val now = clock.epochMillis()
|
||||||
val aggregatedErrors: Map<DetailedErrorName, List<String>>
|
Timber.v("Check failures now $now")
|
||||||
synchronized(failures) {
|
// report the definitely failed
|
||||||
val toReport = mutableListOf<DecryptionFailure>()
|
val toReport = trackedEventsMap.values.filter {
|
||||||
failures.removeAll { failure ->
|
now - it.timeStamp > MAX_WAIT_MILLIS
|
||||||
(now - failure.timeStamp > GRACE_PERIOD_MILLIS).also {
|
|
||||||
if (it) {
|
|
||||||
toReport.add(failure)
|
|
||||||
}
|
}
|
||||||
|
toReport.forEach {
|
||||||
|
reportFailure(
|
||||||
|
it.copy(timeToDecryptMillis = -1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregatedErrors = toReport
|
|
||||||
.groupBy { it.error.toAnalyticsErrorName() }
|
|
||||||
.mapValues {
|
|
||||||
it.value.map { it.failedEventId }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
aggregatedErrors.forEach { aggregation ->
|
|
||||||
// there is now way to send the total/sum in posthog, so iterating
|
|
||||||
aggregation.value
|
|
||||||
// for now we ignore events already reported even if displayed again?
|
|
||||||
.filter { alreadyReported.contains(it).not() }
|
|
||||||
.forEach { failedEventId ->
|
|
||||||
analyticsTracker.capture(Error(
|
|
||||||
context = aggregation.key.first,
|
|
||||||
domain = Error.Domain.E2EE,
|
|
||||||
name = aggregation.key.second,
|
|
||||||
cryptoModule = currentModule
|
|
||||||
))
|
|
||||||
alreadyReported.add(failedEventId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MXCryptoError.ErrorType.toAnalyticsErrorName(): DetailedErrorName {
|
|
||||||
val detailed = "$name | mxc_crypto_error_type"
|
|
||||||
val errorName = when (this) {
|
|
||||||
MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID,
|
|
||||||
MXCryptoError.ErrorType.KEYS_WITHHELD -> Error.Name.OlmKeysNotSentError
|
|
||||||
MXCryptoError.ErrorType.OLM -> Error.Name.OlmUnspecifiedError
|
|
||||||
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX -> Error.Name.OlmIndexError
|
|
||||||
else -> Error.Name.UnknownError
|
|
||||||
}
|
|
||||||
return DetailedErrorName(detailed, errorName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,7 +175,10 @@ class DefaultVectorAnalytics @Inject constructor(
|
||||||
Timber.tag(analyticsTag.value).d("capture($event)")
|
Timber.tag(analyticsTag.value).d("capture($event)")
|
||||||
posthog
|
posthog
|
||||||
?.takeIf { userConsent == true }
|
?.takeIf { userConsent == true }
|
||||||
?.capture(event.getName(), event.getProperties()?.toPostHogProperties())
|
?.capture(
|
||||||
|
event.getName(),
|
||||||
|
event.getProperties()?.toPostHogProperties()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun screen(screen: VectorAnalyticsScreen) {
|
override fun screen(screen: VectorAnalyticsScreen) {
|
||||||
|
|
|
@ -30,11 +30,44 @@ data class Error(
|
||||||
*/
|
*/
|
||||||
val context: String? = null,
|
val context: String? = null,
|
||||||
/**
|
/**
|
||||||
* Which crypto module is the client currently using.
|
* DEPRECATED: Which crypto module is the client currently using.
|
||||||
*/
|
*/
|
||||||
val cryptoModule: CryptoModule? = null,
|
val cryptoModule: CryptoModule? = null,
|
||||||
|
/**
|
||||||
|
* Which crypto backend is the client currently using.
|
||||||
|
*/
|
||||||
|
val cryptoSDK: CryptoSDK? = null,
|
||||||
val domain: Domain,
|
val domain: Domain,
|
||||||
|
/**
|
||||||
|
* An heuristic based on event origin_server_ts and the current device
|
||||||
|
* creation time (origin_server_ts - device_ts). This would be used to
|
||||||
|
* get the source of the event scroll-back/live/initialSync.
|
||||||
|
*/
|
||||||
|
val eventLocalAgeMillis: Int? = null,
|
||||||
|
/**
|
||||||
|
* true if userDomain != senderDomain.
|
||||||
|
*/
|
||||||
|
val isFederated: Boolean? = null,
|
||||||
|
/**
|
||||||
|
* true if the current user is using matrix.org.
|
||||||
|
*/
|
||||||
|
val isMatrixDotOrg: Boolean? = null,
|
||||||
val name: Name,
|
val name: Name,
|
||||||
|
/**
|
||||||
|
* UTDs can be permanent or temporary. If temporary, this field will
|
||||||
|
* contain the time it took to decrypt the message in milliseconds. If
|
||||||
|
* permanent should be -1.
|
||||||
|
*/
|
||||||
|
val timeToDecryptMillis: Int? = null,
|
||||||
|
/**
|
||||||
|
* true if the current user trusts their own identity (verified session)
|
||||||
|
* at time of decryption.
|
||||||
|
*/
|
||||||
|
val userTrustsOwnIdentity: Boolean? = null,
|
||||||
|
/**
|
||||||
|
* true if that unable to decrypt error was visible to the user.
|
||||||
|
*/
|
||||||
|
val wasVisibleToUser: Boolean? = null,
|
||||||
) : VectorAnalyticsEvent {
|
) : VectorAnalyticsEvent {
|
||||||
|
|
||||||
enum class Domain {
|
enum class Domain {
|
||||||
|
@ -44,18 +77,79 @@ data class Error(
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Name {
|
enum class Name {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2EE domain error. Decryption failed for a message sent before the
|
||||||
|
* device logged in, and key backup is not enabled.
|
||||||
|
*/
|
||||||
|
HistoricalMessage,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2EE domain error. The room key is known but is ratcheted (index >
|
||||||
|
* 0).
|
||||||
|
*/
|
||||||
OlmIndexError,
|
OlmIndexError,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2EE domain error. Generic unknown inbound group session error.
|
||||||
|
*/
|
||||||
OlmKeysNotSentError,
|
OlmKeysNotSentError,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2EE domain error. Any other decryption error (missing field, format
|
||||||
|
* errors...).
|
||||||
|
*/
|
||||||
OlmUnspecifiedError,
|
OlmUnspecifiedError,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TO_DEVICE domain error. The to-device message failed to decrypt.
|
||||||
|
*/
|
||||||
ToDeviceFailedToDecrypt,
|
ToDeviceFailedToDecrypt,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2EE domain error. Decryption failed due to unknown error.
|
||||||
|
*/
|
||||||
UnknownError,
|
UnknownError,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VOIP domain error. ICE negotiation failed.
|
||||||
|
*/
|
||||||
VoipIceFailed,
|
VoipIceFailed,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VOIP domain error. ICE negotiation timed out.
|
||||||
|
*/
|
||||||
VoipIceTimeout,
|
VoipIceTimeout,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VOIP domain error. The call invite timed out.
|
||||||
|
*/
|
||||||
VoipInviteTimeout,
|
VoipInviteTimeout,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VOIP domain error. The user hung up the call.
|
||||||
|
*/
|
||||||
VoipUserHangup,
|
VoipUserHangup,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VOIP domain error. The user's media failed to start.
|
||||||
|
*/
|
||||||
VoipUserMediaFailed,
|
VoipUserMediaFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class CryptoSDK {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy crypto backend specific to each platform.
|
||||||
|
*/
|
||||||
|
Legacy,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-platform crypto backend written in Rust.
|
||||||
|
*/
|
||||||
|
Rust,
|
||||||
|
}
|
||||||
|
|
||||||
enum class CryptoModule {
|
enum class CryptoModule {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,8 +169,15 @@ data class Error(
|
||||||
return mutableMapOf<String, Any>().apply {
|
return mutableMapOf<String, Any>().apply {
|
||||||
context?.let { put("context", it) }
|
context?.let { put("context", it) }
|
||||||
cryptoModule?.let { put("cryptoModule", it.name) }
|
cryptoModule?.let { put("cryptoModule", it.name) }
|
||||||
|
cryptoSDK?.let { put("cryptoSDK", it.name) }
|
||||||
put("domain", domain.name)
|
put("domain", domain.name)
|
||||||
|
eventLocalAgeMillis?.let { put("eventLocalAgeMillis", it) }
|
||||||
|
isFederated?.let { put("isFederated", it) }
|
||||||
|
isMatrixDotOrg?.let { put("isMatrixDotOrg", it) }
|
||||||
put("name", name.name)
|
put("name", name.name)
|
||||||
|
timeToDecryptMillis?.let { put("timeToDecryptMillis", it) }
|
||||||
|
userTrustsOwnIdentity?.let { put("userTrustsOwnIdentity", it) }
|
||||||
|
wasVisibleToUser?.let { put("wasVisibleToUser", it) }
|
||||||
}.takeIf { it.isNotEmpty() }
|
}.takeIf { it.isNotEmpty() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,11 +85,28 @@ data class Interaction(
|
||||||
*/
|
*/
|
||||||
MobileRoomAddHome,
|
MobileRoomAddHome,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User switched the favourite toggle on Room Details screen.
|
||||||
|
*/
|
||||||
|
MobileRoomFavouriteToggle,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User tapped on Leave Room button on Room Details screen.
|
* User tapped on Leave Room button on Room Details screen.
|
||||||
*/
|
*/
|
||||||
MobileRoomLeave,
|
MobileRoomLeave,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User adjusted their favourite rooms using the context menu on a room
|
||||||
|
* in the room list.
|
||||||
|
*/
|
||||||
|
MobileRoomListRoomContextMenuFavouriteToggle,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User adjusted their unread rooms using the context menu on a room in
|
||||||
|
* the room list.
|
||||||
|
*/
|
||||||
|
MobileRoomListRoomContextMenuUnreadToggle,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User tapped on Threads button on Room screen.
|
* User tapped on Threads button on Room screen.
|
||||||
*/
|
*/
|
||||||
|
@ -306,6 +323,18 @@ data class Interaction(
|
||||||
*/
|
*/
|
||||||
WebRoomListRoomTileContextMenuLeaveItem,
|
WebRoomListRoomTileContextMenuLeaveItem,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User marked a message as read using the context menu on a room tile
|
||||||
|
* in the room list in Element Web/Desktop.
|
||||||
|
*/
|
||||||
|
WebRoomListRoomTileContextMenuMarkRead,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User marked a room as unread using the context menu on a room tile in
|
||||||
|
* the room list in Element Web/Desktop.
|
||||||
|
*/
|
||||||
|
WebRoomListRoomTileContextMenuMarkUnread,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User accessed room settings using the context menu on a room tile in
|
* User accessed room settings using the context menu on a room tile in
|
||||||
* the room list in Element Web/Desktop.
|
* the room list in Element Web/Desktop.
|
||||||
|
@ -408,6 +437,18 @@ data class Interaction(
|
||||||
*/
|
*/
|
||||||
WebThreadViewBackButton,
|
WebThreadViewBackButton,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User clicked on the Threads Activity Centre button of Element
|
||||||
|
* Web/Desktop.
|
||||||
|
*/
|
||||||
|
WebThreadsActivityCentreButton,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User clicked on a room in the Threads Activity Centre of Element
|
||||||
|
* Web/Desktop.
|
||||||
|
*/
|
||||||
|
WebThreadsActivityCentreRoomItem,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User selected a thread in the Threads panel in Element Web/Desktop.
|
* User selected a thread in the Threads panel in Element Web/Desktop.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -119,6 +119,12 @@ data class MobileScreen(
|
||||||
*/
|
*/
|
||||||
MyGroups,
|
MyGroups,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The screen containing tests to help user to fix issues around
|
||||||
|
* notifications.
|
||||||
|
*/
|
||||||
|
NotificationTroubleshoot,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The People tab on mobile that lists all the DM rooms you have joined.
|
* The People tab on mobile that lists all the DM rooms you have joined.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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 im.vector.app.features.analytics.plan
|
||||||
|
|
||||||
|
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||||
|
|
||||||
|
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
|
||||||
|
// https://github.com/matrix-org/matrix-analytics-events/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered when the user runs the troubleshoot notification test suite.
|
||||||
|
*/
|
||||||
|
data class NotificationTroubleshoot(
|
||||||
|
/**
|
||||||
|
* Whether one or more tests are in error.
|
||||||
|
*/
|
||||||
|
val hasError: Boolean,
|
||||||
|
) : VectorAnalyticsEvent {
|
||||||
|
|
||||||
|
override fun getName() = "NotificationTroubleshoot"
|
||||||
|
|
||||||
|
override fun getProperties(): Map<String, Any>? {
|
||||||
|
return mutableMapOf<String, Any>().apply {
|
||||||
|
put("hasError", hasError)
|
||||||
|
}.takeIf { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||||
data class PollEnd(
|
data class PollEnd(
|
||||||
/**
|
/**
|
||||||
* Do not use this. Remove this property when the kotlin type generator
|
* Do not use this. Remove this property when the kotlin type generator
|
||||||
* can properly generate types without proprties other than the event
|
* can properly generate types without properties other than the event
|
||||||
* name.
|
* name.
|
||||||
*/
|
*/
|
||||||
val doNotUse: Boolean? = null,
|
val doNotUse: Boolean? = null,
|
||||||
|
|
|
@ -27,7 +27,7 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||||
data class PollVote(
|
data class PollVote(
|
||||||
/**
|
/**
|
||||||
* Do not use this. Remove this property when the kotlin type generator
|
* Do not use this. Remove this property when the kotlin type generator
|
||||||
* can properly generate types without proprties other than the event
|
* can properly generate types without properties other than the event
|
||||||
* name.
|
* name.
|
||||||
*/
|
*/
|
||||||
val doNotUse: Boolean? = null,
|
val doNotUse: Boolean? = null,
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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 im.vector.app.features.analytics.plan
|
||||||
|
|
||||||
|
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||||
|
|
||||||
|
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
|
||||||
|
// https://github.com/matrix-org/matrix-analytics-events/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered when a moderation action is performed within a room.
|
||||||
|
*/
|
||||||
|
data class RoomModeration(
|
||||||
|
/**
|
||||||
|
* The action that was performed.
|
||||||
|
*/
|
||||||
|
val action: Action,
|
||||||
|
/**
|
||||||
|
* When the action sets a particular power level, this is the suggested
|
||||||
|
* role for that the power level.
|
||||||
|
*/
|
||||||
|
val role: Role? = null,
|
||||||
|
) : VectorAnalyticsEvent {
|
||||||
|
|
||||||
|
enum class Action {
|
||||||
|
/**
|
||||||
|
* Banned a room member.
|
||||||
|
*/
|
||||||
|
BanMember,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changed a room member's power level.
|
||||||
|
*/
|
||||||
|
ChangeMemberRole,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changed the power level required to ban room members.
|
||||||
|
*/
|
||||||
|
ChangePermissionsBanMembers,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changed the power level required to invite users to the room.
|
||||||
|
*/
|
||||||
|
ChangePermissionsInviteUsers,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changed the power level required to kick room members.
|
||||||
|
*/
|
||||||
|
ChangePermissionsKickMembers,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changed the power level required to redact messages in the room.
|
||||||
|
*/
|
||||||
|
ChangePermissionsRedactMessages,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changed the power level required to set the room's avatar.
|
||||||
|
*/
|
||||||
|
ChangePermissionsRoomAvatar,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changed the power level required to set the room's name.
|
||||||
|
*/
|
||||||
|
ChangePermissionsRoomName,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changed the power level required to set the room's topic.
|
||||||
|
*/
|
||||||
|
ChangePermissionsRoomTopic,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changed the power level required to send messages in the room.
|
||||||
|
*/
|
||||||
|
ChangePermissionsSendMessages,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kicked a room member.
|
||||||
|
*/
|
||||||
|
KickMember,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all of the room permissions back to their default values.
|
||||||
|
*/
|
||||||
|
ResetPermissions,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unbanned a room member.
|
||||||
|
*/
|
||||||
|
UnbanMember,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Role {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A power level of 100.
|
||||||
|
*/
|
||||||
|
Administrator,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A power level of 50.
|
||||||
|
*/
|
||||||
|
Moderator,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any other power level.
|
||||||
|
*/
|
||||||
|
Other,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A power level of 0.
|
||||||
|
*/
|
||||||
|
User,
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName() = "RoomModeration"
|
||||||
|
|
||||||
|
override fun getProperties(): Map<String, Any>? {
|
||||||
|
return mutableMapOf<String, Any>().apply {
|
||||||
|
put("action", action.name)
|
||||||
|
role?.let { put("role", it.name) }
|
||||||
|
}.takeIf { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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 im.vector.app.features.analytics.plan
|
||||||
|
|
||||||
|
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
|
||||||
|
// https://github.com/matrix-org/matrix-analytics-events/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Super Properties are properties associated with events that are sent with
|
||||||
|
* every capture call, be it a $pageview, an autocaptured button click, or
|
||||||
|
* anything else.
|
||||||
|
*/
|
||||||
|
data class SuperProperties(
|
||||||
|
/**
|
||||||
|
* Used by web to identify the platform (Web Platform/Electron Platform).
|
||||||
|
*/
|
||||||
|
val appPlatform: String? = null,
|
||||||
|
/**
|
||||||
|
* Which crypto backend is the client currently using.
|
||||||
|
*/
|
||||||
|
val cryptoSDK: CryptoSDK? = null,
|
||||||
|
/**
|
||||||
|
* Version of the crypto backend.
|
||||||
|
*/
|
||||||
|
val cryptoSDKVersion: String? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
enum class CryptoSDK {
|
||||||
|
/**
|
||||||
|
* Legacy crypto backend specific to each platform.
|
||||||
|
*/
|
||||||
|
Legacy,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-platform crypto backend written in Rust.
|
||||||
|
*/
|
||||||
|
Rust,
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProperties(): Map<String, Any>? {
|
||||||
|
return mutableMapOf<String, Any>().apply {
|
||||||
|
appPlatform?.let { put("appPlatform", it) }
|
||||||
|
cryptoSDK?.let { put("cryptoSDK", it.name) }
|
||||||
|
cryptoSDKVersion?.let { put("cryptoSDKVersion", it) }
|
||||||
|
}.takeIf { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -252,6 +252,12 @@ data class ViewRoom(
|
||||||
*/
|
*/
|
||||||
WebSpacePanelNotificationBadge,
|
WebSpacePanelNotificationBadge,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room accessed via interacting with the Threads Activity Centre in
|
||||||
|
* Element Web/Desktop.
|
||||||
|
*/
|
||||||
|
WebThreadsActivityCentre,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Room accessed via Element Web/Desktop's Unified Search modal.
|
* Room accessed via Element Web/Desktop's Unified Search modal.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.airbnb.mvrx.parentFragmentViewModel
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
@ -43,6 +44,12 @@ class BootstrapReAuthFragment :
|
||||||
|
|
||||||
views.bootstrapRetryButton.debouncedClicks { submit() }
|
views.bootstrapRetryButton.debouncedClicks { submit() }
|
||||||
views.bootstrapCancelButton.debouncedClicks { cancel() }
|
views.bootstrapCancelButton.debouncedClicks { cancel() }
|
||||||
|
|
||||||
|
val viewModel = ViewModelProvider(this).get(BootstrapReAuthViewModel::class.java)
|
||||||
|
if (!viewModel.isFirstSubmitDone) {
|
||||||
|
viewModel.isFirstSubmitDone = true
|
||||||
|
submit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun submit() = withState(sharedViewModel) { state ->
|
private fun submit() = withState(sharedViewModel) { state ->
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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 im.vector.app.features.crypto.recover
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
|
class BootstrapReAuthViewModel : ViewModel() {
|
||||||
|
var isFirstSubmitDone = false
|
||||||
|
}
|
|
@ -61,7 +61,8 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||||
val senderId: String?,
|
val senderId: String?,
|
||||||
val reason: String,
|
val reason: String,
|
||||||
val spam: Boolean = false,
|
val spam: Boolean = false,
|
||||||
val inappropriate: Boolean = false
|
val inappropriate: Boolean = false,
|
||||||
|
val user: Boolean = false,
|
||||||
) : RoomDetailAction()
|
) : RoomDetailAction()
|
||||||
|
|
||||||
data class IgnoreUser(val userId: String?) : RoomDetailAction()
|
data class IgnoreUser(val userId: String?) : RoomDetailAction()
|
||||||
|
|
|
@ -1345,6 +1345,16 @@ class TimelineFragment :
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
data.user -> {
|
||||||
|
MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
|
||||||
|
.setTitle(R.string.user_reported_as_inappropriate_title)
|
||||||
|
.setMessage(R.string.user_reported_as_inappropriate_content)
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.setNegativeButton(R.string.block_user) { _, _ ->
|
||||||
|
timelineViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
|
MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
|
||||||
.setTitle(R.string.content_reported_title)
|
.setTitle(R.string.content_reported_title)
|
||||||
|
@ -1857,6 +1867,13 @@ class TimelineFragment :
|
||||||
is EventSharedAction.IgnoreUser -> {
|
is EventSharedAction.IgnoreUser -> {
|
||||||
action.senderId?.let { askConfirmationToIgnoreUser(it) }
|
action.senderId?.let { askConfirmationToIgnoreUser(it) }
|
||||||
}
|
}
|
||||||
|
is EventSharedAction.ReportUser -> {
|
||||||
|
timelineViewModel.handle(
|
||||||
|
RoomDetailAction.ReportContent(
|
||||||
|
action.eventId, action.senderId, "Reporting user ${action.senderId}", user = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
is EventSharedAction.OnUrlClicked -> {
|
is EventSharedAction.OnUrlClicked -> {
|
||||||
onUrlClicked(action.url, action.title)
|
onUrlClicked(action.url, action.title)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1484,7 +1484,6 @@ class TimelineViewModel @AssistedInject constructor(
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
timeline?.dispose()
|
timeline?.dispose()
|
||||||
timeline?.removeAllListeners()
|
timeline?.removeAllListeners()
|
||||||
decryptionFailureTracker.onTimeLineDisposed(initialState.roomId)
|
|
||||||
if (vectorPreferences.sendTypingNotifs()) {
|
if (vectorPreferences.sendTypingNotifs()) {
|
||||||
room?.typingService()?.userStopsTyping()
|
room?.typingService()?.userStopsTyping()
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,9 @@ import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.setTextIfDifferent
|
import im.vector.app.core.extensions.setTextIfDifferent
|
||||||
import im.vector.app.core.extensions.showKeyboard
|
import im.vector.app.core.extensions.showKeyboard
|
||||||
|
import im.vector.app.core.utils.Debouncer
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
|
import im.vector.app.core.utils.createUIHandler
|
||||||
import im.vector.app.databinding.ComposerRichTextLayoutBinding
|
import im.vector.app.databinding.ComposerRichTextLayoutBinding
|
||||||
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
|
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
|
||||||
import im.vector.app.features.home.room.detail.composer.images.UriContentListener
|
import im.vector.app.features.home.room.detail.composer.images.UriContentListener
|
||||||
|
@ -195,10 +197,16 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
|
||||||
renderComposerMode(MessageComposerMode.Normal(null))
|
renderComposerMode(MessageComposerMode.Normal(null))
|
||||||
|
|
||||||
views.richTextComposerEditText.addTextChangedListener(
|
views.richTextComposerEditText.addTextChangedListener(
|
||||||
TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder(isFullScreen) })
|
TextChangeListener(
|
||||||
|
onTextChanged = {
|
||||||
|
callback?.onTextChanged(it)
|
||||||
|
},
|
||||||
|
onExpandedChanged = { updateTextFieldBorder(isFullScreen) })
|
||||||
)
|
)
|
||||||
views.plainTextComposerEditText.addTextChangedListener(
|
views.plainTextComposerEditText.addTextChangedListener(
|
||||||
TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder(isFullScreen) })
|
TextChangeListener({
|
||||||
|
callback?.onTextChanged(it)
|
||||||
|
}, { updateTextFieldBorder(isFullScreen) })
|
||||||
)
|
)
|
||||||
ViewCompat.setOnReceiveContentListener(
|
ViewCompat.setOnReceiveContentListener(
|
||||||
views.richTextComposerEditText,
|
views.richTextComposerEditText,
|
||||||
|
@ -516,13 +524,15 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
|
||||||
private val onTextChanged: (s: Editable) -> Unit,
|
private val onTextChanged: (s: Editable) -> Unit,
|
||||||
private val onExpandedChanged: (isExpanded: Boolean) -> Unit,
|
private val onExpandedChanged: (isExpanded: Boolean) -> Unit,
|
||||||
) : TextWatcher {
|
) : TextWatcher {
|
||||||
|
|
||||||
|
private val debouncer = Debouncer(createUIHandler())
|
||||||
private var previousTextWasExpanded = false
|
private var previousTextWasExpanded = false
|
||||||
|
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
override fun afterTextChanged(s: Editable) {
|
override fun afterTextChanged(s: Editable) {
|
||||||
|
debouncer.debounce("afterTextChanged", 50L) {
|
||||||
onTextChanged.invoke(s)
|
onTextChanged.invoke(s)
|
||||||
|
|
||||||
val isExpanded = s.lines().count() > 1
|
val isExpanded = s.lines().count() > 1
|
||||||
if (previousTextWasExpanded != isExpanded) {
|
if (previousTextWasExpanded != isExpanded) {
|
||||||
onExpandedChanged(isExpanded)
|
onExpandedChanged(isExpanded)
|
||||||
|
@ -530,4 +540,5 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
|
||||||
previousTextWasExpanded = isExpanded
|
previousTextWasExpanded = isExpanded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,9 @@ sealed class EventSharedAction(
|
||||||
data class IgnoreUser(val senderId: String?) :
|
data class IgnoreUser(val senderId: String?) :
|
||||||
EventSharedAction(R.string.message_ignore_user, R.drawable.ic_alert_triangle, true)
|
EventSharedAction(R.string.message_ignore_user, R.drawable.ic_alert_triangle, true)
|
||||||
|
|
||||||
|
data class ReportUser(val eventId: String, val senderId: String?) :
|
||||||
|
EventSharedAction(R.string.message_report_user, R.drawable.ic_flag, true)
|
||||||
|
|
||||||
data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) :
|
data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) :
|
||||||
EventSharedAction(0, 0)
|
EventSharedAction(0, 0)
|
||||||
|
|
||||||
|
|
|
@ -430,6 +430,12 @@ class MessageActionsViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
add(EventSharedAction.Separator)
|
add(EventSharedAction.Separator)
|
||||||
add(EventSharedAction.IgnoreUser(timelineEvent.root.senderId))
|
add(EventSharedAction.IgnoreUser(timelineEvent.root.senderId))
|
||||||
|
add(
|
||||||
|
EventSharedAction.ReportUser(
|
||||||
|
eventId = eventId,
|
||||||
|
senderId = timelineEvent.root.senderId,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -158,8 +158,8 @@ class TimelineItemFactory @Inject constructor(
|
||||||
defaultItemFactory.create(params)
|
defaultItemFactory.create(params)
|
||||||
}
|
}
|
||||||
}.also {
|
}.also {
|
||||||
if (it != null && event.isEncrypted()) {
|
if (it != null && event.isEncrypted() && event.root.mCryptoError != null) {
|
||||||
decryptionFailureTracker.e2eEventDisplayedInTimeline(event)
|
decryptionFailureTracker.utdDisplayedInTimeline(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||||
sealed class RoomMemberProfileAction : VectorViewModelAction {
|
sealed class RoomMemberProfileAction : VectorViewModelAction {
|
||||||
object RetryFetchingInfo : RoomMemberProfileAction()
|
object RetryFetchingInfo : RoomMemberProfileAction()
|
||||||
object IgnoreUser : RoomMemberProfileAction()
|
object IgnoreUser : RoomMemberProfileAction()
|
||||||
|
object ReportUser : RoomMemberProfileAction()
|
||||||
data class BanOrUnbanUser(val reason: String?) : RoomMemberProfileAction()
|
data class BanOrUnbanUser(val reason: String?) : RoomMemberProfileAction()
|
||||||
data class KickUser(val reason: String?) : RoomMemberProfileAction()
|
data class KickUser(val reason: String?) : RoomMemberProfileAction()
|
||||||
object InviteUser : RoomMemberProfileAction()
|
object InviteUser : RoomMemberProfileAction()
|
||||||
|
|
|
@ -39,6 +39,7 @@ class RoomMemberProfileController @Inject constructor(
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onIgnoreClicked()
|
fun onIgnoreClicked()
|
||||||
|
fun onReportClicked()
|
||||||
fun onTapVerify()
|
fun onTapVerify()
|
||||||
fun onShowDeviceList()
|
fun onShowDeviceList()
|
||||||
fun onShowDeviceListNoCrossSigning()
|
fun onShowDeviceListNoCrossSigning()
|
||||||
|
@ -225,7 +226,7 @@ class RoomMemberProfileController @Inject constructor(
|
||||||
title = stringProvider.getString(R.string.room_participants_action_invite),
|
title = stringProvider.getString(R.string.room_participants_action_invite),
|
||||||
destructive = false,
|
destructive = false,
|
||||||
editable = false,
|
editable = false,
|
||||||
divider = ignoreActionTitle != null,
|
divider = true,
|
||||||
action = { callback?.onInviteClicked() }
|
action = { callback?.onInviteClicked() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -235,10 +236,18 @@ class RoomMemberProfileController @Inject constructor(
|
||||||
title = ignoreActionTitle,
|
title = ignoreActionTitle,
|
||||||
destructive = true,
|
destructive = true,
|
||||||
editable = false,
|
editable = false,
|
||||||
divider = false,
|
divider = true,
|
||||||
action = { callback?.onIgnoreClicked() }
|
action = { callback?.onIgnoreClicked() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
buildProfileAction(
|
||||||
|
id = "report",
|
||||||
|
title = stringProvider.getString(R.string.message_report_user),
|
||||||
|
destructive = true,
|
||||||
|
editable = false,
|
||||||
|
divider = false,
|
||||||
|
action = { callback?.onReportClicked() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,9 +323,9 @@ class RoomMemberProfileController @Inject constructor(
|
||||||
private fun RoomMemberProfileViewState.buildIgnoreActionTitle(): String? {
|
private fun RoomMemberProfileViewState.buildIgnoreActionTitle(): String? {
|
||||||
val isIgnored = isIgnored() ?: return null
|
val isIgnored = isIgnored() ?: return null
|
||||||
return if (isIgnored) {
|
return if (isIgnored) {
|
||||||
stringProvider.getString(R.string.unignore)
|
stringProvider.getString(R.string.room_participants_action_unignore_title)
|
||||||
} else {
|
} else {
|
||||||
stringProvider.getString(R.string.action_ignore)
|
stringProvider.getString(R.string.room_participants_action_ignore_title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,11 +140,20 @@ class RoomMemberProfileFragment :
|
||||||
is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit
|
is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit
|
||||||
is RoomMemberProfileViewEvents.OnInviteActionSuccess -> Unit
|
is RoomMemberProfileViewEvents.OnInviteActionSuccess -> Unit
|
||||||
RoomMemberProfileViewEvents.GoBack -> handleGoBack()
|
RoomMemberProfileViewEvents.GoBack -> handleGoBack()
|
||||||
|
RoomMemberProfileViewEvents.OnReportActionSuccess -> handleReportSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setupLongClicks()
|
setupLongClicks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleReportSuccess() {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.user_reported_as_inappropriate_title)
|
||||||
|
.setMessage(R.string.user_reported_as_inappropriate_content)
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupLongClicks() {
|
private fun setupLongClicks() {
|
||||||
headerViews.memberProfileNameView.copyOnLongClick()
|
headerViews.memberProfileNameView.copyOnLongClick()
|
||||||
headerViews.memberProfileIdView.copyOnLongClick()
|
headerViews.memberProfileIdView.copyOnLongClick()
|
||||||
|
@ -301,6 +310,10 @@ class RoomMemberProfileFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onReportClicked() {
|
||||||
|
viewModel.handle(RoomMemberProfileAction.ReportUser)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTapVerify() {
|
override fun onTapVerify() {
|
||||||
viewModel.handle(RoomMemberProfileAction.VerifyUser)
|
viewModel.handle(RoomMemberProfileAction.VerifyUser)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents {
|
||||||
data class Failure(val throwable: Throwable) : RoomMemberProfileViewEvents()
|
data class Failure(val throwable: Throwable) : RoomMemberProfileViewEvents()
|
||||||
|
|
||||||
object OnIgnoreActionSuccess : RoomMemberProfileViewEvents()
|
object OnIgnoreActionSuccess : RoomMemberProfileViewEvents()
|
||||||
|
object OnReportActionSuccess : RoomMemberProfileViewEvents()
|
||||||
object OnSetPowerLevelSuccess : RoomMemberProfileViewEvents()
|
object OnSetPowerLevelSuccess : RoomMemberProfileViewEvents()
|
||||||
object OnInviteActionSuccess : RoomMemberProfileViewEvents()
|
object OnInviteActionSuccess : RoomMemberProfileViewEvents()
|
||||||
object OnKickActionSuccess : RoomMemberProfileViewEvents()
|
object OnKickActionSuccess : RoomMemberProfileViewEvents()
|
||||||
|
|
|
@ -161,6 +161,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
|
||||||
when (action) {
|
when (action) {
|
||||||
is RoomMemberProfileAction.RetryFetchingInfo -> handleRetryFetchProfileInfo()
|
is RoomMemberProfileAction.RetryFetchingInfo -> handleRetryFetchProfileInfo()
|
||||||
is RoomMemberProfileAction.IgnoreUser -> handleIgnoreAction()
|
is RoomMemberProfileAction.IgnoreUser -> handleIgnoreAction()
|
||||||
|
is RoomMemberProfileAction.ReportUser -> handleReportAction()
|
||||||
is RoomMemberProfileAction.VerifyUser -> prepareVerification()
|
is RoomMemberProfileAction.VerifyUser -> prepareVerification()
|
||||||
is RoomMemberProfileAction.ShareRoomMemberProfile -> handleShareRoomMemberProfile()
|
is RoomMemberProfileAction.ShareRoomMemberProfile -> handleShareRoomMemberProfile()
|
||||||
is RoomMemberProfileAction.SetPowerLevel -> handleSetPowerLevel(action)
|
is RoomMemberProfileAction.SetPowerLevel -> handleSetPowerLevel(action)
|
||||||
|
@ -172,6 +173,25 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleReportAction() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val event = try {
|
||||||
|
// The API need an Event, use the latest Event.
|
||||||
|
val latestEventId = room?.roomSummary()?.latestPreviewableEvent?.eventId ?: return@launch
|
||||||
|
room.reportingService()
|
||||||
|
.reportContent(
|
||||||
|
eventId = latestEventId,
|
||||||
|
score = -100,
|
||||||
|
reason = "Reporting user ${initialState.userId} (eventId is not relevant)"
|
||||||
|
)
|
||||||
|
RoomMemberProfileViewEvents.OnReportActionSuccess
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
RoomMemberProfileViewEvents.Failure(failure)
|
||||||
|
}
|
||||||
|
_viewEvents.post(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleOpenOrCreateDm(action: RoomMemberProfileAction.OpenOrCreateDm) {
|
private fun handleOpenOrCreateDm(action: RoomMemberProfileAction.OpenOrCreateDm) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_viewEvents.post(RoomMemberProfileViewEvents.Loading())
|
_viewEvents.post(RoomMemberProfileViewEvents.Loading())
|
||||||
|
|
|
@ -27,6 +27,7 @@ import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.auth.PendingAuthHandler
|
import im.vector.app.features.auth.PendingAuthHandler
|
||||||
import im.vector.app.features.login.ReAuthHelper
|
import im.vector.app.features.login.ReAuthHelper
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -52,6 +53,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
|
||||||
private val pendingAuthHandler: PendingAuthHandler,
|
private val pendingAuthHandler: PendingAuthHandler,
|
||||||
) : VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
|
) : VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
|
||||||
|
|
||||||
|
private var observeCrossSigningJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
observeCrossSigning()
|
observeCrossSigning()
|
||||||
}
|
}
|
||||||
|
@ -90,6 +93,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// Force a fast refresh of the data
|
||||||
|
observeCrossSigning()
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
handleInitializeXSigningError(failure)
|
handleInitializeXSigningError(failure)
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -114,7 +119,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
|
||||||
// ) { myDevicesInfo, mxCrossSigningInfo ->
|
// ) { myDevicesInfo, mxCrossSigningInfo ->
|
||||||
// myDevicesInfo to mxCrossSigningInfo
|
// myDevicesInfo to mxCrossSigningInfo
|
||||||
// }
|
// }
|
||||||
session.flow().liveCrossSigningInfo(session.myUserId)
|
observeCrossSigningJob?.cancel()
|
||||||
|
observeCrossSigningJob = session.flow().liveCrossSigningInfo(session.myUserId)
|
||||||
.onEach { data ->
|
.onEach { data ->
|
||||||
val crossSigningKeys = data.getOrNull()
|
val crossSigningKeys = data.getOrNull()
|
||||||
val xSigningIsEnableInAccount = crossSigningKeys != null
|
val xSigningIsEnableInAccount = crossSigningKeys != null
|
||||||
|
@ -128,7 +134,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
|
||||||
xSigningKeyCanSign = xSigningKeyCanSign
|
xSigningKeyCanSign = xSigningKeyCanSign
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.launchIn(viewModelScope)
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleInitializeXSigningError(failure: Throwable) {
|
private fun handleInitializeXSigningError(failure: Throwable) {
|
||||||
|
|
|
@ -0,0 +1,760 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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 im.vector.app.features.analytics
|
||||||
|
|
||||||
|
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||||
|
import im.vector.app.features.analytics.plan.Error
|
||||||
|
import im.vector.app.test.fakes.FakeActiveSessionDataSource
|
||||||
|
import im.vector.app.test.fakes.FakeAnalyticsTracker
|
||||||
|
import im.vector.app.test.fakes.FakeClock
|
||||||
|
import im.vector.app.test.fakes.FakeSession
|
||||||
|
import im.vector.app.test.shared.createTimberTestRule
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.runs
|
||||||
|
import io.mockk.slot
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.amshove.kluent.shouldNotBeEqualTo
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.auth.LoginType
|
||||||
|
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||||
|
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
class DecryptionFailureTrackerTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
fun timberTestRule() = createTimberTestRule()
|
||||||
|
|
||||||
|
private val fakeAnalyticsTracker = FakeAnalyticsTracker()
|
||||||
|
|
||||||
|
private val fakeActiveSessionDataSource = FakeActiveSessionDataSource()
|
||||||
|
|
||||||
|
private val fakeClock = FakeClock()
|
||||||
|
|
||||||
|
private val decryptionFailureTracker = DecryptionFailureTracker(
|
||||||
|
fakeAnalyticsTracker,
|
||||||
|
fakeActiveSessionDataSource.instance,
|
||||||
|
fakeClock
|
||||||
|
)
|
||||||
|
|
||||||
|
private val aCredential = Credentials(
|
||||||
|
userId = "@alice:matrix.org",
|
||||||
|
deviceId = "ABCDEFGHT",
|
||||||
|
homeServer = "http://matrix.org",
|
||||||
|
accessToken = "qwerty",
|
||||||
|
refreshToken = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val fakeMxOrgTestSession = FakeSession().apply {
|
||||||
|
givenSessionParams(
|
||||||
|
SessionParams(
|
||||||
|
credentials = aCredential,
|
||||||
|
homeServerConnectionConfig = mockk(relaxed = true),
|
||||||
|
isTokenValid = true,
|
||||||
|
loginType = LoginType.PASSWORD
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fakeUserId = "@alice:matrix.org"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val aUISIError = MXCryptoError.Base(
|
||||||
|
MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID,
|
||||||
|
"",
|
||||||
|
detailedErrorDescription = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
private val aFakeBobMxOrgEvent = Event(
|
||||||
|
originServerTs = 90_000,
|
||||||
|
eventId = "$000",
|
||||||
|
senderId = "@bob:matrix.org",
|
||||||
|
roomId = "!roomA"
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setupTest() {
|
||||||
|
fakeMxOrgTestSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should report late decryption to analytics tracker`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(any())
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
fakeClock.givenEpoch(100_000)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
fakeSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(true)
|
||||||
|
|
||||||
|
val event = aFakeBobMxOrgEvent
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
// advance time by 5 seconds
|
||||||
|
fakeClock.givenEpoch(105_000)
|
||||||
|
|
||||||
|
// Now simulate it's decrypted
|
||||||
|
decryptionFailureTracker.onEventDecrypted(event, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// it should report
|
||||||
|
verify(exactly = 1) { fakeAnalyticsTracker.capture(any()) }
|
||||||
|
|
||||||
|
verify {
|
||||||
|
fakeAnalyticsTracker.capture(
|
||||||
|
im.vector.app.features.analytics.plan.Error(
|
||||||
|
"mxc_crypto_error_type|",
|
||||||
|
cryptoModule = Error.CryptoModule.Rust,
|
||||||
|
domain = Error.Domain.E2EE,
|
||||||
|
name = Error.Name.OlmKeysNotSentError,
|
||||||
|
cryptoSDK = Error.CryptoSDK.Rust,
|
||||||
|
timeToDecryptMillis = 5000,
|
||||||
|
isFederated = false,
|
||||||
|
isMatrixDotOrg = true,
|
||||||
|
userTrustsOwnIdentity = true,
|
||||||
|
wasVisibleToUser = false
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't do that in @Before function, it wont work as test will fail with:
|
||||||
|
// "the test coroutine is not completing, there were active child jobs"
|
||||||
|
// as the decryptionFailureTracker is setup to use the current test coroutine scope (?)
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should not report graced late decryption to analytics tracker`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val event = aFakeBobMxOrgEvent
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
// advance time by 3 seconds
|
||||||
|
currentFakeTime += 3_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
|
||||||
|
// Now simulate it's decrypted
|
||||||
|
decryptionFailureTracker.onEventDecrypted(
|
||||||
|
event,
|
||||||
|
emptyMap()
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// it should not have reported it
|
||||||
|
verify(exactly = 0) { fakeAnalyticsTracker.capture(any()) }
|
||||||
|
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should report time to decrypt for late decryption`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
val eventSlot = slot<VectorAnalyticsEvent>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(event = capture(eventSlot))
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
fakeSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(true)
|
||||||
|
|
||||||
|
val event = aFakeBobMxOrgEvent
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
// advance time by 7 seconds, to be ahead of the 3 seconds grace period
|
||||||
|
currentFakeTime += 7_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
|
||||||
|
// Now simulate it's decrypted
|
||||||
|
decryptionFailureTracker.onEventDecrypted(
|
||||||
|
event,
|
||||||
|
emptyMap()
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// it should report
|
||||||
|
verify(exactly = 1) { fakeAnalyticsTracker.capture(any()) }
|
||||||
|
|
||||||
|
val error = eventSlot.captured as Error
|
||||||
|
error.timeToDecryptMillis shouldBeEqualTo 7000
|
||||||
|
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should report isMatrixDotOrg`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
val eventSlot = slot<VectorAnalyticsEvent>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(event = capture(eventSlot))
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val event = aFakeBobMxOrgEvent
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// advance time by 7 seconds, to be ahead of the grace period
|
||||||
|
currentFakeTime += 7_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
|
||||||
|
// Now simulate it's decrypted
|
||||||
|
decryptionFailureTracker.onEventDecrypted(event, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val error = eventSlot.captured as Error
|
||||||
|
error.isMatrixDotOrg shouldBeEqualTo true
|
||||||
|
|
||||||
|
val otherSession = FakeSession().apply {
|
||||||
|
givenSessionParams(
|
||||||
|
SessionParams(
|
||||||
|
credentials = aCredential.copy(userId = "@alice:another.org"),
|
||||||
|
homeServerConnectionConfig = mockk(relaxed = true),
|
||||||
|
isTokenValid = true,
|
||||||
|
loginType = LoginType.PASSWORD
|
||||||
|
)
|
||||||
|
)
|
||||||
|
every { sessionId } returns "WWEERE"
|
||||||
|
fakeUserId = "@alice:another.org"
|
||||||
|
this.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(true)
|
||||||
|
}
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(otherSession)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val event2 = aFakeBobMxOrgEvent.copy(eventId = "$001")
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event2, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// advance time by 7 seconds, to be ahead of the grace period
|
||||||
|
currentFakeTime += 7_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
|
||||||
|
// Now simulate it's decrypted
|
||||||
|
decryptionFailureTracker.onEventDecrypted(event2, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).isMatrixDotOrg shouldBeEqualTo false
|
||||||
|
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should report if user trusted it's identity at time of decryption`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
val eventSlot = slot<VectorAnalyticsEvent>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(event = capture(eventSlot))
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
fakeSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(false)
|
||||||
|
val event = aFakeBobMxOrgEvent
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
fakeSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(true)
|
||||||
|
val event2 = aFakeBobMxOrgEvent.copy(eventId = "$001")
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event2, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// advance time by 7 seconds, to be ahead of the grace period
|
||||||
|
currentFakeTime += 7_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
|
||||||
|
// Now simulate it's decrypted
|
||||||
|
decryptionFailureTracker.onEventDecrypted(event, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).userTrustsOwnIdentity shouldBeEqualTo false
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecrypted(event2, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).userTrustsOwnIdentity shouldBeEqualTo true
|
||||||
|
|
||||||
|
verify(exactly = 2) { fakeAnalyticsTracker.capture(any()) }
|
||||||
|
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should not report same event twice`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(any())
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val event = aFakeBobMxOrgEvent
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// advance time by 7 seconds, to be ahead of the grace period
|
||||||
|
currentFakeTime += 7_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
|
||||||
|
// Now simulate it's decrypted
|
||||||
|
decryptionFailureTracker.onEventDecrypted(event, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
verify(exactly = 1) { fakeAnalyticsTracker.capture(any()) }
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// advance time by 7 seconds, to be ahead of the grace period
|
||||||
|
currentFakeTime += 7_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecrypted(event, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
verify(exactly = 1) { fakeAnalyticsTracker.capture(any()) }
|
||||||
|
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should report if isFedrated`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
val eventSlot = slot<VectorAnalyticsEvent>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(event = capture(eventSlot))
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val event = aFakeBobMxOrgEvent
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val event2 = aFakeBobMxOrgEvent.copy(
|
||||||
|
eventId = "$001",
|
||||||
|
senderId = "@bob:another.org",
|
||||||
|
)
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event2, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// advance time by 7 seconds, to be ahead of the grace period
|
||||||
|
currentFakeTime += 7_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
|
||||||
|
// Now simulate it's decrypted
|
||||||
|
decryptionFailureTracker.onEventDecrypted(event, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).isFederated shouldBeEqualTo false
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecrypted(event2, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).isFederated shouldBeEqualTo true
|
||||||
|
|
||||||
|
verify(exactly = 2) { fakeAnalyticsTracker.capture(any()) }
|
||||||
|
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should report if wasVisibleToUser`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
val eventSlot = slot<VectorAnalyticsEvent>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(event = capture(eventSlot))
|
||||||
|
} just runs
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val event = aFakeBobMxOrgEvent
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val event2 = aFakeBobMxOrgEvent.copy(
|
||||||
|
eventId = "$001",
|
||||||
|
senderId = "@bob:another.org",
|
||||||
|
)
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event2, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
decryptionFailureTracker.utdDisplayedInTimeline(
|
||||||
|
mockk<TimelineEvent>(relaxed = true).apply {
|
||||||
|
every { root } returns event2
|
||||||
|
every { eventId } returns event2.eventId.orEmpty()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// advance time by 7 seconds, to be ahead of the grace period
|
||||||
|
currentFakeTime += 7_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
|
||||||
|
// Now simulate it's decrypted
|
||||||
|
decryptionFailureTracker.onEventDecrypted(event, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).wasVisibleToUser shouldBeEqualTo false
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecrypted(event2, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).wasVisibleToUser shouldBeEqualTo true
|
||||||
|
|
||||||
|
verify(exactly = 2) { fakeAnalyticsTracker.capture(any()) }
|
||||||
|
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should report if event relative age to session`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
val historicalEventTimestamp = formatter.parse("2024-03-08 09:24:11")!!.time
|
||||||
|
val sessionCreationTime = formatter.parse("2024-03-09 10:00:00")!!.time
|
||||||
|
// 1mn after creation
|
||||||
|
val liveEventTimestamp = formatter.parse("2024-03-09 10:01:00")!!.time
|
||||||
|
|
||||||
|
val eventSlot = slot<VectorAnalyticsEvent>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(event = capture(eventSlot))
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
fakeSession.fakeCryptoService.cryptoDeviceInfo = CryptoDeviceInfo(
|
||||||
|
deviceId = "ABCDEFGHT",
|
||||||
|
userId = "@alice:matrix.org",
|
||||||
|
firstTimeSeenLocalTs = sessionCreationTime
|
||||||
|
)
|
||||||
|
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val event = aFakeBobMxOrgEvent.copy(
|
||||||
|
originServerTs = historicalEventTimestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val liveEvent = aFakeBobMxOrgEvent.copy(
|
||||||
|
eventId = "$001",
|
||||||
|
originServerTs = liveEventTimestamp
|
||||||
|
)
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(liveEvent, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// advance time by 7 seconds, to be ahead of the grace period
|
||||||
|
currentFakeTime += 7_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
|
||||||
|
// Now simulate historical event late decrypt
|
||||||
|
decryptionFailureTracker.onEventDecrypted(event, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).eventLocalAgeMillis shouldBeEqualTo (historicalEventTimestamp - sessionCreationTime).toInt()
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecrypted(liveEvent, emptyMap())
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).eventLocalAgeMillis shouldBeEqualTo (liveEventTimestamp - sessionCreationTime).toInt()
|
||||||
|
(eventSlot.captured as Error).eventLocalAgeMillis shouldBeEqualTo 60 * 1000
|
||||||
|
|
||||||
|
verify(exactly = 2) { fakeAnalyticsTracker.capture(any()) }
|
||||||
|
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should report historical UTDs as an expected UTD if not verified`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
val historicalEventTimestamp = formatter.parse("2024-03-08 09:24:11")!!.time
|
||||||
|
val sessionCreationTime = formatter.parse("2024-03-09 10:00:00")!!.time
|
||||||
|
|
||||||
|
val eventSlot = slot<VectorAnalyticsEvent>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(event = capture(eventSlot))
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
fakeSession.fakeCryptoService.cryptoDeviceInfo = CryptoDeviceInfo(
|
||||||
|
deviceId = "ABCDEFGHT",
|
||||||
|
userId = "@alice:matrix.org",
|
||||||
|
firstTimeSeenLocalTs = sessionCreationTime
|
||||||
|
)
|
||||||
|
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// historical event and session not verified
|
||||||
|
fakeSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(false)
|
||||||
|
val event = aFakeBobMxOrgEvent.copy(
|
||||||
|
originServerTs = historicalEventTimestamp
|
||||||
|
)
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// advance time to be ahead of the permanent UTD period
|
||||||
|
currentFakeTime += 70_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
advanceTimeBy(70_000)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).name shouldBeEqualTo Error.Name.HistoricalMessage
|
||||||
|
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should not report historical UTDs as an expected UTD if verified`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
val historicalEventTimestamp = formatter.parse("2024-03-08 09:24:11")!!.time
|
||||||
|
val sessionCreationTime = formatter.parse("2024-03-09 10:00:00")!!.time
|
||||||
|
|
||||||
|
val eventSlot = slot<VectorAnalyticsEvent>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(event = capture(eventSlot))
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
fakeSession.fakeCryptoService.cryptoDeviceInfo = CryptoDeviceInfo(
|
||||||
|
deviceId = "ABCDEFGHT",
|
||||||
|
userId = "@alice:matrix.org",
|
||||||
|
firstTimeSeenLocalTs = sessionCreationTime
|
||||||
|
)
|
||||||
|
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// historical event and session not verified
|
||||||
|
fakeSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(true)
|
||||||
|
val event = aFakeBobMxOrgEvent.copy(
|
||||||
|
originServerTs = historicalEventTimestamp
|
||||||
|
)
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// advance time to be ahead of the permanent UTD period
|
||||||
|
currentFakeTime += 70_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
advanceTimeBy(70_000)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).name shouldNotBeEqualTo Error.Name.HistoricalMessage
|
||||||
|
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should not report live UTDs as an expected UTD even if not verified`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||||
|
val sessionCreationTime = formatter.parse("2024-03-09 10:00:00")!!.time
|
||||||
|
// 1mn after creation
|
||||||
|
val liveEventTimestamp = formatter.parse("2024-03-09 10:01:00")!!.time
|
||||||
|
|
||||||
|
val eventSlot = slot<VectorAnalyticsEvent>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(event = capture(eventSlot))
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
fakeSession.fakeCryptoService.cryptoDeviceInfo = CryptoDeviceInfo(
|
||||||
|
deviceId = "ABCDEFGHT",
|
||||||
|
userId = "@alice:matrix.org",
|
||||||
|
firstTimeSeenLocalTs = sessionCreationTime
|
||||||
|
)
|
||||||
|
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// historical event and session not verified
|
||||||
|
fakeSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(false)
|
||||||
|
val event = aFakeBobMxOrgEvent.copy(
|
||||||
|
originServerTs = liveEventTimestamp
|
||||||
|
)
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// advance time to be ahead of the permanent UTD period
|
||||||
|
currentFakeTime += 70_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
advanceTimeBy(70_000)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).name shouldNotBeEqualTo Error.Name.HistoricalMessage
|
||||||
|
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should report if permanent UTD`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
val eventSlot = slot<VectorAnalyticsEvent>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(event = capture(eventSlot))
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val event = aFakeBobMxOrgEvent
|
||||||
|
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(event, aUISIError)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
currentFakeTime += 70_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
advanceTimeBy(70_000)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
verify(exactly = 1) { fakeAnalyticsTracker.capture(any()) }
|
||||||
|
|
||||||
|
(eventSlot.captured as Error).timeToDecryptMillis shouldBeEqualTo -1
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `with multiple UTD`() = runTest {
|
||||||
|
val fakeSession = fakeMxOrgTestSession
|
||||||
|
|
||||||
|
every {
|
||||||
|
fakeAnalyticsTracker.capture(any())
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
var currentFakeTime = 100_000L
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
fakeActiveSessionDataSource.setActiveSession(fakeSession)
|
||||||
|
decryptionFailureTracker.start(CoroutineScope(coroutineContext))
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val events = (0..10).map {
|
||||||
|
aFakeBobMxOrgEvent.copy(
|
||||||
|
eventId = "000$it",
|
||||||
|
originServerTs = 50_000 + it * 1000L
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
events.forEach {
|
||||||
|
decryptionFailureTracker.onEventDecryptionError(it, aUISIError)
|
||||||
|
}
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
currentFakeTime += 70_000
|
||||||
|
fakeClock.givenEpoch(currentFakeTime)
|
||||||
|
advanceTimeBy(70_000)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
verify(exactly = 11) { fakeAnalyticsTracker.capture(any()) }
|
||||||
|
|
||||||
|
decryptionFailureTracker.stop()
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,7 +53,10 @@ class FakeSession(
|
||||||
mockkStatic("im.vector.app.core.extensions.SessionKt")
|
mockkStatic("im.vector.app.core.extensions.SessionKt")
|
||||||
}
|
}
|
||||||
|
|
||||||
override val myUserId: String = "@fake:server.fake"
|
var fakeUserId = "@fake:server.fake"
|
||||||
|
|
||||||
|
override val myUserId: String
|
||||||
|
get() = fakeUserId
|
||||||
|
|
||||||
override val coroutineDispatchers = testCoroutineDispatchers
|
override val coroutineDispatchers = testCoroutineDispatchers
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ class FakeUri(contentEquals: String? = null) {
|
||||||
contentEquals?.let {
|
contentEquals?.let {
|
||||||
givenEquals(it)
|
givenEquals(it)
|
||||||
every { instance.toString() } returns it
|
every { instance.toString() } returns it
|
||||||
|
every { instance.scheme } returns contentEquals.substring(0, contentEquals.indexOf(':'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue