Merge branch 'release/1.6.14' into main
This commit is contained in:
commit
310cecf5cb
|
@ -3,6 +3,7 @@
|
|||
/local.properties
|
||||
# idea files: exclude everything except dictionnaries
|
||||
.idea/caches
|
||||
.idea/copilot
|
||||
.idea/libraries
|
||||
.idea/inspectionProfiles
|
||||
.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)
|
||||
=======================================
|
||||
|
||||
|
@ -5,8 +19,8 @@ This update provides important security fixes, please update now.
|
|||
|
||||
Security fixes 🔐
|
||||
-----------------
|
||||
- Add a check on incoming intent. ([#1506 internal](https://github.com/matrix-org/internal-config/issues/1506))
|
||||
- Store temporary files created for Camera in the media folder. ([#1505 internal](https://github.com/matrix-org/internal-config/issues/1505))
|
||||
- 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 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 🐛
|
||||
----------
|
||||
|
|
|
@ -101,7 +101,7 @@ ext.libs = [
|
|||
],
|
||||
element : [
|
||||
'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 : [
|
||||
'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_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="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_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">"All messages"</string>
|
||||
|
|
|
@ -62,7 +62,7 @@ android {
|
|||
// that the app's state is completely cleared between tests.
|
||||
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_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
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.util.JsonDict
|
||||
|
||||
|
@ -27,7 +28,7 @@ interface LiveEventListener {
|
|||
|
||||
fun onEventDecrypted(event: Event, clearEvent: JsonDict)
|
||||
|
||||
fun onEventDecryptionError(event: Event, throwable: Throwable)
|
||||
fun onEventDecryptionError(event: Event, cryptoError: MXCryptoError)
|
||||
|
||||
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 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 {
|
||||
return olmMachine.decryptRoomEvent(event)
|
||||
return cryptoService.decryptEvent(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.
|
||||
**/
|
||||
internal fun toCryptoDeviceInfo(): CryptoDeviceInfo {
|
||||
// val keys = innerDevice.keys.map { (keyId, key) -> keyId to key }.toMap()
|
||||
|
||||
return CryptoDeviceInfo(
|
||||
deviceId = innerDevice.deviceId,
|
||||
userId = innerDevice.userId,
|
||||
|
|
|
@ -189,18 +189,21 @@ internal class OlmMachine @Inject constructor(
|
|||
is OwnUserIdentity -> ownIdentity.trustsOurOwnDevice()
|
||||
else -> false
|
||||
}
|
||||
val ownDevice = inner.getDevice(userId(), deviceId, 0u)!!
|
||||
val creationTime = ownDevice.firstTimeSeenTs.toLong()
|
||||
|
||||
return CryptoDeviceInfo(
|
||||
deviceId(),
|
||||
userId(),
|
||||
// TODO pass the algorithms here.
|
||||
listOf(),
|
||||
ownDevice.algorithms,
|
||||
keys,
|
||||
mapOf(),
|
||||
UnsignedDeviceInfo(),
|
||||
UnsignedDeviceInfo(
|
||||
deviceDisplayName = ownDevice.displayName
|
||||
),
|
||||
DeviceTrustLevel(crossSigningVerified, locallyVerified = true),
|
||||
false,
|
||||
null
|
||||
creationTime
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -291,7 +294,7 @@ internal class OlmMachine @Inject constructor(
|
|||
// checking the returned to devices to check for room keys.
|
||||
// XXX Anyhow there is now proper signaling we should soon stop parsing them manually
|
||||
receiveSyncChanges.toDeviceEvents.map {
|
||||
outAdapter.fromJson(it) ?: Event()
|
||||
outAdapter.fromJson(it) ?: Event()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -882,6 +885,7 @@ internal class OlmMachine @Inject constructor(
|
|||
inner.queryMissingSecretsFromOtherSessions()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(CryptoStoreException::class)
|
||||
suspend fun enableBackupV1(key: String, version: String) {
|
||||
return withContext(coroutineDispatchers.computation) {
|
||||
|
|
|
@ -497,8 +497,11 @@ internal class RustCryptoService @Inject constructor(
|
|||
@Throws(MXCryptoError::class)
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
return try {
|
||||
olmMachine.decryptRoomEvent(event)
|
||||
olmMachine.decryptRoomEvent(event).also {
|
||||
liveEventManager.get().dispatchLiveEventDecrypted(event, it)
|
||||
}
|
||||
} catch (mxCryptoError: MXCryptoError) {
|
||||
liveEventManager.get().dispatchLiveEventDecryptionFailed(event, mxCryptoError)
|
||||
if (mxCryptoError is MXCryptoError.Base && (
|
||||
mxCryptoError.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID ||
|
||||
mxCryptoError.errorType == MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX)) {
|
||||
|
|
|
@ -22,6 +22,7 @@ import kotlinx.coroutines.SupervisorJob
|
|||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
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.events.model.Event
|
||||
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}")
|
||||
coroutineScope.launch {
|
||||
listeners.forEach {
|
||||
|
|
|
@ -37,7 +37,7 @@ ext.versionMinor = 6
|
|||
// Note: even values are reserved for regular release, odd values for hotfix release.
|
||||
// When creating a hotfix, you should decrease the value, since the current value
|
||||
// is the value for the next regular release.
|
||||
ext.versionPatch = 12
|
||||
ext.versionPatch = 14
|
||||
|
||||
static def getGitTimestamp() {
|
||||
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.pushers.FcmHelper
|
||||
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.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.configuration.VectorConfiguration
|
||||
|
@ -100,6 +101,7 @@ class VectorApplication :
|
|||
@Inject lateinit var callManager: WebRtcCallManager
|
||||
@Inject lateinit var invitesAcceptor: InvitesAcceptor
|
||||
@Inject lateinit var autoRageShaker: AutoRageShaker
|
||||
@Inject lateinit var decryptionFailureTracker: DecryptionFailureTracker
|
||||
@Inject lateinit var vectorFileLogger: VectorFileLogger
|
||||
@Inject lateinit var vectorAnalytics: VectorAnalytics
|
||||
@Inject lateinit var flipperProxy: FlipperProxy
|
||||
|
@ -130,6 +132,7 @@ class VectorApplication :
|
|||
vectorAnalytics.init()
|
||||
invitesAcceptor.initialize()
|
||||
autoRageShaker.initialize()
|
||||
decryptionFailureTracker.start()
|
||||
vectorUncaughtExceptionHandler.activate()
|
||||
|
||||
// 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.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.content.EncryptedEventContent
|
||||
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 roomId = event.roomId
|
||||
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.session.ConfigureAndStartSessionUseCase
|
||||
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.crypto.keysrequest.KeyRequestHandler
|
||||
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
|
||||
|
@ -75,11 +74,6 @@ class ActiveSessionHolder @Inject constructor(
|
|||
session.callSignalingService().addCallListener(callManager)
|
||||
imageManager.onSessionStarted(session)
|
||||
guardServiceStarter.start()
|
||||
decryptionFailureTracker.currentModule = if (session.cryptoService().name() == "rust-sdk") {
|
||||
Error.CryptoModule.Rust
|
||||
} else {
|
||||
Error.CryptoModule.Native
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import im.vector.app.features.analytics.plan.Error
|
||||
import im.vector.lib.core.utils.compat.removeIfCompat
|
||||
import im.vector.lib.core.utils.flow.tickerFlow
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.lib.core.utils.timer.Clock
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
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.onEach
|
||||
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.events.model.Event
|
||||
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.Singleton
|
||||
|
||||
private data class DecryptionFailure(
|
||||
val timeStamp: Long,
|
||||
val roomId: String,
|
||||
val failedEventId: String,
|
||||
val error: MXCryptoError.ErrorType
|
||||
)
|
||||
private typealias DetailedErrorName = Pair<String, Error.Name>
|
||||
|
||||
// If we can decrypt in less than 4s, we don't report
|
||||
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
|
||||
* 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
|
||||
class DecryptionFailureTracker @Inject constructor(
|
||||
private val analyticsTracker: AnalyticsTracker,
|
||||
private val sessionDataSource: ActiveSessionDataSource,
|
||||
private val clock: Clock
|
||||
) {
|
||||
) : Session.Listener, LiveEventListener {
|
||||
|
||||
private val scope: CoroutineScope = CoroutineScope(SupervisorJob())
|
||||
private val failures = mutableListOf<DecryptionFailure>()
|
||||
// The active session (set by the sessionDataSource)
|
||||
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>()
|
||||
|
||||
var currentModule: Error.CryptoModule? = null
|
||||
// Mutex to ensure sequential access to internal state
|
||||
private val mutex = Mutex()
|
||||
|
||||
init {
|
||||
start()
|
||||
}
|
||||
// Used to unsubscribe from the active session data source
|
||||
private lateinit var activeSessionSourceDisposable: Job
|
||||
|
||||
fun start() {
|
||||
tickerFlow(scope, CHECK_INTERVAL)
|
||||
.onEach {
|
||||
checkFailures()
|
||||
}.launchIn(scope)
|
||||
// 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 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) {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val mCryptoError = event.root.mCryptoError
|
||||
if (mCryptoError != null) {
|
||||
addDecryptionFailure(DecryptionFailure(clock.epochMillis(), event.roomId, event.eventId, mCryptoError))
|
||||
} else {
|
||||
removeFailureForEventId(event.eventId)
|
||||
private fun post(block: suspend () -> Unit) {
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be called when the timeline is disposed in order
|
||||
* to grace those events as they are not anymore displayed on screen.
|
||||
* */
|
||||
fun onTimeLineDisposed(roomId: String) {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
synchronized(failures) {
|
||||
failures.removeIfCompat { it.roomId == roomId }
|
||||
private suspend fun rescheduleTicker() {
|
||||
currentTicker = scope.launch {
|
||||
Timber.v("Reschedule ticker")
|
||||
delay(CHECK_INTERVAL)
|
||||
post {
|
||||
checkFailures()
|
||||
currentTicker = null
|
||||
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) {
|
||||
// de duplicate
|
||||
synchronized(failures) {
|
||||
if (failures.none { it.failedEventId == failure.failedEventId }) {
|
||||
failures.add(failure)
|
||||
}
|
||||
override fun onEventDecryptionError(event: Event, cryptoError: MXCryptoError) {
|
||||
Timber.v("Decryption error for event ${event.eventId} with error $cryptoError")
|
||||
val session = activeSession ?: return
|
||||
// track the event
|
||||
post {
|
||||
trackEvent(session, event, cryptoError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeFailureForEventId(eventId: String) {
|
||||
synchronized(failures) {
|
||||
failures.removeIfCompat { it.failedEventId == eventId }
|
||||
override fun onLiveToDeviceEvent(event: Event) {}
|
||||
override fun onLiveEvent(roomId: String, event: Event) {}
|
||||
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() {
|
||||
val now = clock.epochMillis()
|
||||
val aggregatedErrors: Map<DetailedErrorName, List<String>>
|
||||
synchronized(failures) {
|
||||
val toReport = mutableListOf<DecryptionFailure>()
|
||||
failures.removeAll { failure ->
|
||||
(now - failure.timeStamp > GRACE_PERIOD_MILLIS).also {
|
||||
if (it) {
|
||||
toReport.add(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aggregatedErrors = toReport
|
||||
.groupBy { it.error.toAnalyticsErrorName() }
|
||||
.mapValues {
|
||||
it.value.map { it.failedEventId }
|
||||
}
|
||||
Timber.v("Check failures now $now")
|
||||
// report the definitely failed
|
||||
val toReport = trackedEventsMap.values.filter {
|
||||
now - it.timeStamp > MAX_WAIT_MILLIS
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
toReport.forEach {
|
||||
reportFailure(
|
||||
it.copy(timeToDecryptMillis = -1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)")
|
||||
posthog
|
||||
?.takeIf { userConsent == true }
|
||||
?.capture(event.getName(), event.getProperties()?.toPostHogProperties())
|
||||
?.capture(
|
||||
event.getName(),
|
||||
event.getProperties()?.toPostHogProperties()
|
||||
)
|
||||
}
|
||||
|
||||
override fun screen(screen: VectorAnalyticsScreen) {
|
||||
|
|
|
@ -30,11 +30,44 @@ data class Error(
|
|||
*/
|
||||
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,
|
||||
/**
|
||||
* Which crypto backend is the client currently using.
|
||||
*/
|
||||
val cryptoSDK: CryptoSDK? = null,
|
||||
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,
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
enum class Domain {
|
||||
|
@ -44,18 +77,79 @@ data class Error(
|
|||
}
|
||||
|
||||
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,
|
||||
|
||||
/**
|
||||
* E2EE domain error. Generic unknown inbound group session error.
|
||||
*/
|
||||
OlmKeysNotSentError,
|
||||
|
||||
/**
|
||||
* E2EE domain error. Any other decryption error (missing field, format
|
||||
* errors...).
|
||||
*/
|
||||
OlmUnspecifiedError,
|
||||
|
||||
/**
|
||||
* TO_DEVICE domain error. The to-device message failed to decrypt.
|
||||
*/
|
||||
ToDeviceFailedToDecrypt,
|
||||
|
||||
/**
|
||||
* E2EE domain error. Decryption failed due to unknown error.
|
||||
*/
|
||||
UnknownError,
|
||||
|
||||
/**
|
||||
* VOIP domain error. ICE negotiation failed.
|
||||
*/
|
||||
VoipIceFailed,
|
||||
|
||||
/**
|
||||
* VOIP domain error. ICE negotiation timed out.
|
||||
*/
|
||||
VoipIceTimeout,
|
||||
|
||||
/**
|
||||
* VOIP domain error. The call invite timed out.
|
||||
*/
|
||||
VoipInviteTimeout,
|
||||
|
||||
/**
|
||||
* VOIP domain error. The user hung up the call.
|
||||
*/
|
||||
VoipUserHangup,
|
||||
|
||||
/**
|
||||
* VOIP domain error. The user's media failed to start.
|
||||
*/
|
||||
VoipUserMediaFailed,
|
||||
}
|
||||
|
||||
enum class CryptoSDK {
|
||||
|
||||
/**
|
||||
* Legacy crypto backend specific to each platform.
|
||||
*/
|
||||
Legacy,
|
||||
|
||||
/**
|
||||
* Cross-platform crypto backend written in Rust.
|
||||
*/
|
||||
Rust,
|
||||
}
|
||||
|
||||
enum class CryptoModule {
|
||||
|
||||
/**
|
||||
|
@ -75,8 +169,15 @@ data class Error(
|
|||
return mutableMapOf<String, Any>().apply {
|
||||
context?.let { put("context", it) }
|
||||
cryptoModule?.let { put("cryptoModule", it.name) }
|
||||
cryptoSDK?.let { put("cryptoSDK", it.name) }
|
||||
put("domain", domain.name)
|
||||
eventLocalAgeMillis?.let { put("eventLocalAgeMillis", it) }
|
||||
isFederated?.let { put("isFederated", it) }
|
||||
isMatrixDotOrg?.let { put("isMatrixDotOrg", it) }
|
||||
put("name", name.name)
|
||||
timeToDecryptMillis?.let { put("timeToDecryptMillis", it) }
|
||||
userTrustsOwnIdentity?.let { put("userTrustsOwnIdentity", it) }
|
||||
wasVisibleToUser?.let { put("wasVisibleToUser", it) }
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,11 +85,28 @@ data class Interaction(
|
|||
*/
|
||||
MobileRoomAddHome,
|
||||
|
||||
/**
|
||||
* User switched the favourite toggle on Room Details screen.
|
||||
*/
|
||||
MobileRoomFavouriteToggle,
|
||||
|
||||
/**
|
||||
* User tapped on Leave Room button on Room Details screen.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -306,6 +323,18 @@ data class Interaction(
|
|||
*/
|
||||
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
|
||||
* the room list in Element Web/Desktop.
|
||||
|
@ -408,6 +437,18 @@ data class Interaction(
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
|
@ -119,6 +119,12 @@ data class MobileScreen(
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
|
@ -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(
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
val doNotUse: Boolean? = null,
|
||||
|
|
|
@ -27,7 +27,7 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
|||
data class PollVote(
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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,
|
||||
|
||||
/**
|
||||
* Room accessed via interacting with the Threads Activity Centre in
|
||||
* Element Web/Desktop.
|
||||
*/
|
||||
WebThreadsActivityCentre,
|
||||
|
||||
/**
|
||||
* 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.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
@ -43,6 +44,12 @@ class BootstrapReAuthFragment :
|
|||
|
||||
views.bootstrapRetryButton.debouncedClicks { submit() }
|
||||
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 ->
|
||||
|
|
|
@ -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 reason: String,
|
||||
val spam: Boolean = false,
|
||||
val inappropriate: Boolean = false
|
||||
val inappropriate: Boolean = false,
|
||||
val user: Boolean = false,
|
||||
) : RoomDetailAction()
|
||||
|
||||
data class IgnoreUser(val userId: String?) : RoomDetailAction()
|
||||
|
|
|
@ -1345,6 +1345,16 @@ class TimelineFragment :
|
|||
}
|
||||
.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 -> {
|
||||
MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive)
|
||||
.setTitle(R.string.content_reported_title)
|
||||
|
@ -1857,6 +1867,13 @@ class TimelineFragment :
|
|||
is EventSharedAction.IgnoreUser -> {
|
||||
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 -> {
|
||||
onUrlClicked(action.url, action.title)
|
||||
}
|
||||
|
|
|
@ -1484,7 +1484,6 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
override fun onCleared() {
|
||||
timeline?.dispose()
|
||||
timeline?.removeAllListeners()
|
||||
decryptionFailureTracker.onTimeLineDisposed(initialState.roomId)
|
||||
if (vectorPreferences.sendTypingNotifs()) {
|
||||
room?.typingService()?.userStopsTyping()
|
||||
}
|
||||
|
|
|
@ -45,7 +45,9 @@ import com.google.android.material.shape.MaterialShapeDrawable
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.setTextIfDifferent
|
||||
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.createUIHandler
|
||||
import im.vector.app.databinding.ComposerRichTextLayoutBinding
|
||||
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
|
||||
import im.vector.app.features.home.room.detail.composer.images.UriContentListener
|
||||
|
@ -195,10 +197,16 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
|
|||
renderComposerMode(MessageComposerMode.Normal(null))
|
||||
|
||||
views.richTextComposerEditText.addTextChangedListener(
|
||||
TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder(isFullScreen) })
|
||||
TextChangeListener(
|
||||
onTextChanged = {
|
||||
callback?.onTextChanged(it)
|
||||
},
|
||||
onExpandedChanged = { updateTextFieldBorder(isFullScreen) })
|
||||
)
|
||||
views.plainTextComposerEditText.addTextChangedListener(
|
||||
TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder(isFullScreen) })
|
||||
TextChangeListener({
|
||||
callback?.onTextChanged(it)
|
||||
}, { updateTextFieldBorder(isFullScreen) })
|
||||
)
|
||||
ViewCompat.setOnReceiveContentListener(
|
||||
views.richTextComposerEditText,
|
||||
|
@ -516,18 +524,21 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
|
|||
private val onTextChanged: (s: Editable) -> Unit,
|
||||
private val onExpandedChanged: (isExpanded: Boolean) -> Unit,
|
||||
) : TextWatcher {
|
||||
|
||||
private val debouncer = Debouncer(createUIHandler())
|
||||
private var previousTextWasExpanded = false
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
onTextChanged.invoke(s)
|
||||
|
||||
val isExpanded = s.lines().count() > 1
|
||||
if (previousTextWasExpanded != isExpanded) {
|
||||
onExpandedChanged(isExpanded)
|
||||
debouncer.debounce("afterTextChanged", 50L) {
|
||||
onTextChanged.invoke(s)
|
||||
val isExpanded = s.lines().count() > 1
|
||||
if (previousTextWasExpanded != isExpanded) {
|
||||
onExpandedChanged(isExpanded)
|
||||
}
|
||||
previousTextWasExpanded = isExpanded
|
||||
}
|
||||
previousTextWasExpanded = isExpanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,6 +98,9 @@ sealed class EventSharedAction(
|
|||
data class IgnoreUser(val senderId: String?) :
|
||||
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) :
|
||||
EventSharedAction(0, 0)
|
||||
|
||||
|
|
|
@ -430,6 +430,12 @@ class MessageActionsViewModel @AssistedInject constructor(
|
|||
|
||||
add(EventSharedAction.Separator)
|
||||
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)
|
||||
}
|
||||
}.also {
|
||||
if (it != null && event.isEncrypted()) {
|
||||
decryptionFailureTracker.e2eEventDisplayedInTimeline(event)
|
||||
if (it != null && event.isEncrypted() && event.root.mCryptoError != null) {
|
||||
decryptionFailureTracker.utdDisplayedInTimeline(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import im.vector.app.core.platform.VectorViewModelAction
|
|||
sealed class RoomMemberProfileAction : VectorViewModelAction {
|
||||
object RetryFetchingInfo : RoomMemberProfileAction()
|
||||
object IgnoreUser : RoomMemberProfileAction()
|
||||
object ReportUser : RoomMemberProfileAction()
|
||||
data class BanOrUnbanUser(val reason: String?) : RoomMemberProfileAction()
|
||||
data class KickUser(val reason: String?) : RoomMemberProfileAction()
|
||||
object InviteUser : RoomMemberProfileAction()
|
||||
|
|
|
@ -39,6 +39,7 @@ class RoomMemberProfileController @Inject constructor(
|
|||
|
||||
interface Callback {
|
||||
fun onIgnoreClicked()
|
||||
fun onReportClicked()
|
||||
fun onTapVerify()
|
||||
fun onShowDeviceList()
|
||||
fun onShowDeviceListNoCrossSigning()
|
||||
|
@ -225,7 +226,7 @@ class RoomMemberProfileController @Inject constructor(
|
|||
title = stringProvider.getString(R.string.room_participants_action_invite),
|
||||
destructive = false,
|
||||
editable = false,
|
||||
divider = ignoreActionTitle != null,
|
||||
divider = true,
|
||||
action = { callback?.onInviteClicked() }
|
||||
)
|
||||
}
|
||||
|
@ -235,10 +236,18 @@ class RoomMemberProfileController @Inject constructor(
|
|||
title = ignoreActionTitle,
|
||||
destructive = true,
|
||||
editable = false,
|
||||
divider = false,
|
||||
divider = true,
|
||||
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? {
|
||||
val isIgnored = isIgnored() ?: return null
|
||||
return if (isIgnored) {
|
||||
stringProvider.getString(R.string.unignore)
|
||||
stringProvider.getString(R.string.room_participants_action_unignore_title)
|
||||
} 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.OnInviteActionSuccess -> Unit
|
||||
RoomMemberProfileViewEvents.GoBack -> handleGoBack()
|
||||
RoomMemberProfileViewEvents.OnReportActionSuccess -> handleReportSuccess()
|
||||
}
|
||||
}
|
||||
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() {
|
||||
headerViews.memberProfileNameView.copyOnLongClick()
|
||||
headerViews.memberProfileIdView.copyOnLongClick()
|
||||
|
@ -301,6 +310,10 @@ class RoomMemberProfileFragment :
|
|||
}
|
||||
}
|
||||
|
||||
override fun onReportClicked() {
|
||||
viewModel.handle(RoomMemberProfileAction.ReportUser)
|
||||
}
|
||||
|
||||
override fun onTapVerify() {
|
||||
viewModel.handle(RoomMemberProfileAction.VerifyUser)
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents {
|
|||
data class Failure(val throwable: Throwable) : RoomMemberProfileViewEvents()
|
||||
|
||||
object OnIgnoreActionSuccess : RoomMemberProfileViewEvents()
|
||||
object OnReportActionSuccess : RoomMemberProfileViewEvents()
|
||||
object OnSetPowerLevelSuccess : RoomMemberProfileViewEvents()
|
||||
object OnInviteActionSuccess : RoomMemberProfileViewEvents()
|
||||
object OnKickActionSuccess : RoomMemberProfileViewEvents()
|
||||
|
|
|
@ -161,6 +161,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
|
|||
when (action) {
|
||||
is RoomMemberProfileAction.RetryFetchingInfo -> handleRetryFetchProfileInfo()
|
||||
is RoomMemberProfileAction.IgnoreUser -> handleIgnoreAction()
|
||||
is RoomMemberProfileAction.ReportUser -> handleReportAction()
|
||||
is RoomMemberProfileAction.VerifyUser -> prepareVerification()
|
||||
is RoomMemberProfileAction.ShareRoomMemberProfile -> handleShareRoomMemberProfile()
|
||||
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) {
|
||||
viewModelScope.launch {
|
||||
_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.login.ReAuthHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -52,6 +53,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
|
|||
private val pendingAuthHandler: PendingAuthHandler,
|
||||
) : VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
|
||||
|
||||
private var observeCrossSigningJob: Job? = null
|
||||
|
||||
init {
|
||||
observeCrossSigning()
|
||||
}
|
||||
|
@ -90,6 +93,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
})
|
||||
// Force a fast refresh of the data
|
||||
observeCrossSigning()
|
||||
} catch (failure: Throwable) {
|
||||
handleInitializeXSigningError(failure)
|
||||
} finally {
|
||||
|
@ -114,7 +119,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
|
|||
// ) { myDevicesInfo, mxCrossSigningInfo ->
|
||||
// myDevicesInfo to mxCrossSigningInfo
|
||||
// }
|
||||
session.flow().liveCrossSigningInfo(session.myUserId)
|
||||
observeCrossSigningJob?.cancel()
|
||||
observeCrossSigningJob = session.flow().liveCrossSigningInfo(session.myUserId)
|
||||
.onEach { data ->
|
||||
val crossSigningKeys = data.getOrNull()
|
||||
val xSigningIsEnableInAccount = crossSigningKeys != null
|
||||
|
@ -128,7 +134,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
|
|||
xSigningKeyCanSign = xSigningKeyCanSign
|
||||
)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
override val myUserId: String = "@fake:server.fake"
|
||||
var fakeUserId = "@fake:server.fake"
|
||||
|
||||
override val myUserId: String
|
||||
get() = fakeUserId
|
||||
|
||||
override val coroutineDispatchers = testCoroutineDispatchers
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ class FakeUri(contentEquals: String? = null) {
|
|||
contentEquals?.let {
|
||||
givenEquals(it)
|
||||
every { instance.toString() } returns it
|
||||
every { instance.scheme } returns contentEquals.substring(0, contentEquals.indexOf(':'))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue